Merge branch 'master' into master.7306.symbolY

# Conflicts:
#	src/Components/Canvas/Symbol.ts
#	src/Components/SVG/Elements/DimensionLayer.tsx
#	src/Components/SVG/Elements/SelectorSymbol/SelectorSymbol.tsx
This commit is contained in:
Carl Fuchs 2023-02-20 10:21:54 +01:00
commit d72c2266f3
52 changed files with 1967 additions and 913 deletions

View file

@ -19,7 +19,8 @@ module.exports = {
}, },
ecmaVersion: 'latest', ecmaVersion: 'latest',
sourceType: 'module', sourceType: 'module',
project: './tsconfig.json' project: './tsconfig.json',
tsconfigRootDir: __dirname
}, },
plugins: [ plugins: [
'only-warn', 'only-warn',
@ -31,6 +32,7 @@ module.exports = {
'prefer-arrow-callback': 'error', 'prefer-arrow-callback': 'error',
'func-style': ['error', 'declaration'], 'func-style': ['error', 'declaration'],
'space-before-function-paren': ['error', 'never'], 'space-before-function-paren': ['error', 'never'],
'max-len': ['error', { 'code': 120 }],
// Import/export // Import/export
'import/no-default-export': 'error', 'import/no-default-export': 'error',

View file

@ -9,7 +9,20 @@ You will be able to navigate through this document with the table of contents.
- [Table of contents](#table-of-contents) - [Table of contents](#table-of-contents)
- [I want to contribute](#i-want-to-contribute) - [I want to contribute](#i-want-to-contribute)
- [I want to contribute to the .NETFramework API](#i-want-to-contribute-to-the-netframework-api) - [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) - [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) - [I want to report a bug](#i-want-to-report-a-bug)
- [Before submitting a bug report](#before-submitting-a-bug-report) - [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) - [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 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 ### Testing the external API without .NETFramework or Windows
Use the Node.js server in `/test-server` to simulate the api. Use the Node.js server in `/test-server` to simulate the api.

View file

@ -20,9 +20,12 @@ namespace SVGLDLibs.Models
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string color; public string color;
/** width */
[DataMember(EmitDefaultValue = false)]
public double width;
/** color */
[DataMember(EmitDefaultValue = false)]
public string dashArray;
} }
} }

7
mprocs.yaml Normal file
View file

@ -0,0 +1,7 @@
procs:
nvim:
shell: "nvim ."
vite:
shell: "npx vite"
test-server:
shell: "npx nodemon ./test-server/http.js"

View file

@ -3,11 +3,12 @@
"private": true, "private": true,
"version": "v1.0.0", "version": "v1.0.0",
"type": "module", "type": "module",
"postinstall": "npx patch-package",
"scripts": { "scripts": {
"d": "mprocs",
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"build:dotnet": "dotnet build ./csharp/SVGLDLibs/SVGLDLibs/SVGLDLibs.csproj", "build:dotnet": "dotnet build ./csharp/SVGLDLibs/SVGLDLibs/SVGLDLibs.csproj",
"preview": "vite preview",
"linter": "eslint src", "linter": "eslint src",
"test": "vitest", "test": "vitest",
"test:ui": "vitest --ui", "test:ui": "vitest --ui",
@ -22,10 +23,11 @@
"interweave": "^13.0.0", "interweave": "^13.0.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^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", "react-window": "^1.8.8",
"sweetalert2": "^11.7.1", "sweetalert2": "^11.7.1",
"sweetalert2-react-content": "^5.0.7" "sweetalert2-react-content": "^5.0.7",
"transformation-matrix": "^2.14.0"
}, },
"devDependencies": { "devDependencies": {
"@testing-library/dom": "^8.20.0", "@testing-library/dom": "^8.20.0",
@ -52,11 +54,18 @@
"eslint-plugin-react": "^7.32.2", "eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"jsdom": "^21.1.0", "jsdom": "^21.1.0",
"mprocs": "^0.6.4",
"nodemon": "^2.0.20",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"sass": "^1.58.0", "sass": "^1.58.0",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"vite": "^4.1.1", "vite": "^4.1.1",
"vitest": "^0.28.4" "vitest": "^0.28.4"
},
"pnpm": {
"patchedDependencies": {
"@types/react-svg-pan-zoom@3.3.5": "patches/@types__react-svg-pan-zoom@3.3.5.patch"
}
} }
} }

View file

@ -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;

View file

@ -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;

109
pnpm-lock.yaml generated
View file

@ -1,5 +1,10 @@
lockfileVersion: 5.4 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: specifiers:
'@heroicons/react': ^2.0.14 '@heroicons/react': ^2.0.14
'@react-hook/size': ^2.1.2 '@react-hook/size': ^2.1.2
@ -28,15 +33,18 @@ specifiers:
eslint-plugin-react-hooks: ^4.6.0 eslint-plugin-react-hooks: ^4.6.0
interweave: ^13.0.0 interweave: ^13.0.0
jsdom: ^21.1.0 jsdom: ^21.1.0
mprocs: ^0.6.4
nodemon: ^2.0.20
postcss: ^8.4.21 postcss: ^8.4.21
react: ^18.2.0 react: ^18.2.0
react-dom: ^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 react-window: ^1.8.8
sass: ^1.58.0 sass: ^1.58.0
sweetalert2: ^11.7.1 sweetalert2: ^11.7.1
sweetalert2-react-content: ^5.0.7 sweetalert2-react-content: ^5.0.7
tailwindcss: ^3.2.4 tailwindcss: ^3.2.4
transformation-matrix: ^2.14.0
typescript: ^4.9.5 typescript: ^4.9.5
vite: ^4.1.1 vite: ^4.1.1
vitest: ^0.28.4 vitest: ^0.28.4
@ -47,10 +55,11 @@ dependencies:
interweave: 13.0.0_react@18.2.0 interweave: 13.0.0_react@18.2.0
react: 18.2.0 react: 18.2.0
react-dom: 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 react-window: 1.8.8_biqbaboplfbrettd7655fr4n2y
sweetalert2: 11.7.1 sweetalert2: 11.7.1
sweetalert2-react-content: 5.0.7_5cbezu6w3tvev2ldv5vdmnpfca sweetalert2-react-content: 5.0.7_5cbezu6w3tvev2ldv5vdmnpfca
transformation-matrix: 2.14.0
devDependencies: devDependencies:
'@testing-library/dom': 8.20.0 '@testing-library/dom': 8.20.0
@ -59,7 +68,7 @@ devDependencies:
'@testing-library/user-event': 14.4.3_yxlyej73nftwmh2fiao7paxmlm '@testing-library/user-event': 14.4.3_yxlyej73nftwmh2fiao7paxmlm
'@types/react': 18.0.27 '@types/react': 18.0.27
'@types/react-dom': 18.0.10 '@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 '@types/react-window': 1.8.5
'@typescript-eslint/eslint-plugin': 5.51.0_b635kmla6dsb4frxfihkw4m47e '@typescript-eslint/eslint-plugin': 5.51.0_b635kmla6dsb4frxfihkw4m47e
'@typescript-eslint/parser': 5.51.0_4vsywjlpuriuw3tl5oq6zy5a64 '@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: 7.32.2_eslint@8.33.0
eslint-plugin-react-hooks: 4.6.0_eslint@8.33.0 eslint-plugin-react-hooks: 4.6.0_eslint@8.33.0
jsdom: 21.1.0 jsdom: 21.1.0
mprocs: 0.6.4
nodemon: 2.0.20
postcss: 8.4.21 postcss: 8.4.21
sass: 1.58.0 sass: 1.58.0
tailwindcss: 3.2.4_postcss@8.4.21 tailwindcss: 3.2.4_postcss@8.4.21
@ -923,11 +934,12 @@ packages:
'@types/react': 18.0.27 '@types/react': 18.0.27
dev: true dev: true
/@types/react-svg-pan-zoom/3.3.5: /@types/react-svg-pan-zoom/3.3.5_kv3ctd73j5hnzcxdc2ceiq5wuy:
resolution: {integrity: sha512-W8GRFCDy7raSDr5OXGjSyvX5KmdWlIQfv0NLa1jfAYVUO4ClVbgorWeAAom7nY3Pl+4h9blXE1Bnu2CW1iMEvQ==} resolution: {integrity: sha512-W8GRFCDy7raSDr5OXGjSyvX5KmdWlIQfv0NLa1jfAYVUO4ClVbgorWeAAom7nY3Pl+4h9blXE1Bnu2CW1iMEvQ==}
dependencies: dependencies:
'@types/react': 18.0.27 '@types/react': 18.0.27
dev: true dev: true
patched: true
/@types/react-window/1.8.5: /@types/react-window/1.8.5:
resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==} resolution: {integrity: sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==}
@ -1172,6 +1184,10 @@ packages:
resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==}
dev: true dev: true
/abbrev/1.1.1:
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
dev: true
/acorn-globals/7.0.1: /acorn-globals/7.0.1:
resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==}
dependencies: dependencies:
@ -1600,6 +1616,18 @@ packages:
ms: 2.1.3 ms: 2.1.3
dev: true 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: /debug/4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
engines: {node: '>=6.0'} engines: {node: '>=6.0'}
@ -2507,6 +2535,10 @@ packages:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
dev: true dev: true
/ignore-by-default/1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
dev: true
/ignore/5.2.4: /ignore/5.2.4:
resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
engines: {node: '>= 4'} engines: {node: '>= 4'}
@ -3010,6 +3042,12 @@ packages:
ufo: 1.0.1 ufo: 1.0.1
dev: true 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: /mrmime/1.0.1:
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3041,6 +3079,30 @@ packages:
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==} resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
dev: true 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: /normalize-path/3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -3352,6 +3414,10 @@ packages:
resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==}
dev: true dev: true
/pstree.remy/1.1.8:
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
dev: true
/punycode/2.3.0: /punycode/2.3.0:
resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3395,13 +3461,10 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
dev: true dev: true
/react-svg-pan-zoom/3.11.0_react@18.2.0: /react-svg-pan-zoom/3.12.1:
resolution: {integrity: sha512-xK2tpfp4YksHOfyMZH5zXP52ARLSBgkoJgWNJmJ1B+6O1tkuf23TQp7Q4m9GG5IRSK5KWO0JEGEWlNYG9+iiug==} resolution: {integrity: sha512-ug1LHCN5qed56C64xFypr/ClajuMFkig1OKvwJrIgGeSyHOjWM7XGgSgeP3IfHAkNw8QEc6a31ggZRpTijWYRw==}
peerDependencies:
react: '>=17.0.0'
dependencies: dependencies:
prop-types: 15.8.1 prop-types: 15.8.1
react: 18.2.0
transformation-matrix: 2.14.0 transformation-matrix: 2.14.0
dev: false dev: false
@ -3549,11 +3612,21 @@ packages:
dependencies: dependencies:
loose-envify: 1.4.0 loose-envify: 1.4.0
/semver/5.7.1:
resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==}
hasBin: true
dev: true
/semver/6.3.0: /semver/6.3.0:
resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==}
hasBin: true hasBin: true
dev: true dev: true
/semver/7.0.0:
resolution: {integrity: sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==}
hasBin: true
dev: true
/semver/7.3.8: /semver/7.3.8:
resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3586,6 +3659,13 @@ packages:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
dev: true 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: /sirv/2.0.2:
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
@ -3831,6 +3911,13 @@ packages:
engines: {node: '>=6'} engines: {node: '>=6'}
dev: true 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: /tough-cookie/4.1.2:
resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3926,6 +4013,10 @@ packages:
which-boxed-primitive: 1.0.2 which-boxed-primitive: 1.0.2
dev: true dev: true
/undefsafe/2.0.5:
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
dev: true
/universalify/0.2.0: /universalify/0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}

View file

@ -10,7 +10,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel'; import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { IHistoryState } from '../../Interfaces/IHistoryState';
import { IPattern } from '../../Interfaces/IPattern'; 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'; import { FetchConfiguration } from './api';
const CSHARP_WEB_API_BASE_URL = 'http://localhost:5209/'; const CSHARP_WEB_API_BASE_URL = 'http://localhost:5209/';
@ -144,23 +144,11 @@ describe.concurrent('Models test suite', () => {
PositionReference: 0, PositionReference: 0,
HideChildrenInTreeview: true, HideChildrenInTreeview: true,
DimensionOptions: { DimensionOptions: {
childrenDimensions: { childrenDimensions: DEFAULT_DIMENSION_OPTION,
color: '#000000', selfDimensions: DEFAULT_DIMENSION_OPTION,
positions: [] selfMarginsDimensions: DEFAULT_DIMENSION_OPTION,
},
selfDimensions: {
color: '#000000',
positions: []
},
selfMarginsDimensions: {
color: '#000000',
positions: []
},
markPosition: [], markPosition: [],
dimensionWithMarks: { dimensionWithMarks: DEFAULT_DIMENSION_OPTION
color: '#000000',
positions: []
}
}, },
IsHidden: true, IsHidden: true,
Blacklist: [ Blacklist: [

View file

@ -17,6 +17,7 @@ import { BarIcon } from './BarIcon';
import { Text } from '../Text/Text'; import { Text } from '../Text/Text';
interface IBarProps { interface IBarProps {
className: string
isComponentsOpen: boolean isComponentsOpen: boolean
isSymbolsOpen: boolean isSymbolsOpen: boolean
isHistoryOpen: boolean isHistoryOpen: boolean
@ -33,7 +34,7 @@ export const BAR_WIDTH = 64; // 4rem
export function Bar(props: IBarProps): JSX.Element { export function Bar(props: IBarProps): JSX.Element {
return ( return (
<div className='bar'> <div className={`bar ${props.className}`}>
<BarIcon <BarIcon
isActive={props.isComponentsOpen} isActive={props.isComponentsOpen}
title={Text({ textId: '@Components' })} title={Text({ textId: '@Components' })}

View file

@ -1,16 +1,85 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { IPoint } from '../../Interfaces/IPoint'; import { type IPoint } from '../../Interfaces/IPoint';
import { BAR_WIDTH } from '../Bar/Bar'; import { BAR_WIDTH } from '../Bar/Bar';
import { SelectorMode } from '../SVG/SVG';
import { type DrawParams } from '../Viewer/Viewer';
import { RenderContainers, RenderSymbols } from './Renderer';
import { RenderContainerSelector } from './SelectorContainer';
import { RenderSymbolSelector } from './SelectorSymbol';
interface ICanvasProps { interface ICanvasProps {
className?: string
width: number width: number
height: number height: number
draw: (context: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint) => void
className?: string
style?: React.CSSProperties style?: React.CSSProperties
drawParams: DrawParams
}; };
function UseCanvas(draw: (context: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint) => void): React.RefObject<HTMLCanvasElement> { 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<HTMLCanvasElement> {
const canvasRef = useRef<HTMLCanvasElement>(null); const canvasRef = useRef<HTMLCanvasElement>(null);
const frameCount = useRef(0); const frameCount = useRef(0);
const translatePos = useRef({ const translatePos = useRef({
@ -123,8 +192,21 @@ interface Viewer {
viewerHeight: number viewerHeight: number
} }
export function Canvas({ width, height, draw, style, className }: ICanvasProps): JSX.Element { export function Canvas({
const canvasRef = UseCanvas(draw); className,
width,
height,
style,
drawParams
}: ICanvasProps): JSX.Element {
const canvasRef = UseCanvas((
...CanvasProps
) => {
Draw(
...CanvasProps,
drawParams
);
});
const [{ viewerWidth, viewerHeight }, setViewer] = React.useState<Viewer>({ const [{ viewerWidth, viewerHeight }, setViewer] = React.useState<Viewer>({
viewerWidth: width, viewerWidth: width,

View file

@ -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();
}

View file

@ -1,4 +1,5 @@
import { NOTCHES_LENGTH } from '../../utils/default'; import { NOTCHES_LENGTH } from '../../utils/default';
import { IDimensionStyle } from '../SVG/Elements/Dimension';
interface IDimensionProps { interface IDimensionProps {
id: string id: string
@ -7,7 +8,7 @@ interface IDimensionProps {
xEnd: number xEnd: number
yEnd: number yEnd: number
text: string text: string
strokeWidth: number style: IDimensionStyle
scale?: number scale?: number
} }
@ -26,8 +27,11 @@ function ApplyParametric(x0: number, t: number, vx: number): number {
export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimensionProps): void { export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimensionProps): void {
const scale = props.scale ?? 1; const scale = props.scale ?? 1;
const strokeStyle = 'black'; const strokeStyle = props.style.color ?? 'black';
const lineWidth = 2 / scale; 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 /// We need to find the points of the notches
// Get the vector of the line // Get the vector of the line
@ -59,6 +63,7 @@ export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimension
ctx.lineWidth = lineWidth; ctx.lineWidth = lineWidth;
ctx.strokeStyle = strokeStyle; ctx.strokeStyle = strokeStyle;
ctx.fillStyle = strokeStyle; ctx.fillStyle = strokeStyle;
ctx.setLineDash(dashArray);
ctx.moveTo(startTopX, startTopY); ctx.moveTo(startTopX, startTopY);
ctx.lineTo(startBottomX, startBottomY); ctx.lineTo(startBottomX, startBottomY);
ctx.stroke(); ctx.stroke();
@ -68,6 +73,7 @@ export function RenderDimension(ctx: CanvasRenderingContext2D, props: IDimension
ctx.moveTo(endTopX, endTopY); ctx.moveTo(endTopX, endTopY);
ctx.lineTo(endBottomX, endBottomY); ctx.lineTo(endBottomX, endBottomY);
ctx.stroke(); ctx.stroke();
ctx.setLineDash([]);
const textX = (props.xStart + props.xEnd) / 2; const textX = (props.xStart + props.xEnd) / 2;
const textY = (props.yStart + props.yEnd) / 2; const textY = (props.yStart + props.yEnd) / 2;
ctx.font = `${16 / scale}px Verdana`; ctx.font = `${16 / scale}px Verdana`;

View file

@ -1,14 +1,14 @@
import { Orientation } from '../../Enums/Orientation'; import { Orientation } from '../../Enums/Orientation';
import { Position } from '../../Enums/Position'; import { Position } from '../../Enums/Position';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { type IContainerModel } from '../../Interfaces/IContainerModel';
import { SHOW_SELF_DIMENSIONS, SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS } from '../../utils/default'; 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 { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../utils/itertools';
import { TransformX, TransformY } from '../../utils/svg'; import { TransformX, TransformY } from '../../utils/svg';
import { type IDimensionStyle } from '../SVG/Elements/Dimension';
import { RenderDimension } from './Dimension'; import { RenderDimension } from './Dimension';
const MODULE_STROKE_WIDTH = 1; export function AddContainerDimensions(
export function AddDimensions(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
containers: Map<string, IContainerModel>, containers: Map<string, IContainerModel>,
container: IContainerModel, container: IContainerModel,
@ -18,7 +18,8 @@ export function AddDimensions(
depth: number depth: number
): void { ): void {
ctx.beginPath(); 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( ActionByPosition(
ctx, ctx,
dimMapped, 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( ActionByPosition(
ctx, ctx,
dimMapped, 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( ActionByPosition(
ctx, ctx,
dimMapped, 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 * Fonction that call another function given the positions
* @param dimMapped Position mapped depending on the Position enum in order: * @param dimMapped Position mapped depending on the Position enum in order:
@ -108,6 +142,7 @@ function AddHorizontalChildrenDimension(
scale: number scale: number
): void { ): void {
const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`; 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 lastChildId = container.children[container.children.length - 1];
const lastChild = FindContainerById(containers, lastChildId); const lastChild = FindContainerById(containers, lastChildId);
@ -115,8 +150,14 @@ function AddHorizontalChildrenDimension(
if (lastChild === undefined) { if (lastChild === undefined) {
return; return;
} }
let xChildrenStart = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); let xChildrenStart = TransformX(
let xChildrenEnd = TransformX(lastChild.properties.x, lastChild.properties.width, lastChild.properties.positionReference); 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 // Find the min and max
for (let i = container.children.length - 2; i >= 0; i--) { for (let i = container.children.length - 2; i >= 0; i--) {
@ -152,9 +193,9 @@ function AddHorizontalChildrenDimension(
xEnd: xChildrenEnd + offset, xEnd: xChildrenEnd + offset,
yStart: yDim, yStart: yDim,
yEnd: yDim, yEnd: yDim,
strokeWidth: MODULE_STROKE_WIDTH,
text: textChildren, text: textChildren,
scale scale,
style
}); });
} }
@ -168,6 +209,7 @@ function AddVerticalChildrenDimension(
scale: number scale: number
): void { ): void {
const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; 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 lastChildId = container.children[container.children.length - 1];
const lastChild = FindContainerById(containers, lastChildId); const lastChild = FindContainerById(containers, lastChildId);
@ -176,7 +218,10 @@ function AddVerticalChildrenDimension(
return; 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; let yChildrenEnd = yChildrenStart;
// Find the min and max // Find the min and max
@ -218,9 +263,9 @@ function AddVerticalChildrenDimension(
xEnd: xDim, xEnd: xDim,
yStart: yChildrenStart + offset, yStart: yChildrenStart + offset,
yEnd: yChildrenEnd + offset, yEnd: yChildrenEnd + offset,
strokeWidth: MODULE_STROKE_WIDTH,
text: textChildren, text: textChildren,
scale scale,
style
}); });
} }
@ -234,6 +279,7 @@ function AddHorizontalBorrowerDimension(
scale: number scale: number
): void { ): void {
const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const style = container.properties.dimensionOptions.dimensionWithMarks;
const marks = []; // list of vertical lines for the dimension const marks = []; // list of vertical lines for the dimension
for (const { for (const {
container: childContainer, currentTransform: childCurrentTransform container: childContainer, currentTransform: childCurrentTransform
@ -274,9 +320,9 @@ function AddHorizontalBorrowerDimension(
xEnd: next, xEnd: next,
yStart: yDim, yStart: yDim,
yEnd: yDim, yEnd: yDim,
strokeWidth: MODULE_STROKE_WIDTH,
text: value.toFixed(0), text: value.toFixed(0),
scale scale,
style
}); });
count++; count++;
} }
@ -293,6 +339,7 @@ function AddVerticalBorrowerDimension(
scale: number scale: number
): void { ): void {
const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const style = container.properties.dimensionOptions.dimensionWithMarks;
const marks = []; // list of vertical lines for the dimension const marks = []; // list of vertical lines for the dimension
for (const { for (const {
container: childContainer, currentTransform: childCurrentTransform container: childContainer, currentTransform: childCurrentTransform
@ -338,9 +385,9 @@ function AddVerticalBorrowerDimension(
xEnd: xDim, xEnd: xDim,
yStart: cur, yStart: cur,
yEnd: next, yEnd: next,
strokeWidth: MODULE_STROKE_WIDTH,
text: value.toFixed(0), text: value.toFixed(0),
scale scale,
style
}); });
count++; count++;
} }
@ -354,6 +401,7 @@ function AddVerticalSelfDimension(
currentTransform: [number, number], currentTransform: [number, number],
scale: number scale: number
): void { ): void {
const style = container.properties.dimensionOptions.selfDimensions;
const height = container.properties.height; const height = container.properties.height;
const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`; const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`;
let yStart = container.properties.y + currentTransform[1] + height; let yStart = container.properties.y + currentTransform[1] + height;
@ -372,9 +420,9 @@ function AddVerticalSelfDimension(
xEnd: xDim, xEnd: xDim,
yStart, yStart,
yEnd, yEnd,
strokeWidth: MODULE_STROKE_WIDTH,
text: textVert, text: textVert,
scale scale,
style
}); });
} }
@ -385,6 +433,7 @@ function AddHorizontalSelfDimension(
currentTransform: [number, number], currentTransform: [number, number],
scale: number scale: number
): void { ): void {
const style = container.properties.dimensionOptions.selfDimensions;
const width = container.properties.width; const width = container.properties.width;
const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`; const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`;
const xStart = container.properties.x + currentTransform[0]; const xStart = container.properties.x + currentTransform[0];
@ -398,8 +447,154 @@ function AddHorizontalSelfDimension(
yStart: yDim, yStart: yDim,
xEnd, xEnd,
yEnd: yDim, yEnd: yDim,
strokeWidth: MODULE_STROKE_WIDTH,
text, 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
}); });
} }

View file

@ -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<string, IContainerModel>,
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<string, IContainerModel>,
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<string, ISymbolModel>,
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<string, IContainerModel>,
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();
}

View file

@ -1,33 +1,26 @@
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { SHOW_SELECTOR_TEXT } from '../../utils/default'; import { SHOW_SELECTOR_TEXT } from '../../utils/default';
import { GetAbsolutePosition } from '../../utils/itertools';
import { RemoveMargin } from '../../utils/svg';
interface ISelectorProps { interface ISelectorProps {
containers: Map<string, IContainerModel> text: string
selected?: IContainerModel x: number
scale?: number y: number
width: number
height: number
scale: number
} }
export function RenderSelector(ctx: CanvasRenderingContext2D, frameCount: number, props: ISelectorProps): void { export function RenderSelector(
if (props.selected === undefined || props.selected === null) { ctx: CanvasRenderingContext2D,
return; frameCount: number,
} {
text,
const scale = (props.scale ?? 1); x,
let [x, y] = GetAbsolutePosition(props.containers, props.selected); y,
let [width, height] = [ width,
props.selected.properties.width, height,
props.selected.properties.height scale
]; }: ISelectorProps
): void {
({ 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 xText = x + width / 2; const xText = x + width / 2;
const yText = y + height / 2; const yText = y + height / 2;
@ -42,7 +35,7 @@ export function RenderSelector(ctx: CanvasRenderingContext2D, frameCount: number
if (SHOW_SELECTOR_TEXT) { if (SHOW_SELECTOR_TEXT) {
ctx.font = `${16 / scale}px Verdana`; ctx.font = `${16 / scale}px Verdana`;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.fillText(props.selected.properties.displayedText, xText, yText); ctx.fillText(text, xText, yText);
ctx.textAlign = 'left'; ctx.textAlign = 'left';
} }
} }

View file

@ -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<string, IContainerModel>
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
}
);
}

View file

@ -0,0 +1,43 @@
import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
import { SYMBOL_MARGIN } from '../../utils/default';
import { RenderSelector } from './Selector';
interface ISelectorProps {
symbols: Map<string, ISymbolModel>
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
}
);
}

View file

@ -1,11 +1,11 @@
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
import { DIMENSION_MARGIN } from '../../utils/default'; import { DIMENSION_MARGIN, SYMBOL_MARGIN } from '../../utils/default';
const IMAGE_CACHE = new Map<string, HTMLImageElement>(); const IMAGE_CACHE = new Map<string, HTMLImageElement>();
export function RenderSymbol( export function RenderSymbol(
symbol: ISymbolModel,
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
symbol: ISymbolModel,
scale: number): void { scale: number): void {
const href = symbol.config.Image.Base64Image ?? symbol.config.Image.Url; const href = symbol.config.Image.Base64Image ?? symbol.config.Image.Url;
@ -26,6 +26,7 @@ export function RenderSymbol(
DrawImage(ctx, scale, image, symbol); DrawImage(ctx, scale, image, symbol);
} }
function DrawImage( function DrawImage(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
scale: number, scale: number,
@ -34,12 +35,14 @@ function DrawImage(
): void { ): void {
ctx.save(); ctx.save();
ctx.fillStyle = '#000000'; ctx.fillStyle = '#000000';
const width = symbol.width / scale;
const height = symbol.height / scale;
ctx.drawImage( ctx.drawImage(
image, image,
symbol.offset, symbol.offset + symbol.width / 2,
-DIMENSION_MARGIN, -SYMBOL_MARGIN - height,
symbol.width, width,
symbol.height height
); );
ctx.restore(); ctx.restore();
} }

View file

@ -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 * as React from 'react';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer'; import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { ICategory } from '../../Interfaces/ICategory'; import { ICategory } from '../../Interfaces/ICategory';
@ -6,11 +6,15 @@ import { IContainerModel } from '../../Interfaces/IContainerModel';
import { TruncateString } from '../../utils/stringtools'; import { TruncateString } from '../../utils/stringtools';
import { Category } from '../Category/Category'; import { Category } from '../Category/Category';
import { Text } from '../Text/Text'; import { Text } from '../Text/Text';
import { IReplaceContainer } from '../../Interfaces/IReplaceContainer';
import { Dispatch } from 'react';
interface IComponentsProps { interface IComponentsProps {
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
componentOptions: IAvailableContainer[] componentOptions: IAvailableContainer[]
categories: ICategory[] categories: ICategory[]
replaceContainer: IReplaceContainer
setReplaceContainer: Dispatch<React.SetStateAction<IReplaceContainer>>
buttonOnClick: (type: string) => void 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; disabled = config.Blacklist?.find(type => type === componentOption.Type) !== undefined ?? false;
} }
if (props.replaceContainer.isReplacing && componentOption.Category !== props.replaceContainer.category) {
disabled = true;
}
if (disabled && hideDisabled) { if (disabled && hideDisabled) {
return; return;
} }
@ -96,6 +104,15 @@ export function Components(props: IComponentsProps): JSX.Element {
return ( return (
<div className='h-full'> <div className='h-full'>
<div className='hover:bg-slate-300 h-7 text-right pr-1 pl-1'> <div className='hover:bg-slate-300 h-7 text-right pr-1 pl-1'>
{props.replaceContainer.isReplacing && <button
onClick={() => {
props.setReplaceContainer({ isReplacing: false, id: undefined, category: undefined });
}}
className='h-full hover:bg-slate-400 rounded-lg p-1'
>
<XCircleIcon className='heroicon'></XCircleIcon>
</button>
}
<button <button
onClick={() => { setHideDisabled(!hideDisabled); }} onClick={() => { setHideDisabled(!hideDisabled); }}
className='h-full hover:bg-slate-400 rounded-lg p-1' className='h-full hover:bg-slate-400 rounded-lg p-1'
@ -117,4 +134,4 @@ export function Components(props: IComponentsProps): JSX.Element {
</div> </div>
</div> </div>
); );
}; }

View file

@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { PropertyType } from '../../Enums/PropertyType'; import { PropertyType } from '../../Enums/PropertyType';
import { IContainerProperties } from '../../Interfaces/IContainerProperties'; import { type IContainerProperties } from '../../Interfaces/IContainerProperties';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
import { import {
SHOW_BORROWER_DIMENSIONS, SHOW_BORROWER_DIMENSIONS,
SHOW_CHILDREN_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS,
@ -46,7 +46,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
inputClassName='' inputClassName=''
type='string' type='string'
value={props.properties.displayedText?.toString()} value={props.properties.displayedText?.toString()}
onChange={(value) => props.onChange('displayedText', value)}/> onChange={(value) => { props.onChange('displayedText', value); }}/>
<OrientationSelector <OrientationSelector
id='orientation' id='orientation'
name='Orientation' name='Orientation'
@ -117,17 +117,19 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
props.properties.width, props.properties.width,
props.properties.positionReference props.properties.positionReference
).toString()} ).toString()}
onChange={(value) => props.onChange( onChange={(value) => {
'x', props.onChange(
ApplyXMargin( 'x',
RestoreX( ApplyXMargin(
Number(value), RestoreX(
props.properties.width, Number(value),
props.properties.positionReference props.properties.width,
), props.properties.positionReference
props.properties.margin.left ),
) props.properties.margin.left
)}/> )
);
}}/>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-y`} id={`${props.properties.id}-y`}
labelText={Text({ textId: '@ContainerY' })} labelText={Text({ textId: '@ContainerY' })}
@ -140,17 +142,19 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
props.properties.height, props.properties.height,
props.properties.positionReference props.properties.positionReference
).toString()} ).toString()}
onChange={(value) => props.onChange( onChange={(value) => {
'y', props.onChange(
ApplyXMargin( 'y',
RestoreY( ApplyXMargin(
Number(value), RestoreY(
props.properties.height, Number(value),
props.properties.positionReference props.properties.height,
), props.properties.positionReference
props.properties.margin.top ),
) props.properties.margin.top
)}/> )
);
}}/>
</div> </div>
</Category> </Category>
@ -171,7 +175,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
type='number' type='number'
min={1} min={1}
value={props.properties.minWidth.toString()} value={props.properties.minWidth.toString()}
onChange={(value) => props.onChange('minWidth', Number(value))}/> onChange={(value) => { props.onChange('minWidth', Number(value)); }}/>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-width`} id={`${props.properties.id}-width`}
labelText={Text({ textId: '@ContainerWidth' })} labelText={Text({ textId: '@ContainerWidth' })}
@ -182,7 +186,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
min={props.properties.minWidth} min={props.properties.minWidth}
max={props.properties.maxWidth} max={props.properties.maxWidth}
value={(RemoveWidthMargin(props.properties.width, props.properties.margin.left, props.properties.margin.right)).toString()} value={(RemoveWidthMargin(props.properties.width, props.properties.margin.left, props.properties.margin.right)).toString()}
onChange={(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}/> isDisabled={props.properties.isFlex}/>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-maxWidth`} id={`${props.properties.id}-maxWidth`}
@ -193,7 +197,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
type='number' type='number'
min={1} min={1}
value={props.properties.maxWidth.toString()} value={props.properties.maxWidth.toString()}
onChange={(value) => props.onChange('maxWidth', Number(value))}/> onChange={(value) => { props.onChange('maxWidth', Number(value)); }}/>
<div className='col-span-5 p-3'></div> <div className='col-span-5 p-3'></div>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-minHeight`} id={`${props.properties.id}-minHeight`}
@ -204,7 +208,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
type='number' type='number'
min={1} min={1}
value={props.properties.minHeight.toString()} value={props.properties.minHeight.toString()}
onChange={(value) => props.onChange('minHeight', Number(value))}/> onChange={(value) => { props.onChange('minHeight', Number(value)); }}/>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-height`} id={`${props.properties.id}-height`}
labelText={Text({ textId: '@ContainerHeight' })} labelText={Text({ textId: '@ContainerHeight' })}
@ -215,7 +219,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
min={props.properties.minHeight} min={props.properties.minHeight}
max={props.properties.maxHeight} max={props.properties.maxHeight}
value={(RemoveWidthMargin(props.properties.height, props.properties.margin.top, props.properties.margin.bottom)).toString()} value={(RemoveWidthMargin(props.properties.height, props.properties.margin.top, props.properties.margin.bottom)).toString()}
onChange={(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} isDisabled={props.properties.isFlex}
/> />
<TextInputGroup <TextInputGroup
@ -227,7 +231,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
type='number' type='number'
min={1} min={1}
value={props.properties.maxHeight.toString()} value={props.properties.maxHeight.toString()}
onChange={(value) => props.onChange('maxHeight', Number(value))}/> onChange={(value) => { props.onChange('maxHeight', Number(value)); }}/>
</div> </div>
</Category> </Category>
@ -247,7 +251,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
type='number' type='number'
min={0} min={0}
value={(props.properties.margin.left ?? 0).toString()} 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); }}/>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-mb`} id={`${props.properties.id}-mb`}
labelText={Text({ textId: '@ContainerMarginBottom' })} labelText={Text({ textId: '@ContainerMarginBottom' })}
@ -257,7 +261,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
type='number' type='number'
min={0} min={0}
value={(props.properties.margin.bottom ?? 0).toString()} value={(props.properties.margin.bottom ?? 0).toString()}
onChange={(value) => props.onChange('bottom', Number(value), PropertyType.Margin)}/> onChange={(value) => { props.onChange('bottom', Number(value), PropertyType.Margin); }}/>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-mt`} id={`${props.properties.id}-mt`}
labelText={Text({ textId: '@ContainerMarginTop' })} labelText={Text({ textId: '@ContainerMarginTop' })}
@ -267,7 +271,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
type='number' type='number'
min={0} min={0}
value={(props.properties.margin.top ?? 0).toString()} value={(props.properties.margin.top ?? 0).toString()}
onChange={(value) => props.onChange('top', Number(value), PropertyType.Margin)}/> onChange={(value) => { props.onChange('top', Number(value), PropertyType.Margin); }}/>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-mr`} id={`${props.properties.id}-mr`}
labelText={Text({ textId: '@ContainerMarginRight' })} labelText={Text({ textId: '@ContainerMarginRight' })}
@ -277,7 +281,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
type='number' type='number'
min={0} min={0}
value={(props.properties.margin.right ?? 0).toString()} value={(props.properties.margin.right ?? 0).toString()}
onChange={(value) => props.onChange('right', Number(value), PropertyType.Margin)}/> onChange={(value) => { props.onChange('right', Number(value), PropertyType.Margin); }}/>
</div> </div>
</Category> </Category>
@ -295,7 +299,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
inputClassName='ml-auto mr-auto block' inputClassName='ml-auto mr-auto block'
type={ToggleType.Full} type={ToggleType.Full}
checked={props.properties.isFlex} checked={props.properties.isFlex}
onChange={(event) => props.onChange('isFlex', event.target.checked)} onChange={(event) => { props.onChange('isFlex', event.target.checked); }}
/> />
<ToggleButton <ToggleButton
labelText={Text({ textId: '@ContainerAnchor' })} labelText={Text({ textId: '@ContainerAnchor' })}
@ -304,7 +308,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
inputClassName='ml-auto mr-auto block' inputClassName='ml-auto mr-auto block'
type={ToggleType.Full} type={ToggleType.Full}
checked={props.properties.isAnchor} checked={props.properties.isAnchor}
onChange={(event) => props.onChange('isAnchor', event.target.checked)}/> onChange={(event) => { props.onChange('isAnchor', event.target.checked); }}/>
</div> </div>
</Category> </Category>
@ -334,7 +338,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
value: symbol.id value: symbol.id
}))} }))}
value={props.properties.linkedSymbolId ?? ''} value={props.properties.linkedSymbolId ?? ''}
onChange={(event) => props.onChange('linkedSymbolId', event.target.value)}/> onChange={(event) => { props.onChange('linkedSymbolId', event.target.value); }}/>
</div> </div>
</Category> </Category>
@ -347,168 +351,244 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
<div className='grid grid-cols-1 gap-6 prop-category-body'> <div className='grid grid-cols-1 gap-6 prop-category-body'>
{ {
SHOW_SELF_DIMENSIONS && SHOW_SELF_DIMENSIONS &&
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<PositionCheckboxes <PositionCheckboxes
id='positionsSelfDimensions' id='positionsSelfDimensions'
attribute='positions' attribute='positions'
name='ShowSelfDimensions' name='ShowSelfDimensions'
labelText={Text({ textId: '@ContainerShowDimension' })} labelText={Text({ textId: '@ContainerShowDimension' })}
value={props.properties.dimensionOptions.selfDimensions.positions} value={props.properties.dimensionOptions.selfDimensions.positions}
onChange={(key, value) => props.onChange(key, value, PropertyType.SelfDimension)} onChange={(key, value) => { props.onChange(key, value, PropertyType.SelfDimension); }}
/> />
<InputGroup <InputGroup
labelText={Text({ textId: '@Color' })} labelText={Text({ textId: '@StyleStrokeColor' })}
inputKey='color' inputKey='color'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='color' type='color'
value={props.properties.dimensionOptions.selfDimensions.color} value={props.properties.dimensionOptions.selfDimensions.color}
onChange={(e) => props.onChange('color', e.target.value, PropertyType.SelfDimension)}/> onChange={(e) => { props.onChange('color', e.target.value, PropertyType.SelfDimension); }}/>
</div> <TextInputGroup
id={`${props.properties.id}-selfDimensions-width`}
labelText={Text({ textId: '@StyleStrokeWidth' })}
inputKey='width'
labelClassName=''
inputClassName=''
type='number'
min={0}
value={(props.properties.dimensionOptions.selfDimensions.width ?? 0).toString()}
onChange={(value) => { props.onChange('width', Number(value), PropertyType.SelfDimension); }}/>
<TextInputGroup
id={`${props.properties.id}-selfDimensions-dasharray`}
labelText={Text({ textId: '@StyleStrokeDashArray' })}
inputKey='dashArray'
labelClassName=''
inputClassName=''
type='text'
value={props.properties.dimensionOptions.selfDimensions.dashArray ?? ''}
onChange={(value) => { props.onChange('dashArray', value, PropertyType.SelfDimension); }}/>
</div>
} }
{ {
SHOW_SELF_MARGINS_DIMENSIONS && SHOW_SELF_MARGINS_DIMENSIONS &&
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<PositionCheckboxes <PositionCheckboxes
id='positionsSelfMarginsDimensions' id='positionsSelfMarginsDimensions'
attribute='positions' attribute='positions'
name='ShowSelfMarginsDimensions' name='ShowSelfMarginsDimensions'
labelText={Text({ textId: '@ContainerShowMarginsDimension' })} labelText={Text({ textId: '@ContainerShowMarginsDimension' })}
value={props.properties.dimensionOptions.selfMarginsDimensions.positions} value={props.properties.dimensionOptions.selfMarginsDimensions.positions}
onChange={(key, value) => props.onChange(key, value, PropertyType.SelfMarginDimension)} onChange={(key, value) => { props.onChange(key, value, PropertyType.SelfMarginDimension); }}
/> />
<InputGroup <InputGroup
labelText={Text({ textId: '@Color' })} labelText={Text({ textId: '@StyleStrokeColor' })}
inputKey='color' inputKey='color'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='color' type='color'
value={props.properties.dimensionOptions.selfMarginsDimensions.color} value={props.properties.dimensionOptions.selfMarginsDimensions.color}
onChange={(e) => props.onChange('color', e.target.value, PropertyType.SelfMarginDimension)}/> onChange={(e) => { props.onChange('color', e.target.value, PropertyType.SelfMarginDimension); }}/>
</div> <TextInputGroup
id={`${props.properties.id}-selfMarginsDimensions-width`}
labelText={Text({ textId: '@StyleStrokeWidth' })}
inputKey='width'
labelClassName=''
inputClassName=''
type='number'
min={0}
value={(props.properties.dimensionOptions.selfMarginsDimensions.width ?? 0).toString()}
onChange={(value) => { props.onChange('width', Number(value), PropertyType.SelfMarginDimension); }}/>
<TextInputGroup
id={`${props.properties.id}-selfMarginsDimensions-dasharray`}
labelText={Text({ textId: '@StyleStrokeDashArray' })}
inputKey='dashArray'
labelClassName=''
inputClassName=''
type='text'
value={props.properties.dimensionOptions.selfMarginsDimensions.dashArray ?? ''}
onChange={(value) => { props.onChange('dashArray', value, PropertyType.SelfMarginDimension); }}/>
</div>
} }
{ {
SHOW_CHILDREN_DIMENSIONS && SHOW_CHILDREN_DIMENSIONS &&
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<PositionCheckboxes <PositionCheckboxes
id='positionsChildrenDimensions' id='positionsChildrenDimensions'
attribute='positions' attribute='positions'
name='ShowChildrenDimensions' name='ShowChildrenDimensions'
labelText={Text({ textId: '@ContainerShowChildrenDimension' })} labelText={Text({ textId: '@ContainerShowChildrenDimension' })}
value={props.properties.dimensionOptions.childrenDimensions.positions} value={props.properties.dimensionOptions.childrenDimensions.positions}
onChange={(key, value) => props.onChange(key, value, PropertyType.ChildrenDimensions)} onChange={(key, value) => { props.onChange(key, value, PropertyType.ChildrenDimensions); }}
/> />
<InputGroup <InputGroup
labelText={Text({ textId: '@Color' })} labelText={Text({ textId: '@StyleStrokeColor' })}
inputKey='color' inputKey='color'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='color' type='color'
value={props.properties.dimensionOptions.childrenDimensions.color} value={props.properties.dimensionOptions.childrenDimensions.color}
onChange={(e) => props.onChange('color', e.target.value, PropertyType.ChildrenDimensions)}/> onChange={(e) => { props.onChange('color', e.target.value, PropertyType.ChildrenDimensions); }}/>
</div> <TextInputGroup
id={`${props.properties.id}-childrenDimensions-width`}
labelText={Text({ textId: '@StyleStrokeWidth' })}
inputKey='width'
labelClassName=''
inputClassName=''
type='number'
min={0}
value={(props.properties.dimensionOptions.childrenDimensions.width ?? 0).toString()}
onChange={(value) => { props.onChange('width', Number(value), PropertyType.ChildrenDimensions); }}/>
<TextInputGroup
id={`${props.properties.id}-childrenDimensions-dasharray`}
labelText={Text({ textId: '@StyleStrokeDashArray' })}
inputKey='dashArray'
labelClassName=''
inputClassName=''
type='text'
value={props.properties.dimensionOptions.childrenDimensions.dashArray ?? ''}
onChange={(value) => { props.onChange('dashArray', value, PropertyType.ChildrenDimensions); }}/>
</div>
} }
{ {
SHOW_BORROWER_DIMENSIONS && SHOW_BORROWER_DIMENSIONS &&
<> <>
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<OrientationCheckboxes <OrientationCheckboxes
id='markPosition' id='markPosition'
name='MarkPosition' name='MarkPosition'
value={props.properties.dimensionOptions.markPosition} value={props.properties.dimensionOptions.markPosition}
labelText={Text({ textId: '@ContainerMarkPosition' })} labelText={Text({ textId: '@ContainerMarkPosition' })}
onChange={(key, value) => props.onChange(key, value, PropertyType.DimensionOptions)} onChange={(key, value) => { props.onChange(key, value, PropertyType.DimensionOptions); }}
/> />
</div> </div>
<div className='grid grid-cols-1 gap-2'> <div className='grid grid-cols-1 gap-2'>
<PositionCheckboxes <PositionCheckboxes
id='positionsDimensionWithMarks' id='positionsDimensionWithMarks'
attribute='positions' attribute='positions'
name='ShowDimensionWithMarks' name='ShowDimensionWithMarks'
labelText={Text({ textId: '@ContainerShowDimensionWithMarks' })} labelText={Text({ textId: '@ContainerShowDimensionWithMarks' })}
value={props.properties.dimensionOptions.dimensionWithMarks.positions} value={props.properties.dimensionOptions.dimensionWithMarks.positions}
onChange={(key, value) => props.onChange(key, value, PropertyType.DimensionWithMarks)} onChange={(key, value) => { props.onChange(key, value, PropertyType.DimensionWithMarks); }}
/> />
<InputGroup <InputGroup
labelText={Text({ textId: '@Color' })} labelText={Text({ textId: '@StyleStrokeColor' })}
inputKey='color' inputKey='color'
labelClassName='' labelClassName=''
inputClassName='' inputClassName=''
type='color' type='color'
value={props.properties.dimensionOptions.dimensionWithMarks.color} value={props.properties.dimensionOptions.dimensionWithMarks.color}
onChange={(e) => props.onChange('color', e.target.value, PropertyType.DimensionWithMarks)}/> onChange={(e) => { props.onChange('color', e.target.value, PropertyType.DimensionWithMarks); }}/>
</div> <TextInputGroup
</> id={`${props.properties.id}-dimensionWithMarks-width`}
labelText={Text({ textId: '@StyleStrokeWidth' })}
inputKey='width'
labelClassName=''
inputClassName=''
type='number'
min={0}
value={(props.properties.dimensionOptions.dimensionWithMarks.width ?? 0).toString()}
onChange={(value) => { props.onChange('width', Number(value), PropertyType.DimensionWithMarks); }}/>
<TextInputGroup
id={`${props.properties.id}-dimensionWithMarks-dasharray`}
labelText={Text({ textId: '@StyleStrokeDashArray' })}
inputKey='dashArray'
labelClassName=''
inputClassName=''
type='text'
value={props.properties.dimensionOptions.dimensionWithMarks.dashArray ?? ''}
onChange={(value) => { props.onChange('dashArray', value, PropertyType.DimensionWithMarks); }}/>
</div>
</>
} }
</div> </div>
</Category> </Category>
{props.properties.style !== undefined && {props.properties.style !== undefined &&
<Category category={{ <Category category={{
Type: 'Style', Type: 'Style',
DisplayedText: Text({ textId: '@ContainerStyle' }) DisplayedText: Text({ textId: '@ContainerStyle' })
}} }}
heightClass={`${categoryHeight}`} heightClass={`${categoryHeight}`}
> >
<div className='grid grid-cols-5 gap-6 items-center prop-category-body'> <div className='grid grid-cols-5 gap-6 items-center prop-category-body'>
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-stroke`} id={`${props.properties.id}-stroke`}
labelText={Text({ textId: '@StyleStroke' })} labelText={Text({ textId: '@StyleStroke' })}
inputKey='stroke' inputKey='stroke'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
type='string' type='string'
value={props.properties.style.stroke ?? 'black'} value={props.properties.style.stroke ?? 'black'}
onChange={(value) => props.onChange('stroke', value, PropertyType.Style)} onChange={(value) => { props.onChange('stroke', value, PropertyType.Style); }}
/> />
<InputGroup <InputGroup
labelKey={`${props.properties.id}-strokeOpacity`} labelKey={`${props.properties.id}-strokeOpacity`}
labelText={Text({ textId: '@StyleStrokeOpacity' })} labelText={Text({ textId: '@StyleStrokeOpacity' })}
inputKey='strokeOpacity' inputKey='strokeOpacity'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
type='range' type='range'
min={0} min={0}
max={1} max={1}
step={0.01} step={0.01}
value={(props.properties.style.strokeOpacity ?? 1).toString()} value={(props.properties.style.strokeOpacity ?? 1).toString()}
onChange={(event) => props.onChange('strokeOpacity', Number(event.target.value), PropertyType.Style)} onChange={(event) => { props.onChange('strokeOpacity', Number(event.target.value), PropertyType.Style); }}
/> />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-strokeWidth`} id={`${props.properties.id}-strokeWidth`}
labelText={Text({ textId: '@StyleStrokeWidth' })} labelText={Text({ textId: '@StyleStrokeWidth' })}
inputKey='strokeWidth' inputKey='strokeWidth'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
type='number' type='number'
value={(props.properties.style.strokeWidth ?? 1).toString()} value={(props.properties.style.strokeWidth ?? 1).toString()}
onChange={(value) => props.onChange('strokeWidth', Number(value), PropertyType.Style)} onChange={(value) => { props.onChange('strokeWidth', Number(value), PropertyType.Style); }}
/> />
<TextInputGroup <TextInputGroup
id={`${props.properties.id}-fill`} id={`${props.properties.id}-fill`}
labelText={Text({ textId: '@StyleFill' })} labelText={Text({ textId: '@StyleFill' })}
inputKey='fill' inputKey='fill'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
type='string' type='string'
value={props.properties.style.fill ?? 'black'} value={props.properties.style.fill ?? 'black'}
onChange={(value) => props.onChange('fill', value, PropertyType.Style)} onChange={(value) => { props.onChange('fill', value, PropertyType.Style); }}
/> />
<InputGroup <InputGroup
labelKey={`${props.properties.id}-fillOpacity`} labelKey={`${props.properties.id}-fillOpacity`}
labelText={Text({ textId: '@StyleFillOpacity' })} labelText={Text({ textId: '@StyleFillOpacity' })}
inputKey='fillOpacity' inputKey='fillOpacity'
labelClassName='col-span-2' labelClassName='col-span-2'
inputClassName='col-span-3' inputClassName='col-span-3'
type='range' type='range'
min={0} min={0}
max={1} max={1}
step={0.01} step={0.01}
value={(props.properties.style.fillOpacity ?? 1).toString()} value={(props.properties.style.fillOpacity ?? 1).toString()}
onChange={(event) => props.onChange('fillOpacity', Number(event.target.value), PropertyType.Style)} onChange={(event) => { props.onChange('fillOpacity', Number(event.target.value), PropertyType.Style); }}
/> />
</div> </div>
</Category> </Category>
} }
</div> </div>
); );

View file

@ -5,6 +5,7 @@ import { PositionReference } from '../../Enums/PositionReference';
import { IContainerProperties } from '../../Interfaces/IContainerProperties'; import { IContainerProperties } from '../../Interfaces/IContainerProperties';
import { Orientation } from '../../Enums/Orientation'; import { Orientation } from '../../Enums/Orientation';
import { ContainerProperties } from './ContainerProperties'; import { ContainerProperties } from './ContainerProperties';
import { DEFAULT_DIMENSION_OPTION } from '../../utils/default';
describe.concurrent('Properties', () => { describe.concurrent('Properties', () => {
it('No properties', () => { it('No properties', () => {
@ -43,23 +44,11 @@ describe.concurrent('Properties', () => {
warning: '', warning: '',
hideChildrenInTreeview: false, hideChildrenInTreeview: false,
dimensionOptions: { dimensionOptions: {
childrenDimensions: { childrenDimensions: DEFAULT_DIMENSION_OPTION,
color: '#000000', selfDimensions: DEFAULT_DIMENSION_OPTION,
positions: [] selfMarginsDimensions: DEFAULT_DIMENSION_OPTION,
},
selfDimensions: {
color: '#000000',
positions: []
},
selfMarginsDimensions: {
color: '#000000',
positions: []
},
markPosition: [], markPosition: [],
dimensionWithMarks: { dimensionWithMarks: DEFAULT_DIMENSION_OPTION
color: '#000000',
positions: []
}
} }
}; };

View file

@ -8,6 +8,8 @@ import Swal from 'sweetalert2';
import { PropertyType } from '../../../Enums/PropertyType'; import { PropertyType } from '../../../Enums/PropertyType';
import { TransformX, TransformY } from '../../../utils/svg'; import { TransformX, TransformY } from '../../../utils/svg';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { AddContainers } from './AddContainer';
import { IConfiguration } from '../../../Interfaces/IConfiguration';
/** /**
* Select a container * Select a container
@ -133,6 +135,58 @@ export function DeleteContainer(
return history; 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 * Returns the next container that will be selected
* after the selectedContainer is removed. * after the selectedContainer is removed.

View file

@ -16,6 +16,7 @@ import { AddContainers } from './AddContainer';
import { DeleteContainer } from './ContainerOperations'; import { DeleteContainer } from './ContainerOperations';
import { DeleteSymbol } from './SymbolOperations'; import { DeleteSymbol } from './SymbolOperations';
import { Text } from '../../Text/Text'; import { Text } from '../../Text/Text';
import { IReplaceContainer } from '../../../Interfaces/IReplaceContainer';
export function InitActions( export function InitActions(
menuActions: Map<string, IMenuAction[]>, menuActions: Map<string, IMenuAction[]>,
@ -23,7 +24,8 @@ export function InitActions(
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
setNewHistory: (newHistory: IHistoryState[]) => void, setNewHistory: (newHistory: IHistoryState[]) => void,
setHistoryCurrentStep: Dispatch<SetStateAction<number>> setHistoryCurrentStep: Dispatch<SetStateAction<number>>,
setIsReplacingContainer: Dispatch<SetStateAction<IReplaceContainer>>
): void { ): void {
menuActions.set( menuActions.set(
'', '',
@ -56,9 +58,24 @@ export function InitActions(
menuActions.set( menuActions.set(
'elements-sidebar-row', 'elements-sidebar-row',
[{ [{
text: Text({ textId: '@ReplaceByContainer' }),
title: Text({ textId: '@ReplaceByContainerTitle' }),
shortcut: '<kbd>R</kbd>',
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' }), text: Text({ textId: '@DeleteContainer' }),
title: Text({ textId: '@DeleteContainerTitle' }), title: Text({ textId: '@DeleteContainerTitle' }),
shortcut: '<kbd>Suppr</kbd>', shortcut: '<kbd>Suppr</kbd>',
action: (target: HTMLElement) => { action: (target: HTMLElement) => {
const id = target.id; const id = target.id;
const newHistory = DeleteContainer( const newHistory = DeleteContainer(

View file

@ -53,6 +53,7 @@ export function SaveEditorAsSVG(): void {
svg.replaceChildren(...mainSvg); svg.replaceChildren(...mainSvg);
// remove the selector // remove the selector
// TODO: Fix this with SelectorMode != Nothing or with some html magic
const group = svg.children[svg.children.length - 1]; const group = svg.children[svg.children.length - 1];
group.removeChild(group.children[group.children.length - 1]); group.removeChild(group.children[group.children.length - 1]);
if (SHOW_SELECTOR_TEXT) { if (SHOW_SELECTOR_TEXT) {

View file

@ -7,7 +7,8 @@ export function OnKey(
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>, setHistoryCurrentStep: Dispatch<SetStateAction<number>>,
deleteAction: () => void deleteAction: () => void,
resetState: () => void
): void { ): void {
if (!ENABLE_SHORTCUTS) { if (!ENABLE_SHORTCUTS) {
return; return;
@ -27,5 +28,7 @@ export function OnKey(
setHistoryCurrentStep(historyCurrentStep + 1); setHistoryCurrentStep(historyCurrentStep + 1);
} else if (event.key === 'Delete') { } else if (event.key === 'Delete') {
deleteAction(); deleteAction();
} else if (event.key === 'Escape') {
resetState();
} }
} }

View file

@ -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 './Editor.scss';
import { IConfiguration } from '../../Interfaces/IConfiguration'; import { type IConfiguration } from '../../Interfaces/IConfiguration';
import { IHistoryState } from '../../Interfaces/IHistoryState'; import { type IHistoryState } from '../../Interfaces/IHistoryState';
import { UI } from '../UI/UI'; 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 { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
import { OnKey } from './Actions/Shortcuts'; import { OnKey } from './Actions/Shortcuts';
import { UseCustomEvents, UseEditorListener } from '../../Events/EditorEvents'; import { UseCustomEvents, UseEditorListener } from '../../Events/EditorEvents';
@ -13,6 +13,7 @@ import { FindContainerById } from '../../utils/itertools';
import { Menu } from '../Menu/Menu'; import { Menu } from '../Menu/Menu';
import { InitActions } from './Actions/ContextMenuActions'; import { InitActions } from './Actions/ContextMenuActions';
import { AddContainerToSelectedContainer, AddContainer } from './Actions/AddContainer'; import { AddContainerToSelectedContainer, AddContainer } from './Actions/AddContainer';
import { type IReplaceContainer } from '../../Interfaces/IReplaceContainer';
interface IEditorProps { interface IEditorProps {
root: Element | Document root: Element | Document
@ -25,16 +26,18 @@ function UseShortcuts(
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>, setHistoryCurrentStep: Dispatch<SetStateAction<number>>,
deleteAction: () => void deleteAction: () => void,
resetState: () => void
): void { ): void {
useEffect(() => { useEffect(() => {
function OnKeyUp(event: KeyboardEvent): void { function OnKeyUp(event: KeyboardEvent): void {
return OnKey( OnKey(
event, event,
history, history,
historyCurrentStep, historyCurrentStep,
setHistoryCurrentStep, setHistoryCurrentStep,
deleteAction deleteAction,
resetState
); );
} }
@ -62,13 +65,20 @@ function UseNewHistoryState(
}; };
} }
export function Editor(props: IEditorProps): JSX.Element { export function Editor(props: IEditorProps): JSX.Element {
// States // States
const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history)); const [history, setHistory] = React.useState<IHistoryState[]>(structuredClone(props.history));
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep); const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
const [replaceContainer, setReplaceContainer] = React.useState<IReplaceContainer>({ isReplacing: false, id: undefined, category: undefined });
const editorRef = useRef<HTMLDivElement>(null); const editorRef = useRef<HTMLDivElement>(null);
const setNewHistory = UseNewHistoryState(setHistory, setHistoryCurrentStep); const setNewHistory = UseNewHistoryState(setHistory, setHistoryCurrentStep);
function ResetState(): void {
setReplaceContainer({ isReplacing: false, id: undefined, category: undefined });
}
// Events // Events
UseShortcuts( UseShortcuts(
history, history,
@ -79,7 +89,8 @@ export function Editor(props: IEditorProps): JSX.Element {
setNewHistory( setNewHistory(
DeleteContainer(current.selectedContainerId, history, historyCurrentStep) DeleteContainer(current.selectedContainerId, history, historyCurrentStep)
); );
} },
ResetState
); );
UseCustomEvents( UseCustomEvents(
props.root, props.root,
@ -104,7 +115,8 @@ export function Editor(props: IEditorProps): JSX.Element {
history, history,
historyCurrentStep, historyCurrentStep,
setNewHistory, setNewHistory,
setHistoryCurrentStep setHistoryCurrentStep,
setReplaceContainer
); );
// Render // Render
@ -113,87 +125,118 @@ export function Editor(props: IEditorProps): JSX.Element {
const selected = FindContainerById(current.containers, current.selectedContainerId); const selected = FindContainerById(current.containers, current.selectedContainerId);
return ( return (
<div ref={editorRef} className="Editor font-sans h-full"> <div ref={editorRef} className="Editor font-sans h-full ">
<UI <UI
editorState={{ editorState={{
configuration: props.configuration, configuration: props.configuration,
history, history,
historyCurrentStep historyCurrentStep
}} }}
selectContainer={(container) => setNewHistory( replaceContainer={replaceContainer}
SelectContainer( selectContainer={(container) => {
container, setNewHistory(
history, SelectContainer(
historyCurrentStep container,
))} history,
deleteContainer={(containerId: string) => setNewHistory( historyCurrentStep
DeleteContainer( ));
containerId, }}
history, deleteContainer={(containerId: string) => {
historyCurrentStep setNewHistory(
))} DeleteContainer(
onPropertyChange={(key, value, type) => setNewHistory( containerId,
OnPropertyChange( history,
key, value, type, historyCurrentStep
selected, ));
history, }}
historyCurrentStep onPropertyChange={(key, value, type) => {
))} setNewHistory(
addContainer={(type) => { OnPropertyChange(
key, value, type,
selected,
history,
historyCurrentStep
));
}}
addOrReplaceContainer={(type) => {
if (selected === null || selected === undefined) { if (selected === null || selected === undefined) {
return; return;
} }
if (replaceContainer.isReplacing && replaceContainer.id !== undefined) {
setNewHistory(AddContainerToSelectedContainer( const newHistory = ReplaceByContainer(
type, replaceContainer.id,
selected, type,
configuration, configuration,
history, history,
historyCurrentStep historyCurrentStep
)); );
setReplaceContainer({ isReplacing: false, id: undefined, category: undefined });
setNewHistory(newHistory);
} else {
setNewHistory(AddContainerToSelectedContainer(
type,
selected,
configuration,
history,
historyCurrentStep
));
}
}} }}
addContainerAt={(index, type, parent) => setNewHistory( addContainerAt={(index, type, parent) => {
AddContainer( setNewHistory(
index, AddContainer(
type, index,
parent, type,
configuration, 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, history,
historyCurrentStep historyCurrentStep,
) configuration
)} );
addSymbol={(type) => setNewHistory( }}
AddSymbol( saveEditorAsSVG={() => { SaveEditorAsSVG(); }}
type, loadState={(move) => { setHistoryCurrentStep(move); }}
configuration, setReplaceContainer={setReplaceContainer}
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)}
/> />
<Menu <Menu
getListener={() => editorRef.current} getListener={() => editorRef.current}

View file

@ -1,21 +1,22 @@
import * as React from 'react'; import * as React from 'react';
import { useState } from 'react';
import useSize from '@react-hook/size'; import useSize from '@react-hook/size';
import { FixedSizeList as List } from 'react-window'; import { FixedSizeList as List } from 'react-window';
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; import { ExclamationTriangleIcon } from '@heroicons/react/24/outline';
import { ContainerProperties } from '../ContainerProperties/ContainerProperties'; import { ContainerProperties } from '../ContainerProperties/ContainerProperties';
import { IContainerModel } from '../../Interfaces/IContainerModel'; import { type IContainerModel } from '../../Interfaces/IContainerModel';
import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools'; import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
import { PropertyType } from '../../Enums/PropertyType'; import { type PropertyType } from '../../Enums/PropertyType';
import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar'; import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar';
import { Text } from '../Text/Text'; import { Text } from '../Text/Text';
import { ExtendedSidebar } from '../UI/UI';
interface IElementsSideBarProps { interface IElementsSidebarProps {
containers: Map<string, IContainerModel> containers: Map<string, IContainerModel>
mainContainer: IContainerModel mainContainer: IContainerModel
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
selectedExtendedSidebar: ExtendedSidebar
onPropertyChange: ( onPropertyChange: (
key: string, key: string,
value: string | number | boolean | number[], value: string | number | boolean | number[],
@ -23,8 +24,7 @@ interface IElementsSideBarProps {
) => void ) => void
selectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
addContainer: (index: number, type: string, parent: string) => void addContainer: (index: number, type: string, parent: string) => void
isExpanded: boolean onExpandChange: (value: ExtendedSidebar) => void
onExpandChange: () => void
} }
function RemoveBorderClasses(target: HTMLButtonElement, exception: string = ''): 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 // States
const divRef = React.useRef<HTMLDivElement>(null); const divRef = React.useRef<HTMLDivElement>(null);
const [,height] = useSize(divRef); const [,height] = useSize(divRef);
const [showProperties, setShowProperties] = useState(props.isExpanded);
// Render // Render
const it = MakeRecursionDFSIterator(props.mainContainer, props.containers, 0, [0, 0], true); const it = MakeRecursionDFSIterator(props.mainContainer, props.containers, 0, [0, 0], true);
@ -167,18 +166,27 @@ export function ElementsSideBar(props: IElementsSideBarProps): JSX.Element {
return ( return (
<div className='flex flex-row h-full w-full' > <div className='flex flex-row h-full w-full' >
{showProperties && {props.selectedExtendedSidebar === ExtendedSidebar.Property &&
<div className='flex flex-1 flex-col w-64 border-r-2 border-slate-400'> <div className='flex flex-1 flex-col w-64 border-r-2 border-slate-400'>
<ContainerProperties <ContainerProperties
properties={props.selectedContainer?.properties} properties={props.selectedContainer?.properties}
symbols={props.symbols} symbols={props.symbols}
onChange={props.onPropertyChange} onChange={props.onPropertyChange}
/> />
</div> </div>
} }
<div className='flex w-64' ref={divRef}> <div className='flex w-64' ref={divRef}>
<div className='w-6'> <div className='w-6'>
<ToggleSideBar title={Text({ textId: '@Properties' })} checked={showProperties} onChange={(newValue) => { setShowProperties(newValue); props.onExpandChange(); }} /> <ToggleSideBar
title={Text({ textId: '@Properties' })}
checked={props.selectedExtendedSidebar === ExtendedSidebar.Property}
onClick={() => {
const newValue = props.selectedExtendedSidebar !== ExtendedSidebar.Property
? ExtendedSidebar.Property
: ExtendedSidebar.None;
props.onExpandChange(newValue);
}}
/>
</div> </div>
<List <List
itemCount={containers.length} itemCount={containers.length}
@ -229,10 +237,10 @@ function ElementsListRow(
key={key} key={key}
style={style} style={style}
title={container.properties.warning} title={container.properties.warning}
onClick={() => selectContainer(container.properties.id)} onClick={() => { selectContainer(container.properties.id); }}
onDrop={(event) => HandleOnDrop(event, containers, mainContainer, addContainer)} onDrop={(event) => { HandleOnDrop(event, containers, mainContainer, addContainer); }}
onDragOver={(event) => HandleDragOver(event, mainContainer)} onDragOver={(event) => { HandleDragOver(event, mainContainer); }}
onDragLeave={(event) => HandleDragLeave(event)} onDragLeave={(event) => { HandleDragLeave(event); }}
> >
{verticalBars} {verticalBars}
{text} {text}

View file

@ -21,6 +21,7 @@ export interface IMenuAction {
/** function to be called on button click */ /** function to be called on button click */
action: (target: HTMLElement) => void action: (target: HTMLElement) => void
} }
function UseMouseEvents( function UseMouseEvents(
@ -139,7 +140,7 @@ function AddClassSpecificActions(
onClick={() => action.action(target)} />); onClick={() => action.action(target)} />);
}); });
children.push(<hr key={`contextmenu-hr-${count}`} className='border-slate-400' />); children.push(<hr key={`contextmenu-hr-${count}`} className='border-slate-400' />);
}; }
return count; return count;
} }

View file

@ -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<string, IContainerModel>
roots: IContainerModel | IContainerModel[] | null
scale?: number
}
function GetDimensionsNodes(
containers: Map<string, IContainerModel>,
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 (
<g>
{dimensions}
</g>
);
}
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(
<Dimension
key={id}
id={id}
xStart={xStart}
yStart={y}
xEnd={xEnd}
yEnd={y}
text={text}
scale={scale}
color={color}
/>
);
}

View file

@ -1,6 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { type IDimensionOptions } from '../../../Interfaces/IDimensionOptions';
import { NOTCHES_LENGTH } from '../../../utils/default'; import { NOTCHES_LENGTH } from '../../../utils/default';
export type IDimensionStyle = Omit<IDimensionOptions, 'positions'>;
interface IDimensionProps { interface IDimensionProps {
id: string id: string
xStart: number xStart: number
@ -8,7 +11,7 @@ interface IDimensionProps {
xEnd: number xEnd: number
yEnd: number yEnd: number
text: string text: string
color: string style: IDimensionStyle
scale?: number scale?: number
} }
@ -28,8 +31,9 @@ function ApplyParametric(x0: number, t: number, vx: number): number {
export function Dimension(props: IDimensionProps): JSX.Element { export function Dimension(props: IDimensionProps): JSX.Element {
const scale = props.scale ?? 1; const scale = props.scale ?? 1;
const style: React.CSSProperties = { const style: React.CSSProperties = {
stroke: props.color, stroke: props.style.color,
strokeWidth: 2 / scale strokeWidth: (props.style.width ?? 2) / scale,
strokeDasharray: props.style.dashArray
}; };
/// We need to find the points of the notches /// We need to find the points of the notches
@ -79,9 +83,11 @@ export function Dimension(props: IDimensionProps): JSX.Element {
x2={endBottomX} x2={endBottomX}
y2={endBottomY} y2={endBottomY}
style={style}/> style={style}/>
<text textAnchor={'middle'} alignmentBaseline={'central'} <text
x={textX} x={textX}
y={textY} y={textY}
textAnchor={'middle'}
alignmentBaseline={'central'}
style={{ style={{
transform: `rotate(${rotation}turn) scale(${1 / scale})`, transform: `rotate(${rotation}turn) scale(${1 / scale})`,
transformOrigin: `${textX}px ${textY}px` transformOrigin: `${textX}px ${textY}px`

View file

@ -1,7 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { Position } from '../../../Enums/Position'; import { Position } from '../../../Enums/Position';
import { type IContainerModel } from '../../../Interfaces/IContainerModel';
import { import {
DIMENSION_MARGIN, DIMENSION_MARGIN,
SHOW_BORROWER_DIMENSIONS, SHOW_BORROWER_DIMENSIONS,
@ -11,7 +10,8 @@ import {
} from '../../../utils/default'; } from '../../../utils/default';
import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../../utils/itertools'; import { FindContainerById, MakeRecursionDFSIterator, Pairwise } from '../../../utils/itertools';
import { TransformX, TransformY } from '../../../utils/svg'; import { TransformX, TransformY } from '../../../utils/svg';
import { Dimension } from './Dimension'; import { Dimension, type IDimensionStyle } from './Dimension';
import { type IContainerModel } from '../../../Interfaces/IContainerModel';
import { type ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { type ISymbolModel } from '../../../Interfaces/ISymbolModel';
interface IDimensionLayerProps { interface IDimensionLayerProps {
@ -252,10 +252,10 @@ function AddHorizontalChildrenDimension(
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number, scale: number
color: string
): void { ): void {
const childrenId = `dim-y${yDim.toFixed(0)}-children-${container.properties.id}`; 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 lastChildId = container.children[container.children.length - 1];
const lastChild = FindContainerById(containers, lastChildId); const lastChild = FindContainerById(containers, lastChildId);
@ -305,7 +305,7 @@ function AddHorizontalChildrenDimension(
yEnd={yDim} yEnd={yDim}
text={textChildren} text={textChildren}
scale={scale} scale={scale}
color={color}/>); style={style}/>);
} }
function AddVerticalChildrenDimension( function AddVerticalChildrenDimension(
@ -315,10 +315,10 @@ function AddVerticalChildrenDimension(
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number, scale: number
color: string
): void { ): void {
const childrenId = `dim-x${xDim.toFixed(0)}-children-${container.properties.id}`; 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 lastChildId = container.children[container.children.length - 1];
const lastChild = FindContainerById(containers, lastChildId); const lastChild = FindContainerById(containers, lastChildId);
@ -373,7 +373,7 @@ function AddVerticalChildrenDimension(
yEnd={yChildrenEnd + offset} yEnd={yChildrenEnd + offset}
text={textChildren} text={textChildren}
scale={scale} scale={scale}
color={color} style={style}
/>); />);
} }
@ -384,9 +384,9 @@ function AddHorizontalBorrowerDimension(
depth: number, depth: number,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number, scale: number
color: string
): void { ): void {
const style = container.properties.dimensionOptions.dimensionWithMarks;
const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const marks = []; // list of vertical lines for the dimension const marks = []; // list of vertical lines for the dimension
for (const { for (const {
@ -431,7 +431,7 @@ function AddHorizontalBorrowerDimension(
yEnd={yDim} yEnd={yDim}
text={value.toFixed(0)} text={value.toFixed(0)}
scale={scale} scale={scale}
color={color}/>); style={style}/>);
count++; count++;
} }
} }
@ -444,9 +444,9 @@ function AddVerticalBorrowerDimension(
depth: number, depth: number,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number, scale: number
color: string
): void { ): void {
const style = container.properties.dimensionOptions.dimensionWithMarks;
const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform); const it = MakeRecursionDFSIterator(container, containers, depth, currentTransform);
const marks = []; // list of vertical lines for the dimension const marks = []; // list of vertical lines for the dimension
for (const { for (const {
@ -496,7 +496,7 @@ function AddVerticalBorrowerDimension(
yEnd={next} yEnd={next}
text={value.toFixed(0)} text={value.toFixed(0)}
scale={scale} scale={scale}
color={color}/>); style={style}/>);
count++; count++;
} }
} }
@ -507,9 +507,9 @@ function AddVerticalSelfDimension(
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number, scale: number
color: string
): void { ): void {
const style = container.properties.dimensionOptions.selfDimensions;
const height = container.properties.height; const height = container.properties.height;
const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`; const idVert = `dim-x${xDim.toFixed(0)}-${container.properties.id}`;
let yStart = container.properties.y + currentTransform[1] + height; let yStart = container.properties.y + currentTransform[1] + height;
@ -532,7 +532,7 @@ function AddVerticalSelfDimension(
yEnd={yEnd} yEnd={yEnd}
text={textVert} text={textVert}
scale={scale} scale={scale}
color={color}/> style={style}/>
); );
} }
@ -541,9 +541,9 @@ function AddHorizontalSelfDimension(
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number, scale: number
color: string
): void { ): void {
const style = container.properties.dimensionOptions.selfDimensions;
const width = container.properties.width; const width = container.properties.width;
const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`; const id = `dim-y${yDim.toFixed(0)}-${container.properties.id}`;
const xStart = container.properties.x + currentTransform[0]; const xStart = container.properties.x + currentTransform[0];
@ -561,7 +561,7 @@ function AddHorizontalSelfDimension(
yEnd={yDim} yEnd={yDim}
text={text} text={text}
scale={scale} scale={scale}
color={color}/> style={style}/>
); );
} }
@ -570,9 +570,9 @@ function AddHorizontalSelfMarginsDimension(
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number, scale: number
color: string
): void { ): void {
const style = container.properties.dimensionOptions.selfMarginsDimensions;
const left = container.properties.margin.left; const left = container.properties.margin.left;
if (left != null) { if (left != null) {
const id = `dim-y-margin-left${yDim.toFixed(0)}-${container.properties.id}`; const id = `dim-y-margin-left${yDim.toFixed(0)}-${container.properties.id}`;
@ -591,7 +591,7 @@ function AddHorizontalSelfMarginsDimension(
yEnd={yDim} yEnd={yDim}
text={text} text={text}
scale={scale} scale={scale}
color={color}/> style={style}/>
); );
} }
@ -613,7 +613,7 @@ function AddHorizontalSelfMarginsDimension(
yEnd={yDim} yEnd={yDim}
text={text} text={text}
scale={scale} scale={scale}
color={color}/> style={style}/>
); );
} }
} }
@ -624,9 +624,9 @@ function AddVerticalSelfMarginDimension(
container: IContainerModel, container: IContainerModel,
currentTransform: [number, number], currentTransform: [number, number],
dimensions: React.ReactNode[], dimensions: React.ReactNode[],
scale: number, scale: number
color: string
): void { ): void {
const style = container.properties.dimensionOptions.selfMarginsDimensions;
const top = container.properties.margin.top; const top = container.properties.margin.top;
if (top != null) { if (top != null) {
const idVert = `dim-x-margin-top${xDim.toFixed(0)}-${container.properties.id}`; const idVert = `dim-x-margin-top${xDim.toFixed(0)}-${container.properties.id}`;
@ -650,7 +650,7 @@ function AddVerticalSelfMarginDimension(
yEnd={yEnd} yEnd={yEnd}
text={textVert} text={textVert}
scale={scale} scale={scale}
color={color}/> style={style}/>
); );
} }
const bottom = container.properties.margin.bottom; const bottom = container.properties.margin.bottom;
@ -676,7 +676,44 @@ function AddVerticalSelfMarginDimension(
yEnd={yEnd} yEnd={yEnd}
text={textVert} text={textVert}
scale={scale} 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(
<Dimension
key={id}
id={id}
xStart={0}
yStart={-offset}
xEnd={width}
yEnd={-offset}
text={text}
scale={scale}
style={defaultDimensionSymbolStyle}/>
);
}

View file

@ -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 (
<>
<rect
x={x}
y={y}
width={width}
height={height}
style={style}
>
</rect>
{SHOW_SELECTOR_TEXT
? <text
x={xText}
y={yText}
style={{
transform: `scale(${1 / scale}) translateX(-50%)`,
transformBox: 'fill-box'
}}
>
{ text }
</text>
: null}
</>
);
}

View file

@ -1,9 +1,9 @@
import '../Selector.scss'; import '../Selector.scss';
import * as React from 'react'; import * as React from 'react';
import { type IContainerModel } from '../../../../Interfaces/IContainerModel'; import { type IContainerModel } from '../../../../Interfaces/IContainerModel';
import { SHOW_SELECTOR_TEXT } from '../../../../utils/default';
import { GetAbsolutePosition } from '../../../../utils/itertools'; import { GetAbsolutePosition } from '../../../../utils/itertools';
import { RemoveMargin } from '../../../../utils/svg'; import { RemoveMargin } from '../../../../utils/svg';
import { Selector } from '../Selector/Selector';
interface ISelectorContainerProps { interface ISelectorContainerProps {
containers: Map<string, IContainerModel> containers: Map<string, IContainerModel>
@ -33,41 +33,14 @@ export function SelectorContainer(props: ISelectorContainerProps): JSX.Element {
props.selected.properties.margin.right 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 ( return (
<> <Selector
<rect text={props.selected.properties.displayedText}
x={x} x={x}
y={y} y={y}
width={width} width={width}
height={height} height={height}
style={style} scale={scale}
> />
</rect>
{SHOW_SELECTOR_TEXT
? <text
x={xText}
y={yText}
style={{
transform: `scale(${1 / scale}) translateX(-50%)`,
transformBox: 'fill-box'
}}
>
{props.selected.properties.displayedText}
</text>
: null}
</>
); );
} }

View file

@ -1,7 +1,8 @@
import '../Selector.scss'; import '../Selector.scss';
import * as React from 'react'; 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 { type ISymbolModel } from '../../../../Interfaces/ISymbolModel';
import { Selector } from '../Selector/Selector';
interface ISelectorSymbolProps { interface ISelectorSymbolProps {
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
@ -19,7 +20,7 @@ export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element {
const scale = (props.scale ?? 1); const scale = (props.scale ?? 1);
const [width, height] = [ const [width, height] = [
props.selected.width, props.selected.width / scale,
props.selected.height / scale props.selected.height / scale
]; ];
@ -29,45 +30,28 @@ export function SelectorSymbol(props: ISelectorSymbolProps): JSX.Element {
x = -SYMBOL_MARGIN; x = -SYMBOL_MARGIN;
y = props.selected.offset; y = props.selected.offset;
} else { } else {
x = props.selected.offset; [x,y] = [
y = -SYMBOL_MARGIN - height; props.selected.offset + props.selected.width / 2,
-SYMBOL_MARGIN - height]
} }
const xText = x + width / 2; const xText = x + width / 2;
const yText = y + height / 2; const yText = y + height / 2;
const style: React.CSSProperties = { const style: React.CSSProperties = {
stroke: '#3B82F6', transform: 'translateX(-50%)',
strokeWidth: 4 / scale, transformBox: 'fill-box'
fillOpacity: 0,
transitionProperty: 'all',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '150ms',
animation: 'fadein 750ms ease-in alternate infinite'
}; };
return ( return (
<> <Selector
<rect text={props.selected.displayedText}
x={x} x={x}
y={y} y={y}
width={width} width={width}
height={height} height={height}
style={style} scale={scale}
> style={style}
</rect> />
{SHOW_SELECTOR_TEXT
? <text
x={xText}
y={yText}
style={{
transform: `scale(${1 / scale}) translateX(-50%)`,
transformBox: 'fill-box'
}}
>
{props.selected.displayedText}
</text>
: null}
</>
); );
} }

View file

@ -1,14 +1,13 @@
import * as React from 'react'; 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 { Container } from './Elements/Container';
import { type IContainerModel } from '../../Interfaces/IContainerModel';
import { SelectorContainer } from './Elements/SelectorContainer/SelectorContainer'; import { SelectorContainer } from './Elements/SelectorContainer/SelectorContainer';
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer'; import { MAX_FRAMERATE } from '../../utils/default';
import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
import { SymbolLayer } from './Elements/SymbolLayer'; import { SymbolLayer } from './Elements/SymbolLayer';
import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
import { DimensionLayer } from './Elements/DimensionLayer'; import { DimensionLayer } from './Elements/DimensionLayer';
import { SelectorSymbol } from './Elements/SelectorSymbol/SelectorSymbol'; import { SelectorSymbol } from './Elements/SelectorSymbol/SelectorSymbol';
import { type IToolbarProps, Toolbar } from './SVGReactPanZoom/ui-toolbar/toolbar';
import { type DrawParams } from '../Viewer/Viewer';
interface ISVGProps { interface ISVGProps {
className?: string className?: string
@ -16,19 +15,27 @@ interface ISVGProps {
viewerHeight: number viewerHeight: number
width: number width: number
height: number height: number
containers: Map<string, IContainerModel> drawParams: DrawParams
children: IContainerModel
selectedContainer?: IContainerModel
symbols: Map<string, ISymbolModel>
selectedSymbol?: ISymbolModel
selectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
isComponentsOpen: boolean }
isSymbolsOpen: boolean
export enum SelectorMode {
Nothing,
Containers,
Symbols
} }
export const ID = 'svg'; export const ID = 'svg';
export function SVG(props: ISVGProps): JSX.Element { export function SVG(props: ISVGProps): JSX.Element {
const {
mainContainer,
selectorMode,
selectedContainer,
selectedSymbol,
containers,
symbols
} = props.drawParams;
const [tool, setTool] = React.useState<Tool>(TOOL_PAN); const [tool, setTool] = React.useState<Tool>(TOOL_PAN);
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const [value, setValue] = React.useState<Value>({} as Value); const [value, setValue] = React.useState<Value>({} as Value);
@ -54,14 +61,33 @@ export function SVG(props: ISVGProps): JSX.Element {
}; };
const children: React.ReactNode | React.ReactNode[] = <Container const children: React.ReactNode | React.ReactNode[] = <Container
key={`container-${props.children.properties.id}`} key={`container-${mainContainer.properties.id}`}
containers={props.containers} containers={containers}
model={props.children} model={mainContainer}
depth={0} depth={0}
scale={scale} scale={scale}
selectContainer={props.selectContainer} selectContainer={props.selectContainer}
/>; />;
function Selector(): JSX.Element {
switch (selectorMode) {
case SelectorMode.Containers:
return <SelectorContainer
containers={containers}
scale={scale}
selected={selectedContainer}
/>;
case SelectorMode.Symbols:
return <SelectorSymbol
symbols={symbols}
scale={scale}
selected={selectedSymbol}
/>;
default:
return <></>;
}
}
return ( return (
<div id={ID} className={props.className}> <div id={ID} className={props.className}>
<ReactSVGPanZoom <ReactSVGPanZoom
@ -86,6 +112,9 @@ export function SVG(props: ISVGProps): JSX.Element {
const value = event as Value; const value = event as Value;
setScale(value.a); setScale(value.a);
}} }}
onDoubleClick={() => {
svgViewer?.current?.setPointOnViewerCenter(props.width / 2, props.height / 2, 0.8);
}}
background={'#ffffff'} background={'#ffffff'}
defaultTool='pan' defaultTool='pan'
miniatureProps={{ miniatureProps={{
@ -94,17 +123,25 @@ export function SVG(props: ISVGProps): JSX.Element {
width: 120, width: 120,
height: 120 height: 120
}} }}
customToolbar={(props: IToolbarProps) => (
<Toolbar
{...props}
SVGAlignX={ALIGN_CENTER}
SVGAlignY={ALIGN_CENTER}
fittingScale={0.8}
/>
)}
> >
<svg {...properties}> <svg {...properties}>
{children} {children}
{SHOW_DIMENSIONS_PER_DEPTH <DimensionLayer
? <DepthDimensionLayer containers={props.containers} scale={scale} roots={props.children} /> containers={containers}
: null} symbols={symbols}
<DimensionLayer containers={props.containers} symbols={props.symbols} scale={scale} root={props.children} /> scale={scale}
<SymbolLayer scale={scale} symbols={props.symbols} /> root={mainContainer}
{/* leave this at the end so it can be removed during the svg export */} />
{ props.isComponentsOpen ? <SelectorContainer containers={props.containers} scale={scale} selected={props.selectedContainer} /> : null } <SymbolLayer scale={scale} symbols={symbols} />
{ props.isSymbolsOpen ? <SelectorSymbol symbols={props.symbols} scale={scale} selected={props.selectedSymbol} /> : null } <Selector />
</svg> </svg>
</ReactSVGPanZoom> </ReactSVGPanZoom>
</div> </div>
@ -112,7 +149,8 @@ export function SVG(props: ISVGProps): JSX.Element {
} }
function UseFitOnce(svgViewer: React.RefObject<ReactSVGPanZoom>, width: number, height: number): void { function UseFitOnce(svgViewer: React.RefObject<ReactSVGPanZoom>, width: number, height: number): void {
React.useEffect(() => { React.useCallback(() => {
svgViewer?.current?.fitToViewer(); // TODO: Fix this
svgViewer?.current?.setPointOnViewerCenter(width / 2, height / 2, 0.8);
}, [svgViewer, width, height]); }, [svgViewer, width, height]);
} }

View file

@ -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.

View file

@ -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<IToolbarButtonProps, IToolbarButtonState> {
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 (
<button
onMouseEnter={e => { this.change(e); }}
onMouseLeave={e => { this.change(e); }}
onTouchStart={e => {
this.change(e);
this.props.onClick(e);
}}
onTouchEnd={e => { this.change(e); }}
onTouchCancel={e => { this.change(e); }}
onClick={this.props.onClick}
style={style}
title={this.props.title}
name={this.props.name}
type="button"
>{this.props.children}</button>
);
}
}

View file

@ -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 (
<div style={style} role="toolbar">
<ToolbarButton
toolbarPosition={position}
active={tool === TOOL_NONE}
activeColor={activeToolColor}
name="unselect-tools"
title="Selection"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleChangeTool(event, TOOL_NONE); } }>
<CursorArrowRaysIcon/>
</ToolbarButton>
<ToolbarButton
toolbarPosition={position}
active={tool === TOOL_PAN}
activeColor={activeToolColor}
name="select-tool-pan"
title="Pan"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleChangeTool(event, TOOL_PAN); } }>
<HandRaisedIcon/>
</ToolbarButton>
<ToolbarButton
toolbarPosition={position}
active={tool === TOOL_ZOOM_IN}
activeColor={activeToolColor}
name="select-tool-zoom-in"
title="Zoom in"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleChangeTool(event, TOOL_ZOOM_IN); } }>
<MagnifyingGlassPlusIcon/>
</ToolbarButton>
<ToolbarButton
toolbarPosition={position}
active={tool === TOOL_ZOOM_OUT}
activeColor={activeToolColor}
name="select-tool-zoom-out"
title="Zoom out"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleChangeTool(event, TOOL_ZOOM_OUT); } }>
<MagnifyingGlassMinusIcon/>
</ToolbarButton>
<ToolbarButton
toolbarPosition={position}
active={false}
activeColor={activeToolColor}
name="fit-to-viewer"
title="Fit to viewer"
onClick={ (event: React.MouseEvent | React.TouchEvent) => { handleFit(event); } }>
<ArrowsPointingOutIcon/>
</ToolbarButton>
</div>
);
}

View file

@ -4,16 +4,16 @@ import './ToggleSideBar.scss';
interface IToggleSidebarProps { interface IToggleSidebarProps {
title: string title: string
checked: boolean 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 ( return (
<div className={`${(checked ? 'bg-slate-400 hover:bg-slate-500' : 'bg-slate-300 hover:bg-slate-400')}`}> <div className={`${(checked ? 'bg-slate-400 hover:bg-slate-500' : 'bg-slate-300 hover:bg-slate-400')}`}>
<button <button
className={'w-full py-2'} className={'w-full py-2'}
type='button' type='button'
onClick={() => onChange(!checked)} onClick={onClick}
> >
<p className='text-vertical'>{title} <p className='text-vertical'>{title}
</p> </p>

View file

@ -5,39 +5,47 @@ import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
import { SymbolProperties } from '../SymbolProperties/SymbolProperties'; import { SymbolProperties } from '../SymbolProperties/SymbolProperties';
import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar'; import { ToggleSideBar } from '../Sidebar/ToggleSideBar/ToggleSideBar';
import { Text } from '../Text/Text'; import { Text } from '../Text/Text';
import { useState } from 'react'; import { ExtendedSidebar } from '../UI/UI';
interface ISymbolsSidebarProps { interface ISymbolsSidebarProps {
selectedSymbolId: string selectedSymbolId: string
symbols: Map<string, ISymbolModel> symbols: Map<string, ISymbolModel>
selectedExtendedSidebar: ExtendedSidebar
onPropertyChange: (key: string, value: string | number | boolean) => void onPropertyChange: (key: string, value: string | number | boolean) => void
selectSymbol: (symbolId: string) => void selectSymbol: (symbolId: string) => void
isExpanded: boolean onExpandChange: (value: ExtendedSidebar) => void
onExpandChange: (isExpanded: boolean) => void
} }
export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element { export function SymbolsSidebar(props: ISymbolsSidebarProps): JSX.Element {
// States // States
const divRef = React.useRef<HTMLDivElement>(null); const divRef = React.useRef<HTMLDivElement>(null);
const height = useSize(divRef)[1]; const height = useSize(divRef)[1];
const [showProperties, setShowProperties] = useState(props.isExpanded);
// Render // Render
const symbols = [...props.symbols.values()]; const symbols = [...props.symbols.values()];
const selectedSymbol = props.symbols.get(props.selectedSymbolId); const selectedSymbol = props.symbols.get(props.selectedSymbolId);
return ( return (
<div className='flex flex-row h-full w-full'> <div className='flex flex-row h-full w-full'>
{showProperties && <div className='flex flex-1 flex-col w-64 border-r-2 border-slate-400'> {props.selectedExtendedSidebar === ExtendedSidebar.Property &&
{(selectedSymbol == null) && <h1 className={'p-4'}>{Text({ textId: '@NoSymbolSelected' })}</h1>} <div className='flex flex-1 flex-col w-64 border-r-2 border-slate-400'>
<SymbolProperties {(selectedSymbol == null) && <h1 className={'p-4'}>{Text({ textId: '@NoSymbolSelected' })}</h1>}
symbol={selectedSymbol} <SymbolProperties
symbols={props.symbols} symbol={selectedSymbol}
onChange={props.onPropertyChange} symbols={props.symbols}
/> onChange={props.onPropertyChange}
</div>} />
</div>}
<div className={'flex w-64'} ref={divRef}> <div className={'flex w-64'} ref={divRef}>
<div className='w-6'> <div className='w-6'>
<ToggleSideBar title={Text({ textId: '@Properties' })} checked={showProperties} onChange={(newValue) => { setShowProperties(newValue); props.onExpandChange(newValue); }} /> <ToggleSideBar
title={Text({ textId: '@Properties' })}
checked={props.selectedExtendedSidebar === ExtendedSidebar.Property}
onClick={() => {
const newValue = props.selectedExtendedSidebar !== ExtendedSidebar.Property
? ExtendedSidebar.Property
: ExtendedSidebar.None;
props.onExpandChange(newValue);
}} />
</div> </div>
<List <List
itemCount={symbols.length} itemCount={symbols.length}

View file

@ -1,9 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { ElementsSideBar } from '../ElementsList/ElementsSideBar'; import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar';
import { History } from '../History/History'; import { History } from '../History/History';
import { Bar, BAR_WIDTH } from '../Bar/Bar'; import { Bar, BAR_WIDTH } from '../Bar/Bar';
import { Symbols } from '../Symbols/Symbols'; import { Symbols } from '../Symbols/Symbols';
import { SymbolsSidebar } from '../SymbolsList/SymbolsSidebar'; import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
import { type PropertyType } from '../../Enums/PropertyType'; import { type PropertyType } from '../../Enums/PropertyType';
import { Messages } from '../Messages/Messages'; import { Messages } from '../Messages/Messages';
import { Sidebar } from '../Sidebar/Sidebar'; import { Sidebar } from '../Sidebar/Sidebar';
@ -17,13 +17,16 @@ import { FindContainerById } from '../../utils/itertools';
import { type IEditorState } from '../../Interfaces/IEditorState'; import { type IEditorState } from '../../Interfaces/IEditorState';
import { GetCurrentHistoryState } from '../Editor/Editor'; import { GetCurrentHistoryState } from '../Editor/Editor';
import { Text } from '../Text/Text'; import { Text } from '../Text/Text';
import { type IReplaceContainer } from '../../Interfaces/IReplaceContainer';
import { type Dispatch } from 'react';
export interface IUIProps { export interface IUIProps {
editorState: IEditorState editorState: IEditorState
replaceContainer: IReplaceContainer
selectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
deleteContainer: (containerId: string) => void deleteContainer: (containerId: string) => void
onPropertyChange: (key: string, value: string | number | boolean | number[], type?: PropertyType) => void onPropertyChange: (key: string, value: string | number | boolean | number[], type?: PropertyType) => void
addContainer: (type: string) => void addOrReplaceContainer: (type: string) => void
addContainerAt: (index: number, type: string, parent: string) => void addContainerAt: (index: number, type: string, parent: string) => void
addSymbol: (type: string) => void addSymbol: (type: string) => void
onSymbolPropertyChange: (key: string, value: string | number | boolean) => void onSymbolPropertyChange: (key: string, value: string | number | boolean) => void
@ -32,19 +35,24 @@ export interface IUIProps {
saveEditorAsJSON: () => void saveEditorAsJSON: () => void
saveEditorAsSVG: () => void saveEditorAsSVG: () => void
loadState: (move: number) => void loadState: (move: number) => void
setReplaceContainer: Dispatch<React.SetStateAction<IReplaceContainer>>
} }
export enum SidebarType { export enum SidebarType {
None, None,
Components, Components,
ComponentsExpanded,
Symbols, Symbols,
SymbolsExpanded,
History, History,
Messages, Messages,
Settings Settings
} }
export enum ExtendedSidebar {
None,
Property
}
function UseSetOrToggleSidebar( function UseSetOrToggleSidebar(
selectedSidebar: SidebarType, selectedSidebar: SidebarType,
setSelectedSidebar: React.Dispatch<React.SetStateAction<SidebarType>> setSelectedSidebar: React.Dispatch<React.SetStateAction<SidebarType>>
@ -59,8 +67,12 @@ function UseSetOrToggleSidebar(
}; };
} }
export function UI({ editorState, ...methods }: IUIProps): JSX.Element { export function UI({ editorState, replaceContainer, setReplaceContainer, ...methods }: IUIProps): JSX.Element {
const [selectedSidebar, setSelectedSidebar] = React.useState<SidebarType>(SidebarType.Components); const [selectedSidebar, setSelectedSidebar] = React.useState<SidebarType>(SidebarType.Components);
const [
selectedExtendedSidebar,
setSelectedExtendedSidebarType
] = React.useState<ExtendedSidebar>(ExtendedSidebar.None);
const [messages, setMessages] = React.useState<IMessage[]>([]); const [messages, setMessages] = React.useState<IMessage[]>([]);
const current = GetCurrentHistoryState(editorState.history, editorState.historyCurrentStep); const current = GetCurrentHistoryState(editorState.history, editorState.historyCurrentStep);
@ -101,14 +113,16 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
switch (selectedSidebar) { switch (selectedSidebar) {
case SidebarType.Components: case SidebarType.Components:
leftSidebarTitle = Text({ textId: '@Components' }); leftSidebarTitle = Text({ textId: '@Components' });
leftChildren = <Components leftChildren = <Components
selectedContainer={selectedContainer} selectedContainer={selectedContainer}
componentOptions={configuration.AvailableContainers} componentOptions={configuration.AvailableContainers}
categories={configuration.Categories} categories={configuration.Categories}
buttonOnClick={methods.addContainer} buttonOnClick={methods.addOrReplaceContainer}
/>; replaceContainer={replaceContainer}
setReplaceContainer={setReplaceContainer}/>;
rightSidebarTitle = Text({ textId: '@Elements' }); rightSidebarTitle = Text({ textId: '@Elements' });
rightChildren = <ElementsSideBar rightChildren = <ElementsSidebar
containers={current.containers} containers={current.containers}
mainContainer={mainContainer} mainContainer={mainContainer}
symbols={current.symbols} symbols={current.symbols}
@ -116,31 +130,11 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
onPropertyChange={methods.onPropertyChange} onPropertyChange={methods.onPropertyChange}
selectContainer={methods.selectContainer} selectContainer={methods.selectContainer}
addContainer={methods.addContainerAt} addContainer={methods.addContainerAt}
isExpanded ={false} selectedExtendedSidebar={selectedExtendedSidebar}
onExpandChange={() => { setOrToggleSidebar(SidebarType.ComponentsExpanded); } } onExpandChange={(value) => { setSelectedExtendedSidebarType(value); } }
/>;
break;
case SidebarType.ComponentsExpanded:
leftSidebarTitle = Text({ textId: '@Components' });
leftChildren = <Components
selectedContainer={selectedContainer}
componentOptions={configuration.AvailableContainers}
categories={configuration.Categories}
buttonOnClick={methods.addContainer}
/>;
rightSidebarTitle = Text({ textId: '@Elements' });
rightChildren = <ElementsSideBar
containers={current.containers}
mainContainer={mainContainer}
symbols={current.symbols}
selectedContainer={selectedContainer}
onPropertyChange={methods.onPropertyChange}
selectContainer={methods.selectContainer}
addContainer={methods.addContainerAt}
isExpanded ={true}
onExpandChange={() => { setOrToggleSidebar(SidebarType.Components); } }
/>; />;
break; break;
case SidebarType.Symbols: case SidebarType.Symbols:
leftSidebarTitle = Text({ textId: '@SymbolsLeft' }); leftSidebarTitle = Text({ textId: '@SymbolsLeft' });
leftChildren = <Symbols leftChildren = <Symbols
@ -153,24 +147,8 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
symbols={current.symbols} symbols={current.symbols}
onPropertyChange={methods.onSymbolPropertyChange} onPropertyChange={methods.onSymbolPropertyChange}
selectSymbol={methods.selectSymbol} selectSymbol={methods.selectSymbol}
isExpanded ={false} selectedExtendedSidebar={selectedExtendedSidebar}
onExpandChange={() => { setOrToggleSidebar(SidebarType.SymbolsExpanded); } } onExpandChange={(value) => { setSelectedExtendedSidebarType(value); } }
/>;
break;
case SidebarType.SymbolsExpanded:
leftSidebarTitle = Text({ textId: '@SymbolsLeft' });
leftChildren = <Symbols
componentOptions={configuration.AvailableSymbols}
buttonOnClick={methods.addSymbol}
/>;
rightSidebarTitle = Text({ textId: '@SymbolsRight' });
rightChildren = <SymbolsSidebar
selectedSymbolId={current.selectedSymbolId}
symbols={current.symbols}
onPropertyChange={methods.onSymbolPropertyChange}
selectSymbol={methods.selectSymbol}
isExpanded ={true}
onExpandChange={() => { setOrToggleSidebar(SidebarType.Symbols); }}
/>; />;
break; break;
@ -202,8 +180,10 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
} }
const isLeftSidebarOpen = selectedSidebar !== SidebarType.None; const isLeftSidebarOpen = selectedSidebar !== SidebarType.None;
const isRightSidebarOpen = selectedSidebar === SidebarType.Components || selectedSidebar === SidebarType.Symbols; const isRightSidebarOpen = [SidebarType.Components, SidebarType.Symbols]
const isRightSidebarOpenExpanded = selectedSidebar === SidebarType.ComponentsExpanded || selectedSidebar === SidebarType.SymbolsExpanded; .includes(selectedSidebar);
const isRightSidebarOpenExpanded = isRightSidebarOpen &&
selectedExtendedSidebar !== ExtendedSidebar.None;
const isLeftSidebarOpenClasses = new Set<string>([ const isLeftSidebarOpenClasses = new Set<string>([
'left-sidebar', 'left-sidebar',
@ -243,30 +223,24 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
isLeftSidebarOpenClasses.add('left-sidebar-single'); isLeftSidebarOpenClasses.add('left-sidebar-single');
} }
const isComponentsOpen = selectedSidebar === SidebarType.Components || selectedSidebar === SidebarType.ComponentsExpanded; const clickRestrictionsClasses = replaceContainer.isReplacing ? 'pointer-events-none opacity-50' : '';
const isSymbolsOpen = selectedSidebar === SidebarType.Symbols || selectedSidebar === SidebarType.SymbolsExpanded; const isComponentsOpen = selectedSidebar === SidebarType.Components;
const isSymbolsOpen = selectedSidebar === SidebarType.Symbols;
return ( return (
<> <>
<Bar <Bar
className={clickRestrictionsClasses}
isComponentsOpen={isComponentsOpen} isComponentsOpen={isComponentsOpen}
isSymbolsOpen={isSymbolsOpen} isSymbolsOpen={isSymbolsOpen}
isHistoryOpen={selectedSidebar === SidebarType.History} isHistoryOpen={selectedSidebar === SidebarType.History}
isMessagesOpen={selectedSidebar === SidebarType.Messages} isMessagesOpen={selectedSidebar === SidebarType.Messages}
isSettingsOpen={selectedSidebar === SidebarType.Settings} isSettingsOpen={selectedSidebar === SidebarType.Settings}
toggleComponents={() => { toggleComponents={() => {
if (selectedSidebar === SidebarType.ComponentsExpanded) { setOrToggleSidebar(SidebarType.Components);
setOrToggleSidebar(SidebarType.ComponentsExpanded);
} else {
setOrToggleSidebar(SidebarType.Components);
}
} } } }
toggleSymbols={() => { toggleSymbols={() => {
if (selectedSidebar === SidebarType.SymbolsExpanded) { setOrToggleSidebar(SidebarType.Symbols);
setOrToggleSidebar(SidebarType.SymbolsExpanded);
} else {
setOrToggleSidebar(SidebarType.Symbols);
}
} } } }
toggleTimeline={() => { toggleTimeline={() => {
setOrToggleSidebar(SidebarType.History); setOrToggleSidebar(SidebarType.History);
@ -285,7 +259,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
{ leftChildren } { leftChildren }
</Sidebar> </Sidebar>
<Viewer <Viewer
className={`${[...viewerMarginClasses.values()].join(' ')} w-full h-full`} className={`${clickRestrictionsClasses} ${[...viewerMarginClasses.values()].join(' ')} w-full h-full`}
current={current} current={current}
isComponentsOpen={isComponentsOpen} isComponentsOpen={isComponentsOpen}
isSymbolsOpen={isSymbolsOpen} isSymbolsOpen={isSymbolsOpen}
@ -295,7 +269,7 @@ export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
margin={marginSidebar} margin={marginSidebar}
/> />
<Sidebar <Sidebar
className={`right-sidebar ${isRightSidebarOpenClasses}`} className={`right-sidebar ${isRightSidebarOpenClasses} ${clickRestrictionsClasses}`}
title={rightSidebarTitle} title={rightSidebarTitle}
> >
{ rightChildren } { rightChildren }

View file

@ -1,43 +1,48 @@
import * as React from 'react'; import * as React from 'react';
import { type IContainerModel } from '../../Interfaces/IContainerModel'; import { type IContainerModel } from '../../Interfaces/IContainerModel';
import { type IHistoryState } from '../../Interfaces/IHistoryState'; import { type IHistoryState } from '../../Interfaces/IHistoryState';
import { type IPoint } from '../../Interfaces/IPoint'; import { USE_EXPERIMENTAL_CANVAS_API } from '../../utils/default';
import { DIMENSION_MARGIN, USE_EXPERIMENTAL_CANVAS_API } from '../../utils/default'; import { FindContainerById } from '../../utils/itertools';
import { FindContainerById, MakeRecursionDFSIterator } from '../../utils/itertools';
import { BAR_WIDTH } from '../Bar/Bar'; import { BAR_WIDTH } from '../Bar/Bar';
import { Canvas } from '../Canvas/Canvas'; import { Canvas } from '../Canvas/Canvas';
import { AddDimensions } from '../Canvas/DimensionLayer'; import { SelectorMode, SVG } from '../SVG/SVG';
import { RenderSelector } from '../Canvas/Selector';
import { SVG } from '../SVG/SVG';
import { RenderSymbol } from '../Canvas/Symbol';
import { useState } from 'react'; import { useState } from 'react';
import { type ISymbolModel } from '../../Interfaces/ISymbolModel'; import { type ISymbolModel } from '../../Interfaces/ISymbolModel';
interface IViewerProps { interface IViewerProps {
className?: string className: string
current: IHistoryState current: IHistoryState
selectedContainer: IContainerModel | undefined selectedContainer: IContainerModel | undefined
selectContainer: (containerId: string) => void
selectedSymbol: ISymbolModel | undefined selectedSymbol: ISymbolModel | undefined
margin: number margin: number
isComponentsOpen: boolean isComponentsOpen: boolean
isSymbolsOpen: boolean isSymbolsOpen: boolean
selectContainer: (containerId: string) => void
}
export interface DrawParams {
mainContainer: IContainerModel
selectorMode: SelectorMode
selectedContainer: IContainerModel | undefined
selectedSymbol: ISymbolModel | undefined
containers: Map<string, IContainerModel>
symbols: Map<string, ISymbolModel>
}
function computeWidth(margin: number): number {
return window.innerWidth - (window.innerWidth < 768 ? BAR_WIDTH : margin);
} }
export function Viewer({ export function Viewer({
className, className,
current, current,
selectedContainer, selectedContainer,
selectContainer,
selectedSymbol, selectedSymbol,
margin, margin,
isComponentsOpen, isComponentsOpen,
isSymbolsOpen isSymbolsOpen,
selectContainer
}: IViewerProps): JSX.Element { }: IViewerProps): JSX.Element {
function computeWidth(margin: number): number {
return window.innerWidth - (window.innerWidth < 768 ? BAR_WIDTH : margin);
}
const [windowSize, setWindowSize] = useState([ const [windowSize, setWindowSize] = useState([
computeWidth(margin), computeWidth(margin),
window.innerHeight window.innerHeight
@ -71,64 +76,29 @@ export function Viewer({
return <></>; return <></>;
} }
let selectorMode = SelectorMode.Nothing;
if (isComponentsOpen) {
selectorMode = SelectorMode.Containers;
} else if (isSymbolsOpen) {
selectorMode = SelectorMode.Symbols;
}
const drawParams: DrawParams = {
mainContainer,
selectorMode,
selectedContainer,
selectedSymbol,
containers: current.containers,
symbols: current.symbols
};
if (USE_EXPERIMENTAL_CANVAS_API) { if (USE_EXPERIMENTAL_CANVAS_API) {
function Draw(ctx: CanvasRenderingContext2D, frameCount: number, scale: number, translatePos: IPoint): 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';
const it = MakeRecursionDFSIterator(mainContainer, current.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
RenderContainers(ctx, container, x, y);
// Draw dimensions
RenderDimensions(
ctx,
leftDim,
bottomDim,
topDim,
rightDim,
depth,
scale,
current.containers,
container,
currentTransform
);
// Draw selector
RenderSelector(ctx, frameCount, {
containers: current.containers,
scale,
selected: selectedContainer
});
}
// Draw symbols
RenderSymbols(current, ctx, scale);
ctx.restore();
}
return ( return (
<Canvas <Canvas
draw={Draw} className={className}
className='ml-16'
width={window.innerWidth - BAR_WIDTH} width={window.innerWidth - BAR_WIDTH}
height={window.innerHeight} height={window.innerHeight}
drawParams={drawParams}
/> />
); );
} }
@ -140,59 +110,8 @@ export function Viewer({
viewerHeight={windowSize[1]} viewerHeight={windowSize[1]}
width={mainContainer.properties.width} width={mainContainer.properties.width}
height={mainContainer.properties.height} height={mainContainer.properties.height}
containers={current.containers} drawParams={drawParams}
selectedContainer={selectedContainer}
symbols={current.symbols}
selectedSymbol={selectedSymbol}
selectContainer={selectContainer} selectContainer={selectContainer}
isComponentsOpen={isComponentsOpen} />
isSymbolsOpen={isSymbolsOpen}
>
{mainContainer}
</SVG>
); );
} }
function RenderSymbols(
current: IHistoryState,
ctx: CanvasRenderingContext2D,
scale: number
): void {
current.symbols.forEach((symbol) => {
RenderSymbol(symbol, ctx, scale);
});
}
function RenderDimensions(
ctx: CanvasRenderingContext2D,
leftDim: number,
bottomDim: number,
topDim: number,
rightDim: number,
depth: number,
scale: number,
containers: Map<string, IContainerModel>,
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];
AddDimensions(ctx, containers, container, dimMapped, currentTransform, scale, depth);
ctx.restore();
}
function RenderContainers(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();
}

View file

@ -27,6 +27,6 @@ export enum PropertyType {
SelfMarginDimension, SelfMarginDimension,
ChildrenDimensions, ChildrenDimensions,
DimensionWithMarks, DimensionWithMarks,
DimensionOptions
DimensionOptions
} }

View file

@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { AddMethod } from '../Enums/AddMethod'; import { type AddMethod } from '../Enums/AddMethod';
import { PositionReference } from '../Enums/PositionReference'; import { type PositionReference } from '../Enums/PositionReference';
import { IAction } from './IAction'; import { type IAction } from './IAction';
import { IMargin } from './IMargin'; import { type IMargin } from './IMargin';
import { Orientation } from '../Enums/Orientation'; import { type Orientation } from '../Enums/Orientation';
import { IKeyValue } from './IKeyValue'; import { type IKeyValue } from './IKeyValue';
import { IStyle } from './IStyle'; import { type IStyle } from './IStyle';
import { IDimensions } from './IDimensions'; import { type IDimensions } from './IDimensions';
/** Model of available container used in application configuration */ /** Model of available container used in application configuration */
export interface IAvailableContainer { export interface IAvailableContainer {

View file

@ -1,10 +1,17 @@
import { Position } from '../Enums/Position'; import { type Position } from '../Enums/Position';
export interface IDimensionOptions { export interface IDimensionOptions {
positions: Position[] positions: Position[]
/** /**
* Stroke color * Stroke color
*/ */
color: string color?: string
/** stroke-width */
width?: number
/** stroke-dasharray */
dashArray?: string
} }

View file

@ -0,0 +1,5 @@
export interface IReplaceContainer {
id: string | undefined
isReplacing: boolean
category: string | undefined
}

View file

@ -22,6 +22,7 @@
"@DeleteContainerTitle": "Delete the container", "@DeleteContainerTitle": "Delete the container",
"@DeleteSymbol": "Delete", "@DeleteSymbol": "Delete",
"@DeleteSymbolTitle": "Delete the container", "@DeleteSymbolTitle": "Delete the container",
"@ReplaceByContainer": "Replace by component",
"@ExportAsJSON": "Export as JSON", "@ExportAsJSON": "Export as JSON",
"@ExportAsSVG": "Export as SVG", "@ExportAsSVG": "Export as SVG",
@ -59,11 +60,14 @@
"@ContainerAlignmentInput": "Alignment", "@ContainerAlignmentInput": "Alignment",
"@ContainerAlignWithSymbol": "Align to symbol", "@ContainerAlignWithSymbol": "Align to symbol",
"@ContainerDimensions": "Dimensions", "@ContainerDimensions": "Dimensions",
"@ContainerShowDimension": "Show Dimension", "@ContainerShowDimension": "Show dimensions",
"@ContainerShowChildrenDimension": "Show surrounding dimension of children", "@ContainerShowChildrenDimension": "Show surrounding dimensions of children",
"@ContainerMarkPosition": "Mark the position for the parents", "@ContainerMarkPosition": "Mark the position for the parents",
"@ContainerShowDimensionWithMarks": "Show dimension with marked children", "@ContainerShowDimensionWithMarks": "Show dimensions with marked children",
"@ContainerShowMarginsDimension": "Show margins dimensions",
"@ContainerStyle": "Style", "@ContainerStyle": "Style",
"@StyleStrokeColor": "Stroke Color",
"@StyleStrokeDashArray": "Stroke Dash Array",
"@StyleStroke": "Stroke", "@StyleStroke": "Stroke",
"@StyleStrokeOpacity": "Stroke Opacity", "@StyleStrokeOpacity": "Stroke Opacity",
"@StyleStrokeWidth": "Stroke Width", "@StyleStrokeWidth": "Stroke Width",

View file

@ -9,7 +9,7 @@
"@Symbols": "Symboles", "@Symbols": "Symboles",
"@SymbolsLeft": "Symboles", "@SymbolsLeft": "Symboles",
"@SymbolsRight": "Symboles", "@SymbolsRight": "Symboles",
"@NoSymbolSelected": "Pas de symbol sélectionné", "@NoSymbolSelected": "Pas de symbole sélectionné",
"@Timeline": "Chronologie", "@Timeline": "Chronologie",
"@Messages": "Messages", "@Messages": "Messages",
"@Settings": "Paramètres", "@Settings": "Paramètres",
@ -22,6 +22,7 @@
"@DeleteContainerTitle": "Supprimer le conteneur", "@DeleteContainerTitle": "Supprimer le conteneur",
"@DeleteSymbol": "Supprimer", "@DeleteSymbol": "Supprimer",
"@DeleteSymbolTitle": "Supprimer le symbole", "@DeleteSymbolTitle": "Supprimer le symbole",
"@ReplaceByContainer": "Replacer par un composant",
"@ExportAsJSON": "Exporter en JSON", "@ExportAsJSON": "Exporter en JSON",
"@ExportAsSVG": "Exporter en SVG", "@ExportAsSVG": "Exporter en SVG",
@ -63,7 +64,10 @@
"@ContainerShowChildrenDimension": "Afficher les cotations englobante des enfants", "@ContainerShowChildrenDimension": "Afficher les cotations englobante des enfants",
"@ContainerMarkPosition": "Marquer la position pour les parents", "@ContainerMarkPosition": "Marquer la position pour les parents",
"@ContainerShowDimensionWithMarks": "Afficher les cotations avec les enfants marqués", "@ContainerShowDimensionWithMarks": "Afficher les cotations avec les enfants marqués",
"@ContainerShowMarginsDimension": "Afficher les cotations des marges",
"@ContainerStyle": "Style", "@ContainerStyle": "Style",
"@StyleStrokeColor": "Couleur du tracé",
"@StyleStrokeDashArray": "Tableau de traits",
"@StyleStroke": "Tracé", "@StyleStroke": "Tracé",
"@StyleStrokeOpacity": "Opacité du tracé", "@StyleStrokeOpacity": "Opacité du tracé",
"@StyleStrokeWidth": "Epaisseur du tracé", "@StyleStrokeWidth": "Epaisseur du tracé",

View file

@ -2,12 +2,14 @@
"@StartFromScratch": "Partir de zéro", "@StartFromScratch": "Partir de zéro",
"@LoadConfigFile": "Charger un fichier de configuration", "@LoadConfigFile": "Charger un fichier de configuration",
"@GoBack": "Revenir", "@GoBack": "Revenir",
"@Properties": "Propriétés",
"@Components": "Composants", "@Components": "Composants",
"@Elements": "Éléments", "@Elements": "Éléments",
"@Symbols": "Ancrages", "@Symbols": "Ancrages",
"@SymbolsLeft": "Ancrages", "@SymbolsLeft": "Ancrages",
"@SymbolsRight": "Ancrages", "@SymbolsRight": "Ancrages",
"@NoSymbolSelected": "Pas d'ancre sélectionnée",
"@Timeline": "Chronologie", "@Timeline": "Chronologie",
"@Messages": "Messages", "@Messages": "Messages",
"@Settings": "Paramètres", "@Settings": "Paramètres",
@ -61,9 +63,18 @@
"@ContainerShowChildrenDimension": "Afficher les cotations englobante des enfants", "@ContainerShowChildrenDimension": "Afficher les cotations englobante des enfants",
"@ContainerMarkPosition": "Marquer la position pour les parents", "@ContainerMarkPosition": "Marquer la position pour les parents",
"@ContainerShowDimensionWithMarks": "Afficher les cotations avec les enfants marqués", "@ContainerShowDimensionWithMarks": "Afficher les cotations avec les enfants marqués",
"@ContainerShowMarginsDimension": "Afficher les cotations des jeux",
"@ContainerStyle": "Style", "@ContainerStyle": "Style",
"@StyleStrokeColor": "Couleur du tracé",
"@StyleStrokeDashArray": "Tableau de traits",
"@StyleStroke": "Tracé",
"@StyleStrokeOpacity": "Opacité du tracé",
"@StyleStrokeWidth": "Epaisseur du tracé",
"@StyleFill": "Remplissage",
"@StyleFillOpacity": "Opacité du remplissage",
"@SymbolName": "Nom", "@SymbolName": "Nom",
"@SymbolDisplayedText": "Texte affiché",
"@SymbolX": "x", "@SymbolX": "x",
"@SymbolHeight": "Hauteur", "@SymbolHeight": "Hauteur",
"@SymbolWidth": "Largeur" "@SymbolWidth": "Largeur"

View file

@ -1,13 +1,14 @@
import { PositionReference } from '../Enums/PositionReference'; import { PositionReference } from '../Enums/PositionReference';
import { IAvailableContainer } from '../Interfaces/IAvailableContainer'; import { type IAvailableContainer } from '../Interfaces/IAvailableContainer';
import { IAvailableSymbol } from '../Interfaces/IAvailableSymbol'; import { type IAvailableSymbol } from '../Interfaces/IAvailableSymbol';
import { IConfiguration } from '../Interfaces/IConfiguration'; import { type IConfiguration } from '../Interfaces/IConfiguration';
import { ContainerModel, IContainerModel } from '../Interfaces/IContainerModel'; import { ContainerModel, type IContainerModel } from '../Interfaces/IContainerModel';
import { IContainerProperties } from '../Interfaces/IContainerProperties'; import { type IContainerProperties } from '../Interfaces/IContainerProperties';
import { IEditorState } from '../Interfaces/IEditorState'; import { type IEditorState } from '../Interfaces/IEditorState';
import { ISymbolModel } from '../Interfaces/ISymbolModel'; import { type ISymbolModel } from '../Interfaces/ISymbolModel';
import { Orientation } from '../Enums/Orientation'; import { Orientation } from '../Enums/Orientation';
import { AppState } from '../Enums/AppState'; import { AppState } from '../Enums/AppState';
import { type IDimensionOptions } from '../Interfaces/IDimensionOptions';
/// EDITOR DEFAULTS /// /// EDITOR DEFAULTS ///
@ -65,7 +66,6 @@ export const SHOW_SELF_DIMENSIONS = true;
export const SHOW_SELF_MARGINS_DIMENSIONS = true; export const SHOW_SELF_MARGINS_DIMENSIONS = true;
export const SHOW_CHILDREN_DIMENSIONS = true; export const SHOW_CHILDREN_DIMENSIONS = true;
export const SHOW_BORROWER_DIMENSIONS = true; export const SHOW_BORROWER_DIMENSIONS = true;
export const SHOW_DIMENSIONS_PER_DEPTH = false;
export const DIMENSION_MARGIN = 50; export const DIMENSION_MARGIN = 50;
export const SYMBOL_MARGIN = 25; export const SYMBOL_MARGIN = 25;
export const NOTCHES_LENGTH = 10; export const NOTCHES_LENGTH = 10;
@ -188,6 +188,12 @@ const DEFAULT_CONTAINER_STYLE = {
strokeWidth: 2 strokeWidth: 2
}; };
export const DEFAULT_DIMENSION_OPTION: IDimensionOptions = {
positions: [],
color: '#000000',
width: 2
};
/** /**
* Default Main container properties * Default Main container properties
*/ */
@ -212,23 +218,11 @@ export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = {
positionReference: PositionReference.TopLeft, positionReference: PositionReference.TopLeft,
hideChildrenInTreeview: false, hideChildrenInTreeview: false,
dimensionOptions: { dimensionOptions: {
childrenDimensions: { childrenDimensions: clone(DEFAULT_DIMENSION_OPTION),
color: '#000000', selfDimensions: clone(DEFAULT_DIMENSION_OPTION),
positions: [] selfMarginsDimensions: clone(DEFAULT_DIMENSION_OPTION),
},
selfDimensions: {
color: '#000000',
positions: []
},
selfMarginsDimensions: {
color: '#000000',
positions: []
},
markPosition: [], markPosition: [],
dimensionWithMarks: { dimensionWithMarks: clone(DEFAULT_DIMENSION_OPTION)
color: '#000000',
positions: []
}
}, },
warning: '', warning: '',
style: DEFAULT_CONTAINER_STYLE style: DEFAULT_CONTAINER_STYLE
@ -277,20 +271,20 @@ export function GetDefaultContainerProps(type: string,
hideChildrenInTreeview: containerConfig.HideChildrenInTreeview ?? false, hideChildrenInTreeview: containerConfig.HideChildrenInTreeview ?? false,
dimensionOptions: { dimensionOptions: {
childrenDimensions: { childrenDimensions: {
color: containerConfig.DimensionOptions?.childrenDimensions.color ?? '#000000', ...containerConfig.DimensionOptions?.selfDimensions,
positions: containerConfig.DimensionOptions?.childrenDimensions.positions ?? [] positions: containerConfig.DimensionOptions?.childrenDimensions.positions ?? []
}, },
selfDimensions: { selfDimensions: {
color: containerConfig.DimensionOptions?.selfDimensions.color ?? '#000000', ...containerConfig.DimensionOptions?.selfDimensions,
positions: containerConfig.DimensionOptions?.selfDimensions.positions ?? [] positions: containerConfig.DimensionOptions?.selfDimensions.positions ?? []
}, },
selfMarginsDimensions: { selfMarginsDimensions: {
color: containerConfig.DimensionOptions?.selfMarginsDimensions.color ?? '#000000', ...containerConfig.DimensionOptions?.selfMarginsDimensions,
positions: containerConfig.DimensionOptions?.selfMarginsDimensions.positions ?? [] positions: containerConfig.DimensionOptions?.selfMarginsDimensions.positions ?? []
}, },
markPosition: containerConfig.DimensionOptions?.markPosition ?? [], markPosition: containerConfig.DimensionOptions?.markPosition ?? [],
dimensionWithMarks: { dimensionWithMarks: {
color: containerConfig.DimensionOptions?.dimensionWithMarks.color ?? '#000000', ...containerConfig.DimensionOptions?.dimensionWithMarks,
positions: containerConfig.DimensionOptions?.dimensionWithMarks.positions ?? [] positions: containerConfig.DimensionOptions?.dimensionWithMarks.positions ?? []
} }
}, },
@ -319,3 +313,10 @@ export function GetDefaultSymbolModel(name: string,
showDimension: false showDimension: false
}; };
} }
/**
* Macro function for JSON.parse(JSON.stringify(obj))
*/
function clone<T>(object: T): T {
return JSON.parse(JSON.stringify(object));
}

View file

@ -84,8 +84,7 @@ const GetSVGLayoutConfiguration = () => {
stroke: 'red', stroke: 'red',
fill: '#d3c9b7', fill: '#d3c9b7',
}, },
ShowSelfDimensions: [0, 2], IsFlex: true,
ShowDimensionWithMarks: [1, 3],
Category: "Stuff" Category: "Stuff"
}, },
{ {
@ -100,8 +99,7 @@ const GetSVGLayoutConfiguration = () => {
stroke: 'red', stroke: 'red',
fill: '#d3c9b7', fill: '#d3c9b7',
}, },
ShowSelfDimensions: [0, 2], IsFlex: true,
ShowDimensionWithMarks: [1, 3],
Category: "Stuff" Category: "Stuff"
}, },
{ {
@ -121,6 +119,7 @@ const GetSVGLayoutConfiguration = () => {
fill: 'white' fill: 'white'
}, },
Category: "Stuff", Category: "Stuff",
IsFlex: true,
Actions: [ Actions: [
{ {
Id: "Insert", Id: "Insert",
@ -173,6 +172,7 @@ const GetSVGLayoutConfiguration = () => {
<line x1="{width}" y1="0" x2="0" y2="{height}" stroke="black" style='{userData.styleLine}'></line> <line x1="{width}" y1="0" x2="0" y2="{height}" stroke="black" style='{userData.styleLine}'></line>
` `
, ,
IsFlex: true,
Actions: [ Actions: [
{ {
Id: "SplitRemplissage", Id: "SplitRemplissage",
@ -254,6 +254,7 @@ const GetSVGLayoutConfiguration = () => {
Type: '200', Type: '200',
MaxWidth: 500, MaxWidth: 500,
MinWidth: 200, MinWidth: 200,
IsFlex: true,
Style: { Style: {
fillOpacity: 1, fillOpacity: 1,
strokeWidth: 2, strokeWidth: 2,
@ -265,6 +266,7 @@ const GetSVGLayoutConfiguration = () => {
Type: '400', Type: '400',
MaxWidth: 500, MaxWidth: 500,
MinWidth: 400, MinWidth: 400,
IsFlex: true,
Style: { Style: {
fillOpacity: 1, fillOpacity: 1,
strokeWidth: 2, strokeWidth: 2,
@ -300,7 +302,7 @@ const GetSVGLayoutConfiguration = () => {
Url: './images/arrow-down.svg' Url: './images/arrow-down.svg'
}, },
Name: 'Arrow', Name: 'Arrow',
PositionReference: 1 PositionReference: 0
}, },
{ {
Width: 32, Width: 32,
@ -329,8 +331,8 @@ const GetSVGLayoutConfiguration = () => {
MainContainer: { MainContainer: {
Type: 'main', Type: 'main',
Width: 800, Width: 800,
Height: 800, Height: 200,
Orientation: 1, Orientation: 0,
Style: { Style: {
stroke: 'black', stroke: 'black',
strokeWidth: 2, strokeWidth: 2,