Merged PR 162: Implement symbols and other stuff (see desc)
Implement symbols - Add, Remove, Select Container - Form - Link with container - Symbol behavior application to container (move to x with xpositionreference) Important changes - Remove SelectedContainer from HistoryState, meaning that it will be slower for each load but will be faster for each operations* (SetHistory, SelectContainer, DeleteContainer, SymbolOperations) - ElementsSidebar now opens with isSidebarOpen meaning that both sidebar will open on toggle - Moved camelize, transformX, restoreX to different modules (stringtools.ts, svg.ts)
This commit is contained in:
parent
58ef28fe89
commit
8b8d88f885
48 changed files with 1453 additions and 188 deletions
|
@ -21,7 +21,7 @@ steps:
|
||||||
path: $(pnpm_config_cache)
|
path: $(pnpm_config_cache)
|
||||||
displayName: Cache pnpm
|
displayName: Cache pnpm
|
||||||
|
|
||||||
- script: |
|
- bash: |
|
||||||
curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7
|
curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7
|
||||||
pnpm config set store-dir $(pnpm_config_cache)
|
pnpm config set store-dir $(pnpm_config_cache)
|
||||||
displayName: "Setup pnpm"
|
displayName: "Setup pnpm"
|
||||||
|
@ -31,7 +31,8 @@ steps:
|
||||||
versionSpec: '16.x'
|
versionSpec: '16.x'
|
||||||
displayName: 'Install Node.js 16.x LTS'
|
displayName: 'Install Node.js 16.x LTS'
|
||||||
|
|
||||||
- script: |
|
- bash: |
|
||||||
|
set -euo pipefail
|
||||||
node --version
|
node --version
|
||||||
node ./test-server/node-http.js &
|
node ./test-server/node-http.js &
|
||||||
jobs
|
jobs
|
||||||
|
@ -46,7 +47,8 @@ steps:
|
||||||
versionSpec: '>=18.7.0'
|
versionSpec: '>=18.7.0'
|
||||||
displayName: 'Install Node.js Latest'
|
displayName: 'Install Node.js Latest'
|
||||||
|
|
||||||
- script: |
|
- bash: |
|
||||||
|
set -euo pipefail
|
||||||
node --version
|
node --version
|
||||||
node ./test-server/node-http.js &
|
node ./test-server/node-http.js &
|
||||||
jobs
|
jobs
|
||||||
|
|
368
docs/Behaviors.md
Normal file
368
docs/Behaviors.md
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
# Container behaviors
|
||||||
|
|
||||||
|
This document is about the special and unique behaviors that a container can have.
|
||||||
|
|
||||||
|
Each behavior is documented into a section with 3 subsections :
|
||||||
|
- Rules
|
||||||
|
- Applications
|
||||||
|
- Code reference and algorithms
|
||||||
|
|
||||||
|
|
||||||
|
# Default behavior
|
||||||
|
|
||||||
|
The default behavior is the floating panel. It can move and resize itself but not its siblings.
|
||||||
|
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
The default behavior does not have any particular rules that applies to itself.
|
||||||
|
|
||||||
|
However it does have a common rule for any behavior that applies to its children.
|
||||||
|
|
||||||
|
Which is to apply the specials behaviors of its children (rigid or anchor).
|
||||||
|
|
||||||
|
|
||||||
|
## Applications
|
||||||
|
|
||||||
|
The default behavior is important to have a good user experience when adding object.
|
||||||
|
|
||||||
|
The golden rule is to never oppose the user which is why we don't want to applies rigid body by default
|
||||||
|
as it can block the addition of container.
|
||||||
|
Allowing freedom of movement can help for better precision if not the same as the rigid property.
|
||||||
|
|
||||||
|
An example would be trying to overlap an element in order to use it as a layer.
|
||||||
|
|
||||||
|
## Code references and algorithms
|
||||||
|
|
||||||
|
In the module `PropertiesOperations.ts` in the following functions:
|
||||||
|
- `OnPropertyChange()`
|
||||||
|
- `OnPropertiesSubmit()`
|
||||||
|
|
||||||
|
and in `ContainerOperation.ts` in `AddContainer()`,
|
||||||
|
|
||||||
|
it uses the `ApplyBehaviors` function of the `Behaviors.ts` module to apply the specials behaviors of its children.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Rigid body behavior
|
||||||
|
|
||||||
|
The rigid body behavior is a special behavior
|
||||||
|
that allows a container to be restricted into a space.
|
||||||
|
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
The main rules are :
|
||||||
|
- The rigid container must be kept inside its parent container
|
||||||
|
- The rigid container must be inside an unallocated space of its parent. Meaning, that it cannot overlap with another sibling.
|
||||||
|
|
||||||
|
|
||||||
|
## Applications
|
||||||
|
|
||||||
|
This behavior has many applications to it. Mainly about recalculations.
|
||||||
|
|
||||||
|
You may want to resize/move quickly and be certain that it does not overflow its parent.
|
||||||
|
|
||||||
|
You may want to resize its parent and makes its resize its children.
|
||||||
|
|
||||||
|
You may want siblings to interact with each other.
|
||||||
|
|
||||||
|
|
||||||
|
## Code references and algorithms
|
||||||
|
|
||||||
|
Its algorithm can be a little complicated due to the numerous uses cases.
|
||||||
|
|
||||||
|
|
||||||
|
### First rule
|
||||||
|
|
||||||
|
Lets start with the first rule : *The rigid container must be kept inside its parent container*
|
||||||
|
|
||||||
|
Inside the `RigidBodyBehaviors.ts`, see `constraintBodyInsideParent()` and `constraintBodyInsideSpace()`.
|
||||||
|
|
||||||
|
As you can see `constraintBodyInsideParent()` is just a wrapper for `constraintBodyInsideSpace()`, so lets just study the last function.
|
||||||
|
|
||||||
|
This is a simple problem of two rectangle.
|
||||||
|
|
||||||
|
In order to restrict the child to its parent,
|
||||||
|
we need to know firstly, if the children is not bigger than its parent.
|
||||||
|
|
||||||
|
If it is, we just need to set the child at the beginning and makes it takes the full size of its parent.
|
||||||
|
|
||||||
|
If it is not, we need to check if the children is out of bound (outside its parent). And if it is out of bound, we need to move it back inside.
|
||||||
|
|
||||||
|
To check if it bigger than its parent we just need to compare their sizes : `childWidth > parentWidth` and vertically `childHeight > parentHeight`.
|
||||||
|
|
||||||
|
If false we need to check out of bound, check for x (and y): `child.x < parent.x` for the left side or `child.x + child.width > parent.x + parent.width` for the right side. We don't want the overlap either which is why we uses `child.width`.
|
||||||
|
|
||||||
|
The condition is also equivalent to `child.x > parent.x + parent.width - child.width` which could makes more sense as the required space must be smaller because of the child size.
|
||||||
|
|
||||||
|
|
||||||
|
In my algorithm, I decided to put them near the edge where they went out of bound :
|
||||||
|
|
||||||
|
```
|
||||||
|
left oob: child.x = parent.x
|
||||||
|
right oob: child.x = parent.x + parent.width - child.width
|
||||||
|
```
|
||||||
|
|
||||||
|
Pseudo-code :
|
||||||
|
|
||||||
|
```c
|
||||||
|
constraintBodyInsideSpace(child, parent) {
|
||||||
|
if (child is bigger than parent) {
|
||||||
|
if (child is larger) {
|
||||||
|
set child x and width;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is taller) {
|
||||||
|
set child y and height;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (child is to the left of parent) {
|
||||||
|
set child x at the left side of parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is to the right of parent) {
|
||||||
|
set child x at the right side of parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is higher than parent) {
|
||||||
|
set child y at the top of parent
|
||||||
|
}
|
||||||
|
|
||||||
|
if (child is lower than parent) {
|
||||||
|
set child y at the bottom of parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Second rule
|
||||||
|
|
||||||
|
The second rule is the most important and complicated as it must interact with its siblings.
|
||||||
|
|
||||||
|
*The rigid container must be inside an unallocated space of its parent. Meaning, that it cannot overlap with another sibling.*
|
||||||
|
|
||||||
|
Let's first define what is a *space* : a *space* is the width of a container. Which consequently means that the rule only applies on the horizontal view. To simply the matter that also means that we only need to work on one dimension.
|
||||||
|
|
||||||
|
To solve this problem, like the parent, we could use collision detection between its siblings. However this could be very slow as the worst case scenario is a cartesian product: O(n2). Because for each container we need to search for other container that collide with use. When it collide we need to move it and search again.
|
||||||
|
|
||||||
|
Remember, this rule is applied every time you change a property of container, this is *lag*. We cannot afford to inefficient loops.
|
||||||
|
|
||||||
|
Let us use a "system of space" that has "containers" that cannot "overlap".
|
||||||
|
|
||||||
|
Memory.
|
||||||
|
|
||||||
|
Memory, RAM, Hard drive space, handles their space through a system of adresses and chunks of spaces (words, bytes...). In our case we don't have chunk of spaces but floating numbers (which can be a pain the work with because of the edge cases).
|
||||||
|
|
||||||
|
This system is particularly useful as it remember the space used after every iteration of allocation, meaning that we can know exactly when there is no more space inside parent and when a container must resized itself in order to fit inside.
|
||||||
|
|
||||||
|
Alright let us start the algorithm. See `constraintBodyInsideUnallocatedWidth()`, `getAvailableWidths()` and `getAvailableWidthsTwoLines()` in `RigidBodyBehaviors.ts` for implementation reference.
|
||||||
|
|
||||||
|
We have initially the whole space available: let `space` be this available space in the parent.
|
||||||
|
|
||||||
|
`space` is a pointer, thus at the beginning it has `0` to its pointer address and `parent.width` as it space.
|
||||||
|
|
||||||
|
To simplify the algorithm when adding a container, let us compare it to eating a bûche de Noël (yule log).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Like eating the cake, we need to cut it and take a part.
|
||||||
|
|
||||||
|
There is 5 possible way to cut it :
|
||||||
|
- Not eating the cake (maybe we prefer to eat a different cake/part)
|
||||||
|
- Eating the whole cake
|
||||||
|
- Cutting the cake at the left side
|
||||||
|
- Cutting the cake at the right right
|
||||||
|
- Cutting the cake in the middle
|
||||||
|
|
||||||
|
Not cutting the cake means returning the whole cake as is.
|
||||||
|
|
||||||
|
Eating the whole cake is to not returns anything.
|
||||||
|
|
||||||
|
Cutting at the left or the right side means leave 1 part.
|
||||||
|
|
||||||
|
Cutting the middle means leaving two part.
|
||||||
|
|
||||||
|
After cutting the cake, *while* there is still some left, we can continue the operation. (it is a *for* loop in the code though for syntax reasons)
|
||||||
|
|
||||||
|
However after serving for the siblings we *may* notice that there is no more left for us. We get angry, we *throw* a tantrum.
|
||||||
|
|
||||||
|
```c
|
||||||
|
// if you did not understood the joke
|
||||||
|
if (there no more cake) {
|
||||||
|
throw tantrum
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait, there is actually cake!
|
||||||
|
|
||||||
|
But it is left in multiple parts, we will just takes the *closest* one that *fits* our hunger.
|
||||||
|
|
||||||
|
If there is one that fits our hunger let's take it!
|
||||||
|
|
||||||
|
Yet! There is cake but none fits our hunger. But we do have a *minimum* acceptance, let us be humble, we will still take the small bit. By the way, taking multiple part would look bad for us. Nonetheless, if my *minimum* acceptance were to be higher than what is left, I would *throw* a *warning* for next time.
|
||||||
|
|
||||||
|
|
||||||
|
Alright lets translate this in pseudo-code.
|
||||||
|
|
||||||
|
Let us start with getting the available spaces :
|
||||||
|
|
||||||
|
```c#
|
||||||
|
getAvailableSpaces(parent, me) {
|
||||||
|
spaces = [{ x: 0, size: parent.width }]
|
||||||
|
|
||||||
|
let i = 0
|
||||||
|
while (spaces.length > 0 and i < parent.length) {
|
||||||
|
let sibling = parent.children[i];
|
||||||
|
|
||||||
|
if (sibling is me or is neither rigid nor is anchor) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let spacesLeft be an array
|
||||||
|
|
||||||
|
foreach(space in spaces) {
|
||||||
|
spacesLeftOfSpace = allocate(sibling, space);
|
||||||
|
spacesLeft.concat(spacesLeftOfSpace)
|
||||||
|
}
|
||||||
|
|
||||||
|
spaces = spacesLeft
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return spaces;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To allocate:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
allocate(sibling, space) {
|
||||||
|
if (sibling is not overlapping the space) {
|
||||||
|
return [space]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sibling overlap the space entirely) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sibling overlap at the left side) {
|
||||||
|
return [{
|
||||||
|
x: right side of sibling
|
||||||
|
size: right side of space - right side of sibling // "cut the left part"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sibling overlap at the right side) {
|
||||||
|
return [{
|
||||||
|
x: left side of space
|
||||||
|
size: leftSide of sibling - leftSide of space
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (sibling overlap in the middle)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
x: left side of space
|
||||||
|
size: left side of sibling - left side of space
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: right side of sibling
|
||||||
|
size: right side of space - right side of sibling
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally the top part:
|
||||||
|
|
||||||
|
```c#
|
||||||
|
constraintBodyInsideUnallocatedWidth(parent, container) {
|
||||||
|
spaces = getAvailableSpaces(parent, container)
|
||||||
|
if (there is no more spaces) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
spaces = sort spaces by closest from the middle of container
|
||||||
|
|
||||||
|
spaceFound = spaces.find(space that fit container.space)
|
||||||
|
|
||||||
|
if (no spaceFound) {
|
||||||
|
|
||||||
|
spaceFound = spaces.find(space that fit container.minimumSpace)
|
||||||
|
|
||||||
|
if (no spaceFound) {
|
||||||
|
show warning
|
||||||
|
return container
|
||||||
|
}
|
||||||
|
|
||||||
|
set container x and width to make it fit
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
constraintBodyInsideSpace(container, spaceFound)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This algorithm is great but, some problems remains:
|
||||||
|
- Finding the closest takes O(nlogn) with n being the number of spaces. This is usually not bad since the objective of the rigid body is to **fill** space. But it still does have a very bad worst case.
|
||||||
|
- There is 2 searches for space, same problem but the previous sort helps to make it faster for the best cases
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Anchor behavior
|
||||||
|
|
||||||
|
The anchor behavior allows a container to gain priority over its siblings.
|
||||||
|
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
It has the following rules:
|
||||||
|
- The container cannot be moved by other rigid siblings container
|
||||||
|
- The container cannot be resized by any other siblings container
|
||||||
|
- The container cannot overlap any other siblings rigid container :
|
||||||
|
- overlapping containers are shifted to the nearest available space/width
|
||||||
|
- or resized when there is no available space left other than theirs
|
||||||
|
- or lose their rigid body properties when there is absolutely no available space left (not even theirs)
|
||||||
|
|
||||||
|
|
||||||
|
## Applications
|
||||||
|
|
||||||
|
Gaining priority can helps makes sure that a rigid object won't move no matter what and will absolutly move no matter what is under it.
|
||||||
|
|
||||||
|
|
||||||
|
## Code references and algorithms
|
||||||
|
|
||||||
|
While there is a more rules applied to this behavior, most of them are just conditions.
|
||||||
|
|
||||||
|
These three rules:
|
||||||
|
- The container cannot be moved by other rigid siblings container
|
||||||
|
- The container cannot be resized by any other siblings container
|
||||||
|
- It cannot overlap any other siblings rigid container
|
||||||
|
|
||||||
|
Can be translate into a single one: "The container is an allocated space so any container is in contact will move or be resized"
|
||||||
|
|
||||||
|
Meaning that applying the rigid body properties of the sibling will also apply this rule. The difference with the default behavior and the anchor behavior is that the anchor container will be taken into account during the calculation of available space.
|
||||||
|
|
||||||
|
You can think of the default container as a floating panel and the anchor container as a wall. You can go under the floating panel but cannot go over the wall.
|
||||||
|
|
||||||
|
To optimize the algorithm, we just need to find the overlapping siblings since the anchor is not applied to those who are not in collision.
|
||||||
|
|
||||||
|
Pseudo-code :
|
||||||
|
|
||||||
|
```c#
|
||||||
|
ImposePosition(container) {
|
||||||
|
let rigidBodies be the rigid siblings that are not anchor
|
||||||
|
let overlappingRigidBodies be the overlapping rigid siblings of rigidBodies
|
||||||
|
|
||||||
|
foreach(overlappingRigidBody of overlappingRigidBodies) {
|
||||||
|
constraintBodyInsideUnallocatedWidth(overlappingRigidBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also, modify `getAvailableSpaces()` so it takes into account anchor containers.
|
||||||
|
|
||||||
|
That's it.
|
BIN
docs/assets/yule-log-cake.jpg
Normal file
BIN
docs/assets/yule-log-cake.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 183 KiB |
|
@ -4,22 +4,19 @@ onmessage = (e) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCircularReplacer = () => {
|
const getCircularReplacer = () => {
|
||||||
const seen = new WeakSet();
|
|
||||||
return (key, value) => {
|
return (key, value) => {
|
||||||
if (key === 'parent') {
|
if (key === 'parent') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'SelectedContainer') {
|
if (key === 'Symbols') {
|
||||||
return;
|
return Array.from(value.entries());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
if (key === 'linkedContainers') {
|
||||||
if (seen.has(value)) {
|
return Array.from(value);
|
||||||
return;
|
|
||||||
}
|
|
||||||
seen.add(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,9 +26,10 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
|
||||||
history: [{
|
history: [{
|
||||||
LastAction: '',
|
LastAction: '',
|
||||||
MainContainer: defaultMainContainer,
|
MainContainer: defaultMainContainer,
|
||||||
SelectedContainer: defaultMainContainer,
|
|
||||||
SelectedContainerId: defaultMainContainer.properties.id,
|
SelectedContainerId: defaultMainContainer.properties.id,
|
||||||
TypeCounters: {}
|
TypeCounters: {},
|
||||||
|
Symbols: new Map(),
|
||||||
|
SelectedSymbolId: ''
|
||||||
}],
|
}],
|
||||||
historyCurrentStep: 0
|
historyCurrentStep: 0
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { fetchConfiguration } from '../API/api';
|
import { fetchConfiguration } from '../API/api';
|
||||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
import { IEditorState } from '../../Interfaces/IEditorState';
|
||||||
import { LoadState } from './Load';
|
import { LoadState } from './Load';
|
||||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
|
||||||
import { DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
|
import { DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
|
||||||
|
|
||||||
export function NewEditor(
|
export function NewEditor(
|
||||||
|
@ -26,6 +25,7 @@ export function NewEditor(
|
||||||
|
|
||||||
// Save the configuration and the new MainContainer
|
// Save the configuration and the new MainContainer
|
||||||
// and default the selected container to it
|
// and default the selected container to it
|
||||||
|
// TODO: Put this in default.ts
|
||||||
const editorState: IEditorState = {
|
const editorState: IEditorState = {
|
||||||
configuration,
|
configuration,
|
||||||
history:
|
history:
|
||||||
|
@ -33,9 +33,10 @@ export function NewEditor(
|
||||||
{
|
{
|
||||||
LastAction: '',
|
LastAction: '',
|
||||||
MainContainer,
|
MainContainer,
|
||||||
SelectedContainer: MainContainer,
|
|
||||||
SelectedContainerId: MainContainer.properties.id,
|
SelectedContainerId: MainContainer.properties.id,
|
||||||
TypeCounters: {}
|
TypeCounters: {},
|
||||||
|
Symbols: new Map(),
|
||||||
|
SelectedSymbolId: ''
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
historyCurrentStep: 0
|
historyCurrentStep: 0
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { ClockIcon, CubeIcon, MapIcon } from '@heroicons/react/outline';
|
import { ClockIcon, CubeIcon, LinkIcon, MapIcon } from '@heroicons/react/outline';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { BarIcon } from './BarIcon';
|
import { BarIcon } from './BarIcon';
|
||||||
|
|
||||||
interface IBarProps {
|
interface IBarProps {
|
||||||
isSidebarOpen: boolean
|
isSidebarOpen: boolean
|
||||||
|
isSymbolsOpen: boolean
|
||||||
isElementsSidebarOpen: boolean
|
isElementsSidebarOpen: boolean
|
||||||
isHistoryOpen: boolean
|
isHistoryOpen: boolean
|
||||||
ToggleSidebar: () => void
|
ToggleSidebar: () => void
|
||||||
ToggleElementsSidebar: () => void
|
ToggleSymbols: () => void
|
||||||
ToggleTimeline: () => void
|
ToggleTimeline: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,10 +24,10 @@ export const Bar: React.FC<IBarProps> = (props) => {
|
||||||
<CubeIcon className='heroicon'/>
|
<CubeIcon className='heroicon'/>
|
||||||
</BarIcon>
|
</BarIcon>
|
||||||
<BarIcon
|
<BarIcon
|
||||||
isActive={props.isElementsSidebarOpen}
|
isActive={props.isSymbolsOpen}
|
||||||
title='Map'
|
title='Symbols'
|
||||||
onClick={() => props.ToggleElementsSidebar()}>
|
onClick={() => props.ToggleSymbols()}>
|
||||||
<MapIcon className='heroicon'/>
|
<LinkIcon className='heroicon'/>
|
||||||
</BarIcon>
|
</BarIcon>
|
||||||
<BarIcon
|
<BarIcon
|
||||||
isActive={props.isHistoryOpen}
|
isActive={props.isHistoryOpen}
|
||||||
|
|
|
@ -21,7 +21,7 @@ import { constraintBodyInsideUnallocatedWidth } from './RigidBodyBehaviors';
|
||||||
* Apply the following modification to the overlapping rigid body container :
|
* Apply the following modification to the overlapping rigid body container :
|
||||||
* @param container Container to impose its position
|
* @param container Container to impose its position
|
||||||
*/
|
*/
|
||||||
export function ImposePosition(container: IContainerModel): IContainerModel {
|
export function ApplyAnchor(container: IContainerModel): IContainerModel {
|
||||||
if (container.parent === undefined ||
|
if (container.parent === undefined ||
|
||||||
container.parent === null) {
|
container.parent === null) {
|
||||||
return container;
|
return container;
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { ImposePosition } from './AnchorBehaviors';
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
import { RecalculatePhysics } from './RigidBodyBehaviors';
|
import { APPLY_BEHAVIORS_ON_CHILDREN } from '../../../utils/default';
|
||||||
|
import { ApplyAnchor } from './AnchorBehaviors';
|
||||||
|
import { ApplyRigidBody } from './RigidBodyBehaviors';
|
||||||
|
import { ApplySymbol } from './SymbolBehaviors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recalculate the position of the container and its neighbors
|
* Recalculate the position of the container and its neighbors
|
||||||
|
@ -8,13 +11,25 @@ import { RecalculatePhysics } from './RigidBodyBehaviors';
|
||||||
* @param container Container to recalculate its positions
|
* @param container Container to recalculate its positions
|
||||||
* @returns Updated container
|
* @returns Updated container
|
||||||
*/
|
*/
|
||||||
export function ApplyBehaviors(container: IContainerModel): IContainerModel {
|
export function ApplyBehaviors(container: IContainerModel, symbols: Map<string, ISymbolModel>): IContainerModel {
|
||||||
if (container.properties.isAnchor) {
|
if (container.properties.isAnchor) {
|
||||||
ImposePosition(container);
|
ApplyAnchor(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (container.properties.isRigidBody) {
|
if (container.properties.isRigidBody) {
|
||||||
RecalculatePhysics(container);
|
ApplyRigidBody(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
const symbol = symbols.get(container.properties.linkedSymbolId);
|
||||||
|
if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
|
||||||
|
ApplySymbol(container, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (APPLY_BEHAVIORS_ON_CHILDREN) {
|
||||||
|
// Apply DFS by recursion
|
||||||
|
for (const child of container.children) {
|
||||||
|
ApplyBehaviors(child, symbols);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { ISizePointer } from '../../../Interfaces/ISizePointer';
|
||||||
* @param container Container to apply its rigid body properties
|
* @param container Container to apply its rigid body properties
|
||||||
* @returns A rigid body container
|
* @returns A rigid body container
|
||||||
*/
|
*/
|
||||||
export function RecalculatePhysics(
|
export function ApplyRigidBody(
|
||||||
container: IContainerModel
|
container: IContainerModel
|
||||||
): IContainerModel {
|
): IContainerModel {
|
||||||
container = constraintBodyInsideParent(container);
|
container = constraintBodyInsideParent(container);
|
||||||
|
@ -231,14 +231,15 @@ function getAvailableWidths(
|
||||||
const width = container.properties.width;
|
const width = container.properties.width;
|
||||||
let unallocatedSpaces: ISizePointer[] = [{ x, width }];
|
let unallocatedSpaces: ISizePointer[] = [{ x, width }];
|
||||||
|
|
||||||
// We will only uses containers that also are rigid or are anchors
|
for (const child of container.children) {
|
||||||
const solidBodies = container.children.filter(
|
if (unallocatedSpaces.length < 1) {
|
||||||
(child) => child.properties.isRigidBody || child.properties.isAnchor
|
return unallocatedSpaces;
|
||||||
);
|
}
|
||||||
|
|
||||||
for (const child of solidBodies) {
|
|
||||||
// Ignore the exception
|
// Ignore the exception
|
||||||
if (child === exception) {
|
// And we will also only uses containers that also are rigid or are anchors
|
||||||
|
if (child === exception ||
|
||||||
|
(!child.properties.isRigidBody && !child.properties.isAnchor)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const childX = child.properties.x;
|
const childX = child.properties.x;
|
||||||
|
|
9
src/Components/Editor/Behaviors/SymbolBehaviors.ts
Normal file
9
src/Components/Editor/Behaviors/SymbolBehaviors.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
|
import { restoreX, transformX } from '../../../utils/svg';
|
||||||
|
|
||||||
|
export function ApplySymbol(container: IContainerModel, symbol: ISymbolModel): IContainerModel {
|
||||||
|
container.properties.x = transformX(symbol.x, symbol.width, symbol.config.XPositionReference);
|
||||||
|
container.properties.x = restoreX(container.properties.x, container.properties.width, container.properties.XPositionReference);
|
||||||
|
return container;
|
||||||
|
}
|
|
@ -2,19 +2,20 @@ import { Dispatch, SetStateAction } from 'react';
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
||||||
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
|
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { findContainerById } from '../../utils/itertools';
|
import { findContainerById, MakeIterator } from '../../utils/itertools';
|
||||||
import { getCurrentHistory } from './Editor';
|
import { getCurrentHistory, UpdateCounters } from './Editor';
|
||||||
import { AddMethod } from '../../Enums/AddMethod';
|
import { AddMethod } from '../../Enums/AddMethod';
|
||||||
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
||||||
import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../utils/default';
|
import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../utils/default';
|
||||||
import { ApplyBehaviors } from './Behaviors/Behaviors';
|
import { ApplyBehaviors } from './Behaviors/Behaviors';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select a container
|
* Select a container
|
||||||
* @param container Selected container
|
* @param container Selected container
|
||||||
*/
|
*/
|
||||||
export function SelectContainer(
|
export function SelectContainer(
|
||||||
container: ContainerModel,
|
containerId: string,
|
||||||
fullHistory: IHistoryState[],
|
fullHistory: IHistoryState[],
|
||||||
historyCurrentStep: number,
|
historyCurrentStep: number,
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
@ -23,19 +24,13 @@ export function SelectContainer(
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
const current = history[history.length - 1];
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
const mainContainerClone = structuredClone(current.MainContainer);
|
|
||||||
const selectedContainer = findContainerById(mainContainerClone, container.properties.id);
|
|
||||||
|
|
||||||
if (selectedContainer === undefined) {
|
|
||||||
throw new Error('[SelectContainer] Cannot find container among children of main container!');
|
|
||||||
}
|
|
||||||
|
|
||||||
history.push({
|
history.push({
|
||||||
LastAction: `Select ${selectedContainer.properties.id}`,
|
LastAction: `Select ${containerId}`,
|
||||||
MainContainer: mainContainerClone,
|
MainContainer: structuredClone(current.MainContainer),
|
||||||
SelectedContainer: selectedContainer,
|
SelectedContainerId: containerId,
|
||||||
SelectedContainerId: selectedContainer.properties.id,
|
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
Symbols: structuredClone(current.Symbols),
|
||||||
|
SelectedSymbolId: current.SelectedSymbolId
|
||||||
});
|
});
|
||||||
setHistory(history);
|
setHistory(history);
|
||||||
setHistoryCurrentStep(history.length - 1);
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
@ -76,6 +71,8 @@ export function DeleteContainer(
|
||||||
if (container === null || container === undefined) {
|
if (container === null || container === undefined) {
|
||||||
throw new Error('[DeleteContainer] Container model was not found among children of the main container!');
|
throw new Error('[DeleteContainer] Container model was not found among children of the main container!');
|
||||||
}
|
}
|
||||||
|
const newSymbols = structuredClone(current.Symbols)
|
||||||
|
UnlinkSymbol(newSymbols, container);
|
||||||
|
|
||||||
const index = container.parent.children.indexOf(container);
|
const index = container.parent.children.indexOf(container);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
|
@ -94,14 +91,26 @@ export function DeleteContainer(
|
||||||
history.push({
|
history.push({
|
||||||
LastAction: `Delete ${containerId}`,
|
LastAction: `Delete ${containerId}`,
|
||||||
MainContainer: mainContainerClone,
|
MainContainer: mainContainerClone,
|
||||||
SelectedContainer,
|
|
||||||
SelectedContainerId,
|
SelectedContainerId,
|
||||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||||
|
Symbols: newSymbols,
|
||||||
|
SelectedSymbolId: current.SelectedSymbolId
|
||||||
});
|
});
|
||||||
setHistory(history);
|
setHistory(history);
|
||||||
setHistoryCurrentStep(history.length - 1);
|
setHistoryCurrentStep(history.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UnlinkSymbol(symbols: Map<string, ISymbolModel>, container: IContainerModel): void {
|
||||||
|
const it = MakeIterator(container);
|
||||||
|
for (const child of it) {
|
||||||
|
const symbol = symbols.get(child.properties.linkedSymbolId);
|
||||||
|
if (symbol === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
symbol.linkedContainers.delete(child.properties.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a new container to a selected container
|
* Add a new container to a selected container
|
||||||
* @param type The type of container
|
* @param type The type of container
|
||||||
|
@ -114,21 +123,19 @@ export function DeleteContainer(
|
||||||
*/
|
*/
|
||||||
export function AddContainerToSelectedContainer(
|
export function AddContainerToSelectedContainer(
|
||||||
type: string,
|
type: string,
|
||||||
|
selected: IContainerModel | undefined,
|
||||||
configuration: IConfiguration,
|
configuration: IConfiguration,
|
||||||
fullHistory: IHistoryState[],
|
fullHistory: IHistoryState[],
|
||||||
historyCurrentStep: number,
|
historyCurrentStep: number,
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
): void {
|
): void {
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
if (selected === null ||
|
||||||
const current = history[history.length - 1];
|
selected === undefined) {
|
||||||
|
|
||||||
if (current.SelectedContainer === null ||
|
|
||||||
current.SelectedContainer === undefined) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = current.SelectedContainer;
|
const parent = selected;
|
||||||
AddContainer(
|
AddContainer(
|
||||||
parent.children.length,
|
parent.children.length,
|
||||||
type,
|
type,
|
||||||
|
@ -166,11 +173,6 @@ export function AddContainer(
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
const current = history[history.length - 1];
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
if (current.MainContainer === null ||
|
|
||||||
current.MainContainer === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the preset properties from the API
|
// Get the preset properties from the API
|
||||||
const containerConfig = configuration.AvailableContainers
|
const containerConfig = configuration.AvailableContainers
|
||||||
.find(option => option.Type === type);
|
.find(option => option.Type === type);
|
||||||
|
@ -220,7 +222,7 @@ export function AddContainer(
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
ApplyBehaviors(newContainer);
|
ApplyBehaviors(newContainer, current.Symbols);
|
||||||
|
|
||||||
// And push it the the parent children
|
// And push it the the parent children
|
||||||
if (index === parentClone.children.length) {
|
if (index === parentClone.children.length) {
|
||||||
|
@ -235,23 +237,15 @@ export function AddContainer(
|
||||||
history.push({
|
history.push({
|
||||||
LastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`,
|
LastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`,
|
||||||
MainContainer: clone,
|
MainContainer: clone,
|
||||||
SelectedContainer: parentClone,
|
|
||||||
SelectedContainerId: parentClone.properties.id,
|
SelectedContainerId: parentClone.properties.id,
|
||||||
TypeCounters: newCounters
|
TypeCounters: newCounters,
|
||||||
|
Symbols: structuredClone(current.Symbols),
|
||||||
|
SelectedSymbolId: current.SelectedSymbolId
|
||||||
});
|
});
|
||||||
setHistory(history);
|
setHistory(history);
|
||||||
setHistoryCurrentStep(history.length - 1);
|
setHistoryCurrentStep(history.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdateCounters(counters: Record<string, number>, type: string): void {
|
|
||||||
if (counters[type] === null ||
|
|
||||||
counters[type] === undefined) {
|
|
||||||
counters[type] = 0;
|
|
||||||
} else {
|
|
||||||
counters[type]++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function InitializeDefaultChild(
|
function InitializeDefaultChild(
|
||||||
configuration: IConfiguration,
|
configuration: IConfiguration,
|
||||||
containerConfig: IAvailableContainer,
|
containerConfig: IAvailableContainer,
|
||||||
|
|
|
@ -11,6 +11,8 @@ import { OnPropertyChange, OnPropertiesSubmit } from './PropertiesOperations';
|
||||||
import EditorEvents from '../../Events/EditorEvents';
|
import EditorEvents from '../../Events/EditorEvents';
|
||||||
import { IEditorState } from '../../Interfaces/IEditorState';
|
import { IEditorState } from '../../Interfaces/IEditorState';
|
||||||
import { MAX_HISTORY } from '../../utils/default';
|
import { MAX_HISTORY } from '../../utils/default';
|
||||||
|
import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './SymbolOperations';
|
||||||
|
import { findContainerById } from '../../utils/itertools';
|
||||||
|
|
||||||
interface IEditorProps {
|
interface IEditorProps {
|
||||||
configuration: IConfiguration
|
configuration: IConfiguration
|
||||||
|
@ -18,6 +20,15 @@ interface IEditorProps {
|
||||||
historyCurrentStep: number
|
historyCurrentStep: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function UpdateCounters(counters: Record<string, number>, type: string): void {
|
||||||
|
if (counters[type] === null ||
|
||||||
|
counters[type] === undefined) {
|
||||||
|
counters[type] = 0;
|
||||||
|
} else {
|
||||||
|
counters[type]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] =>
|
export const getCurrentHistory = (history: IHistoryState[], historyCurrentStep: number): IHistoryState[] =>
|
||||||
history.slice(
|
history.slice(
|
||||||
Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow)
|
Math.max(0, history.length - MAX_HISTORY), // change this to 0 for unlimited (not recommanded because of overflow)
|
||||||
|
@ -70,13 +81,16 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
|
|
||||||
const configuration = props.configuration;
|
const configuration = props.configuration;
|
||||||
const current = getCurrentHistoryState(history, historyCurrentStep);
|
const current = getCurrentHistoryState(history, historyCurrentStep);
|
||||||
|
const selected = findContainerById(current.MainContainer, current.SelectedContainerId);
|
||||||
return (
|
return (
|
||||||
<div ref={editorRef} className="Editor font-sans h-full">
|
<div ref={editorRef} className="Editor font-sans h-full">
|
||||||
<UI
|
<UI
|
||||||
|
SelectedContainer={selected}
|
||||||
current={current}
|
current={current}
|
||||||
history={history}
|
history={history}
|
||||||
historyCurrentStep={historyCurrentStep}
|
historyCurrentStep={historyCurrentStep}
|
||||||
AvailableContainers={configuration.AvailableContainers}
|
AvailableContainers={configuration.AvailableContainers}
|
||||||
|
AvailableSymbols={configuration.AvailableSymbols}
|
||||||
SelectContainer={(container) => SelectContainer(
|
SelectContainer={(container) => SelectContainer(
|
||||||
container,
|
container,
|
||||||
history,
|
history,
|
||||||
|
@ -93,6 +107,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
)}
|
)}
|
||||||
OnPropertyChange={(key, value, isStyle) => OnPropertyChange(
|
OnPropertyChange={(key, value, isStyle) => OnPropertyChange(
|
||||||
key, value, isStyle,
|
key, value, isStyle,
|
||||||
|
selected,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
|
@ -100,6 +115,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
)}
|
)}
|
||||||
OnPropertiesSubmit={(event) => OnPropertiesSubmit(
|
OnPropertiesSubmit={(event) => OnPropertiesSubmit(
|
||||||
event,
|
event,
|
||||||
|
selected,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
setHistory,
|
setHistory,
|
||||||
|
@ -107,6 +123,7 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
)}
|
)}
|
||||||
AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer(
|
AddContainerToSelectedContainer={(type) => AddContainerToSelectedContainer(
|
||||||
type,
|
type,
|
||||||
|
selected,
|
||||||
configuration,
|
configuration,
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
|
@ -123,6 +140,35 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
setHistory,
|
setHistory,
|
||||||
setHistoryCurrentStep
|
setHistoryCurrentStep
|
||||||
)}
|
)}
|
||||||
|
AddSymbol={(type) => AddSymbol(
|
||||||
|
type,
|
||||||
|
configuration,
|
||||||
|
history,
|
||||||
|
historyCurrentStep,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep
|
||||||
|
)}
|
||||||
|
OnSymbolPropertyChange={(key, value) => OnSymbolPropertyChange(
|
||||||
|
key, value,
|
||||||
|
history,
|
||||||
|
historyCurrentStep,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep
|
||||||
|
)}
|
||||||
|
SelectSymbol={(symbolId) => SelectSymbol(
|
||||||
|
symbolId,
|
||||||
|
history,
|
||||||
|
historyCurrentStep,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep
|
||||||
|
)}
|
||||||
|
DeleteSymbol={(symbolId) => DeleteSymbol(
|
||||||
|
symbolId,
|
||||||
|
history,
|
||||||
|
historyCurrentStep,
|
||||||
|
setHistory,
|
||||||
|
setHistoryCurrentStep
|
||||||
|
)}
|
||||||
SaveEditorAsJSON={() => SaveEditorAsJSON(
|
SaveEditorAsJSON={() => SaveEditorAsJSON(
|
||||||
history,
|
history,
|
||||||
historyCurrentStep,
|
historyCurrentStep,
|
||||||
|
@ -134,7 +180,8 @@ const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||||
<SVG
|
<SVG
|
||||||
width={current.MainContainer?.properties.width}
|
width={current.MainContainer?.properties.width}
|
||||||
height={current.MainContainer?.properties.height}
|
height={current.MainContainer?.properties.height}
|
||||||
selected={current.SelectedContainer}
|
selected={selected}
|
||||||
|
symbols={current.Symbols}
|
||||||
>
|
>
|
||||||
{ current.MainContainer }
|
{ current.MainContainer }
|
||||||
</SVG>
|
</SVG>
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerMode
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
import { findContainerById } from '../../utils/itertools';
|
import { findContainerById } from '../../utils/itertools';
|
||||||
import { getCurrentHistory } from './Editor';
|
import { getCurrentHistory } from './Editor';
|
||||||
import { restoreX } from '../SVG/Elements/Container';
|
|
||||||
import { ApplyBehaviors } from './Behaviors/Behaviors';
|
import { ApplyBehaviors } from './Behaviors/Behaviors';
|
||||||
|
import { restoreX } from '../../utils/svg';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handled the property change event in the properties form
|
* Handled the property change event in the properties form
|
||||||
|
@ -16,6 +17,7 @@ export function OnPropertyChange(
|
||||||
key: string,
|
key: string,
|
||||||
value: string | number | boolean,
|
value: string | number | boolean,
|
||||||
isStyle: boolean = false,
|
isStyle: boolean = false,
|
||||||
|
selected: IContainerModel | undefined,
|
||||||
fullHistory: IHistoryState[],
|
fullHistory: IHistoryState[],
|
||||||
historyCurrentStep: number,
|
historyCurrentStep: number,
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
@ -24,37 +26,66 @@ export function OnPropertyChange(
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
const current = history[history.length - 1];
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
if (current.SelectedContainer === null ||
|
if (selected === null ||
|
||||||
current.SelectedContainer === undefined) {
|
selected === undefined) {
|
||||||
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
|
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
||||||
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
|
const container: ContainerModel | undefined = findContainerById(mainContainerClone, selected.properties.id);
|
||||||
|
|
||||||
if (container === null || container === undefined) {
|
if (container === null || container === undefined) {
|
||||||
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const oldSymbolId = container.properties.linkedSymbolId;
|
||||||
|
|
||||||
if (isStyle) {
|
if (isStyle) {
|
||||||
(container.properties.style as any)[key] = value;
|
(container.properties.style as any)[key] = value;
|
||||||
} else {
|
} else {
|
||||||
(container.properties as any)[key] = value;
|
(container.properties as any)[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyBehaviors(container);
|
LinkSymbol(
|
||||||
|
container.properties.id,
|
||||||
|
oldSymbolId,
|
||||||
|
container.properties.linkedSymbolId,
|
||||||
|
current.Symbols
|
||||||
|
);
|
||||||
|
|
||||||
|
ApplyBehaviors(container, current.Symbols);
|
||||||
|
|
||||||
history.push({
|
history.push({
|
||||||
LastAction: `Change ${key} of ${container.properties.id}`,
|
LastAction: `Change ${key} of ${container.properties.id}`,
|
||||||
MainContainer: mainContainerClone,
|
MainContainer: mainContainerClone,
|
||||||
SelectedContainer: container,
|
|
||||||
SelectedContainerId: container.properties.id,
|
SelectedContainerId: container.properties.id,
|
||||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||||
|
Symbols: structuredClone(current.Symbols),
|
||||||
|
SelectedSymbolId: current.SelectedSymbolId
|
||||||
});
|
});
|
||||||
setHistory(history);
|
setHistory(history);
|
||||||
setHistoryCurrentStep(history.length - 1);
|
setHistoryCurrentStep(history.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LinkSymbol(
|
||||||
|
containerId: string,
|
||||||
|
oldSymbolId: string,
|
||||||
|
newSymbolId: string,
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
): void {
|
||||||
|
const oldSymbol = symbols.get(oldSymbolId);
|
||||||
|
const newSymbol = symbols.get(newSymbolId);
|
||||||
|
|
||||||
|
if (newSymbol === undefined) {
|
||||||
|
if (oldSymbol !== undefined) {
|
||||||
|
oldSymbol.linkedContainers.delete(containerId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSymbol.linkedContainers.add(containerId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handled the property change event in the properties form
|
* Handled the property change event in the properties form
|
||||||
* @param key Property name
|
* @param key Property name
|
||||||
|
@ -63,6 +94,7 @@ export function OnPropertyChange(
|
||||||
*/
|
*/
|
||||||
export function OnPropertiesSubmit(
|
export function OnPropertiesSubmit(
|
||||||
event: React.SyntheticEvent<HTMLFormElement>,
|
event: React.SyntheticEvent<HTMLFormElement>,
|
||||||
|
selected: IContainerModel | undefined,
|
||||||
fullHistory: IHistoryState[],
|
fullHistory: IHistoryState[],
|
||||||
historyCurrentStep: number,
|
historyCurrentStep: number,
|
||||||
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
@ -72,13 +104,13 @@ export function OnPropertiesSubmit(
|
||||||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
const current = history[history.length - 1];
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
if (current.SelectedContainer === null ||
|
if (selected === null ||
|
||||||
current.SelectedContainer === undefined) {
|
selected === undefined) {
|
||||||
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
|
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
||||||
const container: ContainerModel | undefined = findContainerById(mainContainerClone, current.SelectedContainer.properties.id);
|
const container: ContainerModel | undefined = findContainerById(mainContainerClone, selected.properties.id);
|
||||||
|
|
||||||
if (container === null || container === undefined) {
|
if (container === null || container === undefined) {
|
||||||
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
|
||||||
|
@ -110,14 +142,15 @@ export function OnPropertiesSubmit(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the behaviors
|
// Apply the behaviors
|
||||||
ApplyBehaviors(container);
|
ApplyBehaviors(container, current.Symbols);
|
||||||
|
|
||||||
history.push({
|
history.push({
|
||||||
LastAction: `Change properties of ${container.properties.id}`,
|
LastAction: `Change properties of ${container.properties.id}`,
|
||||||
MainContainer: mainContainerClone,
|
MainContainer: mainContainerClone,
|
||||||
SelectedContainer: container,
|
|
||||||
SelectedContainerId: container.properties.id,
|
SelectedContainerId: container.properties.id,
|
||||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||||
|
Symbols: structuredClone(current.Symbols),
|
||||||
|
SelectedSymbolId: current.SelectedSymbolId
|
||||||
});
|
});
|
||||||
setHistory(history);
|
setHistory(history);
|
||||||
setHistoryCurrentStep(history.length - 1);
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
@ -187,3 +220,4 @@ const submitRadioButtons = (
|
||||||
|
|
||||||
(container.properties as any)[property] = radiobutton.value;
|
(container.properties as any)[property] = radiobutton.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
180
src/Components/Editor/SymbolOperations.ts
Normal file
180
src/Components/Editor/SymbolOperations.ts
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
||||||
|
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { DEFAULT_SYMBOL_HEIGHT, DEFAULT_SYMBOL_WIDTH } from '../../utils/default';
|
||||||
|
import { findContainerById } from '../../utils/itertools';
|
||||||
|
import { restoreX } from '../../utils/svg';
|
||||||
|
import { ApplyBehaviors } from './Behaviors/Behaviors';
|
||||||
|
import { getCurrentHistory, UpdateCounters } from './Editor';
|
||||||
|
|
||||||
|
export function AddSymbol(
|
||||||
|
name: string,
|
||||||
|
configuration: IConfiguration,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
const symbolConfig = configuration.AvailableSymbols
|
||||||
|
.find(option => option.Name === name);
|
||||||
|
|
||||||
|
if (symbolConfig === undefined) {
|
||||||
|
throw new Error('[AddSymbol] Symbol could not be found in the config');
|
||||||
|
}
|
||||||
|
const type = `symbol-${name}`;
|
||||||
|
const newCounters = structuredClone(current.TypeCounters);
|
||||||
|
UpdateCounters(newCounters, type);
|
||||||
|
|
||||||
|
const newSymbols = structuredClone(current.Symbols);
|
||||||
|
// TODO: Put this in default.ts as GetDefaultConfig
|
||||||
|
const newSymbol: ISymbolModel = {
|
||||||
|
id: `${name}-${newCounters[type]}`,
|
||||||
|
type: name,
|
||||||
|
config: structuredClone(symbolConfig),
|
||||||
|
x: 0,
|
||||||
|
width: symbolConfig.Width ?? DEFAULT_SYMBOL_WIDTH,
|
||||||
|
height: symbolConfig.Height ?? DEFAULT_SYMBOL_HEIGHT,
|
||||||
|
linkedContainers: new Set()
|
||||||
|
};
|
||||||
|
newSymbol.x = restoreX(newSymbol.x, newSymbol.width, newSymbol.config.XPositionReference);
|
||||||
|
|
||||||
|
newSymbols.set(newSymbol.id, newSymbol);
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
LastAction: `Add ${name}`,
|
||||||
|
MainContainer: structuredClone(current.MainContainer),
|
||||||
|
SelectedContainerId: current.SelectedContainerId,
|
||||||
|
TypeCounters: newCounters,
|
||||||
|
Symbols: newSymbols,
|
||||||
|
SelectedSymbolId: newSymbol.id
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SelectSymbol(
|
||||||
|
symbolId: string,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
LastAction: `Select ${symbolId}`,
|
||||||
|
MainContainer: structuredClone(current.MainContainer),
|
||||||
|
SelectedContainerId: current.SelectedContainerId,
|
||||||
|
TypeCounters: structuredClone(current.TypeCounters),
|
||||||
|
Symbols: structuredClone(current.Symbols),
|
||||||
|
SelectedSymbolId: symbolId
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteSymbol(
|
||||||
|
symbolId: string,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
const newSymbols = structuredClone(current.Symbols);
|
||||||
|
const symbol = newSymbols.get(symbolId);
|
||||||
|
|
||||||
|
if (symbol === undefined) {
|
||||||
|
throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMainContainer = structuredClone(current.MainContainer);
|
||||||
|
|
||||||
|
UnlinkContainers(symbol, newMainContainer);
|
||||||
|
|
||||||
|
newSymbols.delete(symbolId);
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
LastAction: `Select ${symbolId}`,
|
||||||
|
MainContainer: newMainContainer,
|
||||||
|
SelectedContainerId: current.SelectedContainerId,
|
||||||
|
TypeCounters: structuredClone(current.TypeCounters),
|
||||||
|
Symbols: newSymbols,
|
||||||
|
SelectedSymbolId: symbolId
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnlinkContainers(symbol: ISymbolModel, newMainContainer: IContainerModel) {
|
||||||
|
symbol.linkedContainers.forEach((containerId) => {
|
||||||
|
const container = findContainerById(newMainContainer, containerId);
|
||||||
|
|
||||||
|
if (container === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.properties.linkedSymbolId = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handled the property change event in the properties form
|
||||||
|
* @param key Property name
|
||||||
|
* @param value New value of the property
|
||||||
|
* @returns void
|
||||||
|
*/
|
||||||
|
export function OnPropertyChange(
|
||||||
|
key: string,
|
||||||
|
value: string | number | boolean,
|
||||||
|
fullHistory: IHistoryState[],
|
||||||
|
historyCurrentStep: number,
|
||||||
|
setHistory: Dispatch<SetStateAction<IHistoryState[]>>,
|
||||||
|
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
|
||||||
|
): void {
|
||||||
|
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||||
|
const current = history[history.length - 1];
|
||||||
|
|
||||||
|
if (current.SelectedSymbolId === '') {
|
||||||
|
throw new Error('[OnSymbolPropertyChange] Property was changed before selecting a symbol');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSymbols: Map<string, ISymbolModel> = structuredClone(current.Symbols);
|
||||||
|
const symbol = newSymbols.get(current.SelectedSymbolId);
|
||||||
|
|
||||||
|
if (symbol === null || symbol === undefined) {
|
||||||
|
throw new Error('[OnSymbolPropertyChange] Symbol model was not found in state!');
|
||||||
|
}
|
||||||
|
|
||||||
|
(symbol as any)[key] = value;
|
||||||
|
|
||||||
|
const newMainContainer = structuredClone(current.MainContainer);
|
||||||
|
symbol.linkedContainers.forEach((containerId) => {
|
||||||
|
const container = findContainerById(newMainContainer, containerId);
|
||||||
|
|
||||||
|
if (container === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyBehaviors(container, newSymbols);
|
||||||
|
});
|
||||||
|
|
||||||
|
history.push({
|
||||||
|
LastAction: `Change ${key} of ${symbol.id}`,
|
||||||
|
MainContainer: newMainContainer,
|
||||||
|
SelectedContainerId: current.SelectedContainerId,
|
||||||
|
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||||
|
Symbols: newSymbols,
|
||||||
|
SelectedSymbolId: symbol.id
|
||||||
|
});
|
||||||
|
setHistory(history);
|
||||||
|
setHistoryCurrentStep(history.length - 1);
|
||||||
|
}
|
|
@ -4,16 +4,19 @@ import { fireEvent, render, screen } from '../../utils/test-utils';
|
||||||
import { ElementsSidebar } from './ElementsSidebar';
|
import { ElementsSidebar } from './ElementsSidebar';
|
||||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
|
import { findContainerById } from '../../utils/itertools';
|
||||||
|
|
||||||
describe.concurrent('Elements sidebar', () => {
|
describe.concurrent('Elements sidebar', () => {
|
||||||
it('With a MainContainer', () => {
|
it('With a MainContainer', () => {
|
||||||
render(<ElementsSidebar
|
render(<ElementsSidebar
|
||||||
|
symbols={new Map()}
|
||||||
MainContainer={{
|
MainContainer={{
|
||||||
children: [],
|
children: [],
|
||||||
parent: null,
|
parent: null,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: null,
|
parentId: null,
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -28,7 +31,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
}}
|
}}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
SelectedContainer={null}
|
SelectedContainer={undefined}
|
||||||
OnPropertyChange={() => {}}
|
OnPropertyChange={() => {}}
|
||||||
OnPropertiesSubmit={() => {}}
|
OnPropertiesSubmit={() => {}}
|
||||||
SelectContainer={() => {}}
|
SelectContainer={() => {}}
|
||||||
|
@ -42,12 +45,13 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('With a selected MainContainer', () => {
|
it('With a selected MainContainer', () => {
|
||||||
const MainContainer = {
|
const MainContainer: IContainerModel = {
|
||||||
children: [],
|
children: [],
|
||||||
parent: null,
|
parent: null,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: '',
|
parentId: '',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -62,6 +66,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<ElementsSidebar
|
const { container } = render(<ElementsSidebar
|
||||||
|
symbols={new Map()}
|
||||||
MainContainer={MainContainer}
|
MainContainer={MainContainer}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
|
@ -102,12 +107,13 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
|
|
||||||
it('With multiple containers', () => {
|
it('With multiple containers', () => {
|
||||||
const children: IContainerModel[] = [];
|
const children: IContainerModel[] = [];
|
||||||
const MainContainer = {
|
const MainContainer: IContainerModel = {
|
||||||
children,
|
children,
|
||||||
parent: null,
|
parent: null,
|
||||||
properties: {
|
properties: {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: '',
|
parentId: '',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -128,6 +134,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
properties: {
|
properties: {
|
||||||
id: 'child-1',
|
id: 'child-1',
|
||||||
parentId: 'main',
|
parentId: 'main',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'child-1',
|
displayedText: 'child-1',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -149,6 +156,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
properties: {
|
properties: {
|
||||||
id: 'child-2',
|
id: 'child-2',
|
||||||
parentId: 'main',
|
parentId: 'main',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'child-2',
|
displayedText: 'child-2',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -164,6 +172,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
render(<ElementsSidebar
|
render(<ElementsSidebar
|
||||||
|
symbols={new Map()}
|
||||||
MainContainer={MainContainer}
|
MainContainer={MainContainer}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
|
@ -190,6 +199,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
properties: {
|
properties: {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: '',
|
parentId: '',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -209,6 +219,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
properties: {
|
properties: {
|
||||||
id: 'child-1',
|
id: 'child-1',
|
||||||
parentId: 'main',
|
parentId: 'main',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'child-1',
|
displayedText: 'child-1',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -223,12 +234,13 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
};
|
};
|
||||||
children.push(child1Model);
|
children.push(child1Model);
|
||||||
|
|
||||||
let SelectedContainer = MainContainer;
|
let SelectedContainer: IContainerModel | undefined = MainContainer;
|
||||||
const selectContainer = vi.fn((container: IContainerModel) => {
|
const selectContainer = vi.fn((containerId: string) => {
|
||||||
SelectedContainer = container;
|
SelectedContainer = findContainerById(MainContainer, containerId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const { container, rerender } = render(<ElementsSidebar
|
const { container, rerender } = render(<ElementsSidebar
|
||||||
|
symbols={new Map()}
|
||||||
MainContainer={MainContainer}
|
MainContainer={MainContainer}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
|
@ -253,6 +265,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
fireEvent.click(child1);
|
fireEvent.click(child1);
|
||||||
|
|
||||||
rerender(<ElementsSidebar
|
rerender(<ElementsSidebar
|
||||||
|
symbols={new Map()}
|
||||||
MainContainer={MainContainer}
|
MainContainer={MainContainer}
|
||||||
isOpen={true}
|
isOpen={true}
|
||||||
isHistoryOpen={false}
|
isHistoryOpen={false}
|
||||||
|
|
|
@ -7,15 +7,17 @@ import { Menu } from '../Menu/Menu';
|
||||||
import { MenuItem } from '../Menu/MenuItem';
|
import { MenuItem } from '../Menu/MenuItem';
|
||||||
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers';
|
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers';
|
||||||
import { IPoint } from '../../Interfaces/IPoint';
|
import { IPoint } from '../../Interfaces/IPoint';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
|
||||||
interface IElementsSidebarProps {
|
interface IElementsSidebarProps {
|
||||||
MainContainer: IContainerModel
|
MainContainer: IContainerModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
isHistoryOpen: boolean
|
isHistoryOpen: boolean
|
||||||
SelectedContainer: IContainerModel | null
|
SelectedContainer: IContainerModel | undefined
|
||||||
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
SelectContainer: (container: IContainerModel) => void
|
SelectContainer: (containerId: string) => void
|
||||||
DeleteContainer: (containerid: string) => void
|
DeleteContainer: (containerid: string) => void
|
||||||
AddContainer: (index: number, type: string, parent: string) => void
|
AddContainer: (index: number, type: string, parent: string) => void
|
||||||
}
|
}
|
||||||
|
@ -104,7 +106,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
||||||
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
|
onDrop={(event) => handleOnDrop(event, props.MainContainer, props.AddContainer)}
|
||||||
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
|
onDragOver={(event) => handleDragOver(event, props.MainContainer)}
|
||||||
onDragLeave={(event) => handleDragLeave(event)}
|
onDragLeave={(event) => handleDragLeave(event)}
|
||||||
onClick={() => props.SelectContainer(container)}
|
onClick={() => props.SelectContainer(container.properties.id)}
|
||||||
>
|
>
|
||||||
{ text }
|
{ text }
|
||||||
</button>
|
</button>
|
||||||
|
@ -140,6 +142,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
||||||
</Menu>
|
</Menu>
|
||||||
<Properties
|
<Properties
|
||||||
properties={props.SelectedContainer?.properties}
|
properties={props.SelectedContainer?.properties}
|
||||||
|
symbols={props.symbols}
|
||||||
onChange={props.OnPropertyChange}
|
onChange={props.OnPropertyChange}
|
||||||
onSubmit={props.OnPropertiesSubmit}
|
onSubmit={props.OnPropertiesSubmit}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,18 +1,21 @@
|
||||||
import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline';
|
import { MenuAlt2Icon, MenuAlt3Icon, MenuIcon } from '@heroicons/react/outline';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { restoreX, transformX } from '../../utils/svg';
|
||||||
import { InputGroup } from '../InputGroup/InputGroup';
|
import { InputGroup } from '../InputGroup/InputGroup';
|
||||||
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
||||||
import { restoreX, transformX } from '../SVG/Elements/Container';
|
import { Select } from '../Select/Select';
|
||||||
|
|
||||||
interface IDynamicFormProps {
|
interface IDynamicFormProps {
|
||||||
properties: IProperties
|
properties: IContainerProperties
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCSSInputs = (
|
const getCSSInputs = (
|
||||||
properties: IProperties,
|
properties: IContainerProperties,
|
||||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
): JSX.Element[] => {
|
): JSX.Element[] => {
|
||||||
const groupInput: JSX.Element[] = [];
|
const groupInput: JSX.Element[] = [];
|
||||||
|
@ -67,6 +70,7 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
|
||||||
labelClassName=''
|
labelClassName=''
|
||||||
inputClassName=''
|
inputClassName=''
|
||||||
type='number'
|
type='number'
|
||||||
|
isDisabled={props.properties.linkedSymbolId !== ''}
|
||||||
value={transformX(props.properties.x, props.properties.width, props.properties.XPositionReference).toString()}
|
value={transformX(props.properties.x, props.properties.width, props.properties.XPositionReference).toString()}
|
||||||
onChange={(event) => props.onChange('x', restoreX(Number(event.target.value), props.properties.width, props.properties.XPositionReference))}
|
onChange={(event) => props.onChange('x', restoreX(Number(event.target.value), props.properties.width, props.properties.XPositionReference))}
|
||||||
/>
|
/>
|
||||||
|
@ -160,9 +164,22 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
|
||||||
]}
|
]}
|
||||||
onChange={(event) => props.onChange('XPositionReference', Number(event.target.value))}
|
onChange={(event) => props.onChange('XPositionReference', Number(event.target.value))}
|
||||||
/>
|
/>
|
||||||
|
<Select
|
||||||
|
inputKey='linkedSymbolId'
|
||||||
|
labelText='Align with symbol'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
inputs={[...props.symbols.values()].map(symbol => ({
|
||||||
|
text: symbol.id,
|
||||||
|
value: symbol.id
|
||||||
|
}))}
|
||||||
|
value={props.properties.linkedSymbolId ?? ''}
|
||||||
|
onChange={(event) => props.onChange('linkedSymbolId', event.target.value)}
|
||||||
|
/>
|
||||||
{ getCSSInputs(props.properties, props.onChange) }
|
{ getCSSInputs(props.properties, props.onChange) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default DynamicForm;
|
export default DynamicForm;
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
import DynamicForm from './DynamicForm';
|
import DynamicForm from './DynamicForm';
|
||||||
import StaticForm from './StaticForm';
|
import StaticForm from './StaticForm';
|
||||||
|
|
||||||
interface IFormProps {
|
interface IFormProps {
|
||||||
properties: IProperties
|
properties: IContainerProperties
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
isDynamicInput: boolean
|
isDynamicInput: boolean
|
||||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
|
@ -14,6 +16,7 @@ export const Form: React.FunctionComponent<IFormProps> = (props) => {
|
||||||
if (props.isDynamicInput) {
|
if (props.isDynamicInput) {
|
||||||
return <DynamicForm
|
return <DynamicForm
|
||||||
properties={props.properties}
|
properties={props.properties}
|
||||||
|
symbols={props.symbols}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { expect, describe, it, vi } from 'vitest';
|
import { expect, describe, it, vi } from 'vitest';
|
||||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||||
import { Properties } from './Properties';
|
import { Properties } from './Properties';
|
||||||
|
|
||||||
describe.concurrent('Properties', () => {
|
describe.concurrent('Properties', () => {
|
||||||
|
@ -11,6 +11,7 @@ describe.concurrent('Properties', () => {
|
||||||
properties={undefined}
|
properties={undefined}
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
|
symbols={new Map()}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.queryByText('id')).toBeNull();
|
expect(screen.queryByText('id')).toBeNull();
|
||||||
|
@ -20,9 +21,10 @@ describe.concurrent('Properties', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Some properties, change values with dynamic input', () => {
|
it('Some properties, change values with dynamic input', () => {
|
||||||
const prop: IProperties = {
|
const prop: IContainerProperties = {
|
||||||
id: 'stuff',
|
id: 'stuff',
|
||||||
parentId: 'parentId',
|
parentId: 'parentId',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'stuff',
|
displayedText: 'stuff',
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 1,
|
y: 1,
|
||||||
|
@ -42,6 +44,7 @@ describe.concurrent('Properties', () => {
|
||||||
properties={prop}
|
properties={prop}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
|
symbols={new Map()}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.queryByText('id')).toBeDefined();
|
expect(screen.queryByText('id')).toBeDefined();
|
||||||
|
@ -76,6 +79,7 @@ describe.concurrent('Properties', () => {
|
||||||
properties={Object.assign({}, prop)}
|
properties={Object.assign({}, prop)}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onSubmit={() => {}}
|
onSubmit={() => {}}
|
||||||
|
symbols={new Map()}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
propertyId = container.querySelector('#id');
|
propertyId = container.querySelector('#id');
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
||||||
import { Form } from './Form';
|
import { Form } from './Form';
|
||||||
|
|
||||||
interface IPropertiesProps {
|
interface IPropertiesProps {
|
||||||
properties?: IProperties
|
properties?: IContainerProperties
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
}
|
}
|
||||||
|
@ -27,6 +29,7 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
|
||||||
/>
|
/>
|
||||||
<Form
|
<Form
|
||||||
properties={props.properties}
|
properties={props.properties}
|
||||||
|
symbols={props.symbols}
|
||||||
isDynamicInput={isDynamicInput}
|
isDynamicInput={isDynamicInput}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
onSubmit={props.onSubmit}
|
onSubmit={props.onSubmit}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { MenuAlt2Icon, MenuIcon, MenuAlt3Icon } from '@heroicons/react/outline';
|
import { MenuAlt2Icon, MenuIcon, MenuAlt3Icon } from '@heroicons/react/outline';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
import IProperties from '../../Interfaces/IProperties';
|
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||||
|
import { transformX } from '../../utils/svg';
|
||||||
import { InputGroup } from '../InputGroup/InputGroup';
|
import { InputGroup } from '../InputGroup/InputGroup';
|
||||||
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
|
||||||
import { transformX } from '../SVG/Elements/Container';
|
|
||||||
|
|
||||||
interface IStaticFormProps {
|
interface IStaticFormProps {
|
||||||
properties: IProperties
|
properties: IContainerProperties
|
||||||
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCSSInputs = (properties: IProperties): JSX.Element[] => {
|
const getCSSInputs = (properties: IContainerProperties): JSX.Element[] => {
|
||||||
const groupInput: JSX.Element[] = [];
|
const groupInput: JSX.Element[] = [];
|
||||||
for (const key in properties.style) {
|
for (const key in properties.style) {
|
||||||
groupInput.push(<InputGroup
|
groupInput.push(<InputGroup
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Interweave, Node } from 'interweave';
|
import { Interweave, Node } from 'interweave';
|
||||||
import { XPositionReference } from '../../../Enums/XPositionReference';
|
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { DIMENSION_MARGIN, SHOW_CHILDREN_DIMENSIONS, SHOW_PARENT_DIMENSION, SHOW_TEXT } from '../../../utils/default';
|
import { DIMENSION_MARGIN, SHOW_CHILDREN_DIMENSIONS, SHOW_PARENT_DIMENSION, SHOW_TEXT } from '../../../utils/default';
|
||||||
import { getDepth } from '../../../utils/itertools';
|
import { getDepth } from '../../../utils/itertools';
|
||||||
import { Dimension } from './Dimension';
|
import { Dimension } from './Dimension';
|
||||||
import IProperties from '../../../Interfaces/IProperties';
|
import IContainerProperties from '../../../Interfaces/IContainerProperties';
|
||||||
|
import { transformX } from '../../../utils/svg';
|
||||||
|
import { camelize } from '../../../utils/stringtools';
|
||||||
|
|
||||||
interface IContainerProps {
|
interface IContainerProps {
|
||||||
model: IContainerModel
|
model: IContainerModel
|
||||||
|
@ -45,7 +46,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
|
||||||
</rect>);
|
</rect>);
|
||||||
// Dimension props
|
// Dimension props
|
||||||
const depth = getDepth(props.model);
|
const depth = getDepth(props.model);
|
||||||
const dimensionMargin = DIMENSION_MARGIN * (depth + 1);
|
const dimensionMargin = DIMENSION_MARGIN * depth;
|
||||||
const id = `dim-${props.model.properties.id}`;
|
const id = `dim-${props.model.properties.id}`;
|
||||||
const xStart: number = 0;
|
const xStart: number = 0;
|
||||||
const xEnd = props.model.properties.width;
|
const xEnd = props.model.properties.width;
|
||||||
|
@ -132,27 +133,7 @@ function GetChildrenDimensionProps(props: IContainerProps, dimensionMargin: numb
|
||||||
return { childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren };
|
return { childrenId, xChildrenStart, xChildrenEnd, yChildren, textChildren };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
function CreateReactCustomSVG(customSVG: string, props: IContainerProperties): React.ReactNode {
|
||||||
let transformedX = x;
|
|
||||||
if (xPositionReference === XPositionReference.Center) {
|
|
||||||
transformedX += width / 2;
|
|
||||||
} else if (xPositionReference === XPositionReference.Right) {
|
|
||||||
transformedX += width;
|
|
||||||
}
|
|
||||||
return transformedX;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function restoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
|
||||||
let transformedX = x;
|
|
||||||
if (xPositionReference === XPositionReference.Center) {
|
|
||||||
transformedX -= width / 2;
|
|
||||||
} else if (xPositionReference === XPositionReference.Right) {
|
|
||||||
transformedX -= width;
|
|
||||||
}
|
|
||||||
return transformedX;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateReactCustomSVG(customSVG: string, props: IProperties): React.ReactNode {
|
|
||||||
return <Interweave
|
return <Interweave
|
||||||
tagName='g'
|
tagName='g'
|
||||||
disableLineBreaks={true}
|
disableLineBreaks={true}
|
||||||
|
@ -162,7 +143,7 @@ function CreateReactCustomSVG(customSVG: string, props: IProperties): React.Reac
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function transform(node: HTMLElement, children: Node[], props: IProperties): React.ReactNode {
|
function transform(node: HTMLElement, children: Node[], props: IContainerProperties): React.ReactNode {
|
||||||
const supportedTags = ['line', 'path', 'rect'];
|
const supportedTags = ['line', 'path', 'rect'];
|
||||||
if (supportedTags.includes(node.tagName.toLowerCase())) {
|
if (supportedTags.includes(node.tagName.toLowerCase())) {
|
||||||
const attributes: {[att: string]: string | object | null} = {};
|
const attributes: {[att: string]: string | object | null} = {};
|
||||||
|
@ -207,7 +188,3 @@ function transform(node: HTMLElement, children: Node[], props: IProperties): Rea
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function camelize(str: string): any {
|
|
||||||
return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join('');
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||||
import { ContainerModel } from '../../../Interfaces/IContainerModel';
|
import { ContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { DIMENSION_MARGIN } from '../../../utils/default';
|
import { DIMENSION_MARGIN } from '../../../utils/default';
|
||||||
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
|
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
|
||||||
import { transformX } from './Container';
|
import { transformX } from '../../../utils/svg';
|
||||||
import { Dimension } from './Dimension';
|
import { Dimension } from './Dimension';
|
||||||
|
|
||||||
interface IDimensionLayerProps {
|
interface IDimensionLayerProps {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { getAbsolutePosition } from '../../../utils/itertools';
|
import { getAbsolutePosition } from '../../../utils/itertools';
|
||||||
|
|
||||||
interface ISelectorProps {
|
interface ISelectorProps {
|
||||||
selected: IContainerModel | null
|
selected?: IContainerModel
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Selector: React.FC<ISelectorProps> = (props) => {
|
export const Selector: React.FC<ISelectorProps> = (props) => {
|
||||||
|
|
39
src/Components/SVG/Elements/Symbol.tsx
Normal file
39
src/Components/SVG/Elements/Symbol.tsx
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { Interweave } from 'interweave';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
|
import { DIMENSION_MARGIN } from '../../../utils/default';
|
||||||
|
|
||||||
|
interface ISymbolProps {
|
||||||
|
model: ISymbolModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Symbol: React.FC<ISymbolProps> = (props) => {
|
||||||
|
const href = props.model.config.Image.Base64Image ?? props.model.config.Image.Url;
|
||||||
|
const hasSVG = props.model.config.Image.Svg !== undefined &&
|
||||||
|
props.model.config.Image.Svg !== null;
|
||||||
|
if (hasSVG) {
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
x={props.model.x}
|
||||||
|
y={-DIMENSION_MARGIN}
|
||||||
|
>
|
||||||
|
<Interweave
|
||||||
|
noWrap={true}
|
||||||
|
disableLineBreaks={true}
|
||||||
|
content={props.model.config.Image.Svg}
|
||||||
|
allowElements={true}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<image
|
||||||
|
href={href}
|
||||||
|
x={props.model.x}
|
||||||
|
y={-DIMENSION_MARGIN}
|
||||||
|
height={props.model.height}
|
||||||
|
width={props.model.width}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
23
src/Components/SVG/Elements/SymbolLayer.tsx
Normal file
23
src/Components/SVG/Elements/SymbolLayer.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
|
import { Symbol } from './Symbol';
|
||||||
|
|
||||||
|
interface ISymbolLayerProps {
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SymbolLayer: React.FC<ISymbolLayerProps> = (props) => {
|
||||||
|
const symbols: JSX.Element[] = [];
|
||||||
|
props.symbols.forEach((symbol) => {
|
||||||
|
symbols.push(
|
||||||
|
<Symbol key={`symbol-${symbol.id}`} model={symbol} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
{
|
||||||
|
symbols
|
||||||
|
}
|
||||||
|
</g>
|
||||||
|
);
|
||||||
|
};
|
|
@ -4,15 +4,17 @@ import { Container } from './Elements/Container';
|
||||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { Selector } from './Elements/Selector';
|
import { Selector } from './Elements/Selector';
|
||||||
import { BAR_WIDTH } from '../Bar/Bar';
|
import { BAR_WIDTH } from '../Bar/Bar';
|
||||||
import { DimensionLayer } from './Elements/DimensionLayer';
|
|
||||||
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
|
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
|
||||||
import { SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
|
import { SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
|
||||||
|
import { SymbolLayer } from './Elements/SymbolLayer';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
|
||||||
interface ISVGProps {
|
interface ISVGProps {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
children: ContainerModel | ContainerModel[] | null
|
children: ContainerModel | ContainerModel[] | null
|
||||||
selected: ContainerModel | null
|
selected?: ContainerModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Viewer {
|
interface Viewer {
|
||||||
|
@ -81,6 +83,7 @@ export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
|
||||||
? <DepthDimensionLayer roots={props.children}/>
|
? <DepthDimensionLayer roots={props.children}/>
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
<SymbolLayer symbols={props.symbols} />
|
||||||
<Selector selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */}
|
<Selector selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */}
|
||||||
</svg>
|
</svg>
|
||||||
</UncontrolledReactSVGPanZoom>
|
</UncontrolledReactSVGPanZoom>
|
||||||
|
|
55
src/Components/Select/Select.tsx
Normal file
55
src/Components/Select/Select.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IInputGroup } from '../../Interfaces/IInputGroup';
|
||||||
|
|
||||||
|
interface ISelectProps {
|
||||||
|
labelKey?: string
|
||||||
|
labelText: string
|
||||||
|
inputKey: string
|
||||||
|
labelClassName: string
|
||||||
|
inputClassName: string
|
||||||
|
inputs: IInputGroup[]
|
||||||
|
value?: string
|
||||||
|
onChange?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const className = `
|
||||||
|
w-full
|
||||||
|
text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
|
||||||
|
bg-white border-2 border-white rounded-lg placeholder-gray-800
|
||||||
|
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
|
||||||
|
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
|
||||||
|
|
||||||
|
export const Select: React.FC<ISelectProps> = (props) => {
|
||||||
|
const options = [(
|
||||||
|
<option key='symbol-none' value=''>None</option>
|
||||||
|
)];
|
||||||
|
|
||||||
|
props.inputs.forEach(input => {
|
||||||
|
options.push(<option
|
||||||
|
key={input.value}
|
||||||
|
value={input.value}
|
||||||
|
>
|
||||||
|
{input.text}
|
||||||
|
</option>);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label
|
||||||
|
key={props.labelKey}
|
||||||
|
className={`mt-4 text-xs font-medium text-gray-800 ${props.labelClassName}`}
|
||||||
|
htmlFor={props.inputKey}
|
||||||
|
>
|
||||||
|
{ props.labelText }
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id={props.inputKey}
|
||||||
|
value={props.value}
|
||||||
|
onChange={props.onChange}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{ options }
|
||||||
|
</select>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
56
src/Components/SymbolProperties/DynamicForm.tsx
Normal file
56
src/Components/SymbolProperties/DynamicForm.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { restoreX, transformX } from '../../utils/svg';
|
||||||
|
import { InputGroup } from '../InputGroup/InputGroup';
|
||||||
|
|
||||||
|
interface IDynamicFormProps {
|
||||||
|
symbol: ISymbolModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
onChange: (key: string, value: string | number | boolean) => void
|
||||||
|
}
|
||||||
|
const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
|
||||||
|
return (
|
||||||
|
<div className='grid grid-cols-2 gap-y-4'>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Name'
|
||||||
|
inputKey='id'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='string'
|
||||||
|
value={props.symbol.id.toString()}
|
||||||
|
isDisabled={true}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='x'
|
||||||
|
inputKey='x'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
value={transformX(props.symbol.x, props.symbol.width, props.symbol.config.XPositionReference).toString()}
|
||||||
|
onChange={(event) => props.onChange('x', restoreX(Number(event.target.value), props.symbol.width, props.symbol.config.XPositionReference))}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Height'
|
||||||
|
inputKey='height'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={props.symbol.height.toString()}
|
||||||
|
onChange={(event) => props.onChange('height', Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Width'
|
||||||
|
inputKey='width'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='number'
|
||||||
|
min={0}
|
||||||
|
value={props.symbol.width.toString()}
|
||||||
|
onChange={(event) => props.onChange('width', Number(event.target.value))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicForm;
|
17
src/Components/SymbolProperties/Form.tsx
Normal file
17
src/Components/SymbolProperties/Form.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import DynamicForm from './DynamicForm';
|
||||||
|
|
||||||
|
interface IFormProps {
|
||||||
|
symbol: ISymbolModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
onChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Form: React.FunctionComponent<IFormProps> = (props) => {
|
||||||
|
return <DynamicForm
|
||||||
|
symbol={props.symbol}
|
||||||
|
symbols={props.symbols}
|
||||||
|
onChange={props.onChange}
|
||||||
|
/>;
|
||||||
|
};
|
27
src/Components/SymbolProperties/SymbolProperties.tsx
Normal file
27
src/Components/SymbolProperties/SymbolProperties.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import IContainerProperties from '../../Interfaces/IContainerProperties';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { ToggleButton } from '../ToggleButton/ToggleButton';
|
||||||
|
import { Form } from './Form';
|
||||||
|
|
||||||
|
interface ISymbolPropertiesProps {
|
||||||
|
symbol?: ISymbolModel
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
onChange: (key: string, value: string | number | boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SymbolProperties: React.FC<ISymbolPropertiesProps> = (props: ISymbolPropertiesProps) => {
|
||||||
|
if (props.symbol === undefined) {
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='h-3/5 p-3 bg-slate-200 overflow-y-auto'>
|
||||||
|
<Form
|
||||||
|
symbol={props.symbol}
|
||||||
|
symbols={props.symbols}
|
||||||
|
onChange={props.onChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
68
src/Components/Symbols/Symbols.tsx
Normal file
68
src/Components/Symbols/Symbols.tsx
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
|
||||||
|
import { truncateString } from '../../utils/stringtools';
|
||||||
|
|
||||||
|
interface ISymbolsProps {
|
||||||
|
componentOptions: IAvailableSymbol[]
|
||||||
|
isOpen: boolean
|
||||||
|
buttonOnClick: (type: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragStart(event: React.DragEvent<HTMLButtonElement>): void {
|
||||||
|
event.dataTransfer.setData('type', (event.target as HTMLButtonElement).id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Symbols: React.FC<ISymbolsProps> = (props: ISymbolsProps) => {
|
||||||
|
const listElements = props.componentOptions.map(componentOption => {
|
||||||
|
if (componentOption.Image.Url !== undefined || componentOption.Image.Base64Image !== undefined) {
|
||||||
|
const url = componentOption.Image.Base64Image ?? componentOption.Image.Url;
|
||||||
|
return (<button
|
||||||
|
className='justify-center sidebar-component-card hover:h-full'
|
||||||
|
key={componentOption.Name}
|
||||||
|
id={componentOption.Name}
|
||||||
|
title={componentOption.Name}
|
||||||
|
onClick={() => props.buttonOnClick(componentOption.Name)}
|
||||||
|
draggable={true}
|
||||||
|
onDragStart={(event) => handleDragStart(event)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<img
|
||||||
|
className='transition-all h-12 w-full object-cover'
|
||||||
|
src={url}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{truncateString(componentOption.Name, 5)}
|
||||||
|
</div>
|
||||||
|
</button>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<button
|
||||||
|
className='group justify-center sidebar-component hover:h-full'
|
||||||
|
key={componentOption.Name}
|
||||||
|
id={componentOption.Name}
|
||||||
|
title={componentOption.Name}
|
||||||
|
onClick={() => props.buttonOnClick(componentOption.Name)}
|
||||||
|
draggable={true}
|
||||||
|
onDragStart={(event) => handleDragStart(event)}
|
||||||
|
>
|
||||||
|
|
||||||
|
{truncateString(componentOption.Name, 5)}
|
||||||
|
</button>);
|
||||||
|
});
|
||||||
|
|
||||||
|
const isOpenClasses = props.isOpen ? 'left-16' : '-left-64';
|
||||||
|
return (
|
||||||
|
<div className={`fixed z-10 bg-slate-200
|
||||||
|
text-gray-700 transition-all h-full w-64
|
||||||
|
overflow-y-auto ${isOpenClasses}`}>
|
||||||
|
<div className='bg-slate-100 sidebar-title'>
|
||||||
|
Symbols
|
||||||
|
</div>
|
||||||
|
<div className='grid grid-cols-1 md:grid-cols-3 gap-2
|
||||||
|
m-2 md:text-xs font-bold'>
|
||||||
|
{listElements}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
34
src/Components/SymbolsSidebar/MouseEventHandlers.ts
Normal file
34
src/Components/SymbolsSidebar/MouseEventHandlers.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { IPoint } from '../../Interfaces/IPoint';
|
||||||
|
|
||||||
|
export function handleRightClick(
|
||||||
|
event: MouseEvent,
|
||||||
|
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
setOnClickSymbolId: React.Dispatch<React.SetStateAction<string>>,
|
||||||
|
setContextMenuPosition: React.Dispatch<React.SetStateAction<IPoint>>
|
||||||
|
): void {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (!(event.target instanceof HTMLButtonElement)) {
|
||||||
|
setIsContextMenuOpen(false);
|
||||||
|
setOnClickSymbolId('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextMenuPosition: IPoint = { x: event.pageX, y: event.pageY };
|
||||||
|
setIsContextMenuOpen(true);
|
||||||
|
setOnClickSymbolId(event.target.id);
|
||||||
|
setContextMenuPosition(contextMenuPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleLeftClick(
|
||||||
|
isContextMenuOpen: boolean,
|
||||||
|
setIsContextMenuOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
setOnClickContainerId: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
): void {
|
||||||
|
if (!isContextMenuOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsContextMenuOpen(false);
|
||||||
|
setOnClickContainerId('');
|
||||||
|
}
|
137
src/Components/SymbolsSidebar/SymbolsSidebar.tsx
Normal file
137
src/Components/SymbolsSidebar/SymbolsSidebar.tsx
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { FixedSizeList as List } from 'react-window';
|
||||||
|
import { Menu } from '../Menu/Menu';
|
||||||
|
import { MenuItem } from '../Menu/MenuItem';
|
||||||
|
import { handleLeftClick, handleRightClick } from './MouseEventHandlers';
|
||||||
|
import { IPoint } from '../../Interfaces/IPoint';
|
||||||
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { SymbolProperties } from '../SymbolProperties/SymbolProperties';
|
||||||
|
|
||||||
|
interface ISymbolsSidebarProps {
|
||||||
|
SelectedSymbolId: string
|
||||||
|
symbols: Map<string, ISymbolModel>
|
||||||
|
isOpen: boolean
|
||||||
|
isHistoryOpen: boolean
|
||||||
|
OnPropertyChange: (key: string, value: string | number | boolean) => void
|
||||||
|
SelectSymbol: (symbolId: string) => void
|
||||||
|
DeleteSymbol: (containerid: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SymbolsSidebar: React.FC<ISymbolsSidebarProps> = (props: ISymbolsSidebarProps): JSX.Element => {
|
||||||
|
// States
|
||||||
|
const [isContextMenuOpen, setIsContextMenuOpen] = React.useState<boolean>(false);
|
||||||
|
const [onClickSymbolId, setOnClickSymbolId] = React.useState<string>('');
|
||||||
|
const [contextMenuPosition, setContextMenuPosition] = React.useState<IPoint>({
|
||||||
|
x: 0,
|
||||||
|
y: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const elementRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
React.useEffect(() => {
|
||||||
|
const onContextMenu = (event: MouseEvent): void => handleRightClick(
|
||||||
|
event,
|
||||||
|
setIsContextMenuOpen,
|
||||||
|
setOnClickSymbolId,
|
||||||
|
setContextMenuPosition
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLeftClick = (): void => handleLeftClick(
|
||||||
|
isContextMenuOpen,
|
||||||
|
setIsContextMenuOpen,
|
||||||
|
setOnClickSymbolId
|
||||||
|
);
|
||||||
|
|
||||||
|
elementRef.current?.addEventListener(
|
||||||
|
'contextmenu',
|
||||||
|
onContextMenu
|
||||||
|
);
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
'click',
|
||||||
|
onLeftClick
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
elementRef.current?.removeEventListener(
|
||||||
|
'contextmenu',
|
||||||
|
onContextMenu
|
||||||
|
);
|
||||||
|
|
||||||
|
window.removeEventListener(
|
||||||
|
'click',
|
||||||
|
onLeftClick
|
||||||
|
);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Render
|
||||||
|
let isOpenClasses = '-right-64';
|
||||||
|
if (props.isOpen) {
|
||||||
|
isOpenClasses = props.isHistoryOpen
|
||||||
|
? 'right-64'
|
||||||
|
: 'right-0';
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers = [...props.symbols.values()];
|
||||||
|
const Row = ({ index, style }: {index: number, style: React.CSSProperties}): JSX.Element => {
|
||||||
|
const container = containers[index];
|
||||||
|
const key = container.id.toString();
|
||||||
|
const text = key;
|
||||||
|
const selectedClass: string = props.SelectedSymbolId !== '' &&
|
||||||
|
props.SelectedSymbolId === container.id
|
||||||
|
? 'border-l-4 bg-slate-400/60 hover:bg-slate-400'
|
||||||
|
: 'bg-slate-300/60 hover:bg-slate-300';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
`w-full border-blue-500 elements-sidebar-row whitespace-pre
|
||||||
|
text-left text-sm font-medium transition-all ${selectedClass}`
|
||||||
|
}
|
||||||
|
id={key}
|
||||||
|
key={key}
|
||||||
|
style={style}
|
||||||
|
onClick={() => props.SelectSymbol(key)}
|
||||||
|
>
|
||||||
|
{ text }
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`fixed flex flex-col bg-slate-100 text-gray-800 transition-all h-full w-64 overflow-y-auto z-20 ${isOpenClasses}`}>
|
||||||
|
<div className='bg-slate-100 font-bold sidebar-title'>
|
||||||
|
Elements
|
||||||
|
</div>
|
||||||
|
<div ref={elementRef} className='h-96 text-gray-800'>
|
||||||
|
<List
|
||||||
|
className='List divide-y divide-black'
|
||||||
|
itemCount={containers.length}
|
||||||
|
itemSize={35}
|
||||||
|
height={384}
|
||||||
|
width={256}
|
||||||
|
>
|
||||||
|
{ Row }
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
<Menu
|
||||||
|
className='transition-opacity rounded bg-slate-200 py-1 drop-shadow-xl'
|
||||||
|
x={contextMenuPosition.x}
|
||||||
|
y={contextMenuPosition.y}
|
||||||
|
isOpen={isContextMenuOpen}
|
||||||
|
>
|
||||||
|
<MenuItem className='contextmenu-item' text='Delete' onClick={() => {
|
||||||
|
setIsContextMenuOpen(false);
|
||||||
|
props.DeleteSymbol(onClickSymbolId);
|
||||||
|
}} />
|
||||||
|
</Menu>
|
||||||
|
<SymbolProperties
|
||||||
|
symbol={props.symbols.get(props.SelectedSymbolId)}
|
||||||
|
symbols={props.symbols}
|
||||||
|
onChange={props.OnPropertyChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,38 +3,55 @@ import { ElementsSidebar } from '../ElementsSidebar/ElementsSidebar';
|
||||||
import { Sidebar } from '../Sidebar/Sidebar';
|
import { Sidebar } from '../Sidebar/Sidebar';
|
||||||
import { History } from '../History/History';
|
import { History } from '../History/History';
|
||||||
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
|
||||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
||||||
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
||||||
import { Bar } from '../Bar/Bar';
|
import { Bar } from '../Bar/Bar';
|
||||||
|
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
|
||||||
|
import { Symbols } from '../Symbols/Symbols';
|
||||||
|
import { SymbolsSidebar } from '../SymbolsSidebar/SymbolsSidebar';
|
||||||
|
|
||||||
interface IUIProps {
|
interface IUIProps {
|
||||||
|
SelectedContainer: IContainerModel | undefined
|
||||||
current: IHistoryState
|
current: IHistoryState
|
||||||
history: IHistoryState[]
|
history: IHistoryState[]
|
||||||
historyCurrentStep: number
|
historyCurrentStep: number
|
||||||
AvailableContainers: IAvailableContainer[]
|
AvailableContainers: IAvailableContainer[]
|
||||||
SelectContainer: (container: ContainerModel) => void
|
AvailableSymbols: IAvailableSymbol[]
|
||||||
|
SelectContainer: (containerId: string) => void
|
||||||
DeleteContainer: (containerId: string) => void
|
DeleteContainer: (containerId: string) => void
|
||||||
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
OnPropertyChange: (key: string, value: string | number | boolean, isStyle?: boolean) => void
|
||||||
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
OnPropertiesSubmit: (event: React.FormEvent<HTMLFormElement>) => void
|
||||||
AddContainerToSelectedContainer: (type: string) => void
|
AddContainerToSelectedContainer: (type: string) => void
|
||||||
AddContainer: (index: number, type: string, parentId: string) => void
|
AddContainer: (index: number, type: string, parentId: string) => void
|
||||||
|
AddSymbol: (type: string) => void
|
||||||
|
OnSymbolPropertyChange: (key: string, value: string | number | boolean) => void
|
||||||
|
SelectSymbol: (symbolId: string) => void
|
||||||
|
DeleteSymbol: (symbolId: string) => void
|
||||||
SaveEditorAsJSON: () => void
|
SaveEditorAsJSON: () => void
|
||||||
SaveEditorAsSVG: () => void
|
SaveEditorAsSVG: () => void
|
||||||
LoadState: (move: number) => void
|
LoadState: (move: number) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CloseOtherSidebars(
|
||||||
|
setIsSidebarOpen: React.Dispatch<React.SetStateAction<boolean>>,
|
||||||
|
setIsSymbolsOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
): void {
|
||||||
|
setIsSidebarOpen(false);
|
||||||
|
setIsSymbolsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
const [isSidebarOpen, setIsSidebarOpen] = React.useState(true);
|
||||||
const [isElementsSidebarOpen, setIsElementsSidebarOpen] = React.useState(false);
|
const [isSymbolsOpen, setIsSymbolsOpen] = React.useState(false);
|
||||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
||||||
|
|
||||||
let buttonRightOffsetClasses = 'right-12';
|
let buttonRightOffsetClasses = 'right-12';
|
||||||
if (isElementsSidebarOpen || isHistoryOpen) {
|
if (isSidebarOpen || isHistoryOpen) {
|
||||||
buttonRightOffsetClasses = 'right-72';
|
buttonRightOffsetClasses = 'right-72';
|
||||||
}
|
}
|
||||||
if (isHistoryOpen && isElementsSidebarOpen) {
|
if (isHistoryOpen && isSidebarOpen) {
|
||||||
buttonRightOffsetClasses = 'right-[544px]';
|
buttonRightOffsetClasses = 'right-[544px]';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,22 +59,35 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
||||||
<>
|
<>
|
||||||
<Bar
|
<Bar
|
||||||
isSidebarOpen={isSidebarOpen}
|
isSidebarOpen={isSidebarOpen}
|
||||||
isElementsSidebarOpen={isElementsSidebarOpen}
|
isSymbolsOpen={isSymbolsOpen}
|
||||||
|
isElementsSidebarOpen={isSidebarOpen}
|
||||||
isHistoryOpen={isHistoryOpen}
|
isHistoryOpen={isHistoryOpen}
|
||||||
ToggleElementsSidebar={() => setIsElementsSidebarOpen(!isElementsSidebarOpen)}
|
ToggleSidebar={() => {
|
||||||
ToggleSidebar={() => setIsSidebarOpen(!isSidebarOpen)}
|
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
|
||||||
|
setIsSidebarOpen(!isSidebarOpen);
|
||||||
|
}}
|
||||||
|
ToggleSymbols={() => {
|
||||||
|
CloseOtherSidebars(setIsSidebarOpen, setIsSymbolsOpen);
|
||||||
|
setIsSymbolsOpen(!isSymbolsOpen);
|
||||||
|
}}
|
||||||
ToggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
|
ToggleTimeline={() => setIsHistoryOpen(!isHistoryOpen)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Sidebar
|
<Sidebar
|
||||||
componentOptions={props.AvailableContainers}
|
componentOptions={props.AvailableContainers}
|
||||||
isOpen={isSidebarOpen}
|
isOpen={isSidebarOpen}
|
||||||
buttonOnClick={(type: string) => props.AddContainerToSelectedContainer(type)}
|
buttonOnClick={props.AddContainerToSelectedContainer}
|
||||||
|
/>
|
||||||
|
<Symbols
|
||||||
|
componentOptions={props.AvailableSymbols}
|
||||||
|
isOpen={isSymbolsOpen}
|
||||||
|
buttonOnClick={props.AddSymbol}
|
||||||
/>
|
/>
|
||||||
<ElementsSidebar
|
<ElementsSidebar
|
||||||
MainContainer={props.current.MainContainer}
|
MainContainer={props.current.MainContainer}
|
||||||
SelectedContainer={props.current.SelectedContainer}
|
symbols={props.current.Symbols}
|
||||||
isOpen={isElementsSidebarOpen}
|
SelectedContainer={props.SelectedContainer}
|
||||||
|
isOpen={isSidebarOpen}
|
||||||
isHistoryOpen={isHistoryOpen}
|
isHistoryOpen={isHistoryOpen}
|
||||||
OnPropertyChange={props.OnPropertyChange}
|
OnPropertyChange={props.OnPropertyChange}
|
||||||
OnPropertiesSubmit={props.OnPropertiesSubmit}
|
OnPropertiesSubmit={props.OnPropertiesSubmit}
|
||||||
|
@ -65,6 +95,15 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
||||||
DeleteContainer={props.DeleteContainer}
|
DeleteContainer={props.DeleteContainer}
|
||||||
AddContainer={props.AddContainer}
|
AddContainer={props.AddContainer}
|
||||||
/>
|
/>
|
||||||
|
<SymbolsSidebar
|
||||||
|
SelectedSymbolId={props.current.SelectedSymbolId}
|
||||||
|
symbols={props.current.Symbols}
|
||||||
|
isOpen={isSymbolsOpen}
|
||||||
|
isHistoryOpen={isHistoryOpen}
|
||||||
|
OnPropertyChange={props.OnSymbolPropertyChange}
|
||||||
|
SelectSymbol={props.SelectSymbol}
|
||||||
|
DeleteSymbol={props.DeleteSymbol}
|
||||||
|
/>
|
||||||
<History
|
<History
|
||||||
history={props.history}
|
history={props.history}
|
||||||
historyCurrentStep={props.historyCurrentStep}
|
historyCurrentStep={props.historyCurrentStep}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import { IImage } from './IImage';
|
||||||
* Model of available symbol to configure the application */
|
* Model of available symbol to configure the application */
|
||||||
export interface IAvailableSymbol {
|
export interface IAvailableSymbol {
|
||||||
Name: string
|
Name: string
|
||||||
XPositionReference: XPositionReference
|
|
||||||
Image: IImage
|
Image: IImage
|
||||||
Width: number
|
Width?: number
|
||||||
Height: number
|
Height?: number
|
||||||
|
XPositionReference?: XPositionReference
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
import IProperties from './IProperties';
|
import IContainerProperties from './IContainerProperties';
|
||||||
|
|
||||||
export interface IContainerModel {
|
export interface IContainerModel {
|
||||||
children: IContainerModel[]
|
children: IContainerModel[]
|
||||||
parent: IContainerModel | null
|
parent: IContainerModel | null
|
||||||
properties: IProperties
|
properties: IContainerProperties
|
||||||
userData: Record<string, string | number>
|
userData: Record<string, string | number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Macro for creating the interface
|
||||||
|
* Do not add methods since they will be lost during serialization
|
||||||
|
*/
|
||||||
export class ContainerModel implements IContainerModel {
|
export class ContainerModel implements IContainerModel {
|
||||||
public children: IContainerModel[];
|
public children: IContainerModel[];
|
||||||
public parent: IContainerModel | null;
|
public parent: IContainerModel | null;
|
||||||
public properties: IProperties;
|
public properties: IContainerProperties;
|
||||||
public userData: Record<string, string | number>;
|
public userData: Record<string, string | number>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
parent: IContainerModel | null,
|
parent: IContainerModel | null,
|
||||||
properties: IProperties,
|
properties: IContainerProperties,
|
||||||
children: IContainerModel[] = [],
|
children: IContainerModel[] = [],
|
||||||
userData = {}) {
|
userData = {}) {
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
|
|
@ -4,13 +4,17 @@ import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
/**
|
/**
|
||||||
* Properties of a container
|
* Properties of a container
|
||||||
*/
|
*/
|
||||||
export default interface IProperties {
|
export default interface IContainerProperties {
|
||||||
/** id of the container */
|
/** id of the container */
|
||||||
id: string
|
id: string
|
||||||
|
|
||||||
|
// TODO: replace null by empty string
|
||||||
/** id of the parent container (null when there is no parent) */
|
/** id of the parent container (null when there is no parent) */
|
||||||
parentId: string | null
|
parentId: string | null
|
||||||
|
|
||||||
|
/** id of the linked symbol ('' when there is no parent) */
|
||||||
|
linkedSymbolId: string
|
||||||
|
|
||||||
/** Text displayed in the container */
|
/** Text displayed in the container */
|
||||||
displayedText: string
|
displayedText: string
|
||||||
|
|
|
@ -1,9 +1,22 @@
|
||||||
import { IContainerModel } from './IContainerModel';
|
import { IContainerModel } from './IContainerModel';
|
||||||
|
import { ISymbolModel } from './ISymbolModel';
|
||||||
|
|
||||||
export interface IHistoryState {
|
export interface IHistoryState {
|
||||||
|
/** Last editor action */
|
||||||
LastAction: string
|
LastAction: string
|
||||||
|
|
||||||
|
/** Reference to the main container */
|
||||||
MainContainer: IContainerModel
|
MainContainer: IContainerModel
|
||||||
SelectedContainer: IContainerModel | null
|
|
||||||
|
/** Id of the selected container */
|
||||||
SelectedContainerId: string
|
SelectedContainerId: string
|
||||||
|
|
||||||
|
/** Counter of type of container. Used for ids. */
|
||||||
TypeCounters: Record<string, number>
|
TypeCounters: Record<string, number>
|
||||||
|
|
||||||
|
/** List of symbols */
|
||||||
|
Symbols: Map<string, ISymbolModel>
|
||||||
|
|
||||||
|
/** Selected symbols id */
|
||||||
|
SelectedSymbolId: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
/** Model of an image with multiple source */
|
/**
|
||||||
|
* Model of an image with multiple source
|
||||||
|
* It must at least have one source.
|
||||||
|
*
|
||||||
|
* If Url/Base64Image and Svg are set,
|
||||||
|
* Url/Base64Image will be shown in the menu while SVG will be drawn
|
||||||
|
*/
|
||||||
export interface IImage {
|
export interface IImage {
|
||||||
|
/** Name of the image */
|
||||||
Name: string
|
Name: string
|
||||||
Url: string
|
|
||||||
Base64Image: string
|
/** (optional) Url of the image */
|
||||||
Svg: string
|
Url?: string
|
||||||
|
|
||||||
|
/** (optional) base64 data of the image */
|
||||||
|
Base64Image?: string
|
||||||
|
|
||||||
|
/** (optional) SVG string */
|
||||||
|
Svg?: string
|
||||||
}
|
}
|
||||||
|
|
24
src/Interfaces/ISymbolModel.ts
Normal file
24
src/Interfaces/ISymbolModel.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { IAvailableSymbol } from './IAvailableSymbol';
|
||||||
|
|
||||||
|
export interface ISymbolModel {
|
||||||
|
/** Identifier */
|
||||||
|
id: string
|
||||||
|
|
||||||
|
/** Type */
|
||||||
|
type: string
|
||||||
|
|
||||||
|
/** Configuration of the symbol */
|
||||||
|
config: IAvailableSymbol
|
||||||
|
|
||||||
|
/** Horizontal offset */
|
||||||
|
x: number
|
||||||
|
|
||||||
|
/** Width */
|
||||||
|
width: number
|
||||||
|
|
||||||
|
/** Height */
|
||||||
|
height: number
|
||||||
|
|
||||||
|
/** List of linked container id */
|
||||||
|
linkedContainers: Set<string>
|
||||||
|
}
|
|
@ -11,6 +11,10 @@
|
||||||
@apply transition-all px-2 py-6 text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300
|
@apply transition-all px-2 py-6 text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar-component-card {
|
||||||
|
@apply transition-all overflow-hidden text-sm rounded-lg bg-slate-300/60 hover:bg-slate-300
|
||||||
|
}
|
||||||
|
|
||||||
.elements-sidebar-row {
|
.elements-sidebar-row {
|
||||||
@apply pl-6 pr-6 pt-2 pb-2 w-full
|
@apply pl-6 pr-6 pt-2 pb-2 w-full
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
import { IAvailableContainer } from '../Interfaces/IAvailableContainer';
|
import { IAvailableContainer } from '../Interfaces/IAvailableContainer';
|
||||||
import { IConfiguration } from '../Interfaces/IConfiguration';
|
import { IConfiguration } from '../Interfaces/IConfiguration';
|
||||||
import { IContainerModel } from '../Interfaces/IContainerModel';
|
import { IContainerModel } from '../Interfaces/IContainerModel';
|
||||||
import IProperties from '../Interfaces/IProperties';
|
import IContainerProperties from '../Interfaces/IContainerProperties';
|
||||||
|
|
||||||
/// CONTAINRE DEFAULTS ///
|
/// CONTAINER DEFAULTS ///
|
||||||
|
|
||||||
export const SHOW_TEXT = true;
|
export const SHOW_TEXT = true;
|
||||||
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;
|
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;
|
||||||
|
@ -18,10 +18,16 @@ export const SHOW_DIMENSIONS_PER_DEPTH = true;
|
||||||
export const DIMENSION_MARGIN = 50;
|
export const DIMENSION_MARGIN = 50;
|
||||||
export const NOTCHES_LENGTH = 4;
|
export const NOTCHES_LENGTH = 4;
|
||||||
|
|
||||||
|
/// SYMBOL DEFAULTS ///
|
||||||
|
|
||||||
|
export const DEFAULT_SYMBOL_WIDTH = 32;
|
||||||
|
export const DEFAULT_SYMBOL_HEIGHT = 32;
|
||||||
|
|
||||||
/// EDITOR DEFAULTS ///
|
/// EDITOR DEFAULTS ///
|
||||||
|
|
||||||
export const ENABLE_SHORTCUTS = true;
|
export const ENABLE_SHORTCUTS = true;
|
||||||
export const MAX_HISTORY = 200;
|
export const MAX_HISTORY = 200;
|
||||||
|
export const APPLY_BEHAVIORS_ON_CHILDREN = true;
|
||||||
|
|
||||||
export const DEFAULT_CONFIG: IConfiguration = {
|
export const DEFAULT_CONFIG: IConfiguration = {
|
||||||
AvailableContainers: [
|
AvailableContainers: [
|
||||||
|
@ -47,9 +53,10 @@ export const DEFAULT_CONFIG: IConfiguration = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
|
export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = {
|
||||||
id: 'main',
|
id: 'main',
|
||||||
parentId: 'null',
|
parentId: 'null',
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: 'main',
|
displayedText: 'main',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
|
@ -72,9 +79,10 @@ export const GetDefaultContainerProps = (
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
containerConfig: IAvailableContainer
|
containerConfig: IAvailableContainer
|
||||||
): IProperties => ({
|
): IContainerProperties => ({
|
||||||
id: `${type}-${typeCount}`,
|
id: `${type}-${typeCount}`,
|
||||||
parentId: parent.properties.id,
|
parentId: parent.properties.id,
|
||||||
|
linkedSymbolId: '',
|
||||||
displayedText: `${type}-${typeCount}`,
|
displayedText: `${type}-${typeCount}`,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
@ -83,7 +91,7 @@ export const GetDefaultContainerProps = (
|
||||||
isRigidBody: false, // set this to true to replicate Florian's project
|
isRigidBody: false, // set this to true to replicate Florian's project
|
||||||
isAnchor: false,
|
isAnchor: false,
|
||||||
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
|
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
|
||||||
minWidth: containerConfig.MinWidth ?? 0,
|
minWidth: containerConfig.MinWidth ?? 1,
|
||||||
customSVG: containerConfig.CustomSVG,
|
customSVG: containerConfig.CustomSVG,
|
||||||
style: structuredClone(containerConfig.Style),
|
style: structuredClone(containerConfig.Style),
|
||||||
userData: structuredClone(containerConfig.UserData)
|
userData: structuredClone(containerConfig.UserData)
|
||||||
|
|
|
@ -18,6 +18,11 @@ export function Revive(editorState: IEditorState): void {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.Symbols = new Map(state.Symbols);
|
||||||
|
for (const symbol of state.Symbols.values()) {
|
||||||
|
symbol.linkedContainers = new Set(symbol.linkedContainers);
|
||||||
|
}
|
||||||
|
|
||||||
const it = MakeIterator(state.MainContainer);
|
const it = MakeIterator(state.MainContainer);
|
||||||
for (const container of it) {
|
for (const container of it) {
|
||||||
const parentId = container.properties.parentId;
|
const parentId = container.properties.parentId;
|
||||||
|
@ -31,24 +36,21 @@ export function Revive(editorState: IEditorState): void {
|
||||||
}
|
}
|
||||||
container.parent = parent;
|
container.parent = parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const selected = findContainerById(state.MainContainer, state.SelectedContainerId);
|
|
||||||
if (selected === undefined) {
|
|
||||||
state.SelectedContainer = null;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
state.SelectedContainer = selected;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getCircularReplacer = (): (key: any, value: object | null) => object | null | undefined => {
|
export const getCircularReplacer = (): (key: any, value: object | Map<string, any> | null) => object | null | undefined => {
|
||||||
return (key: any, value: object | null) => {
|
return (key: any, value: object | null) => {
|
||||||
if (key === 'parent') {
|
if (key === 'parent') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (key === 'SelectedContainer') {
|
if (key === 'Symbols') {
|
||||||
return;
|
return Array.from((value as Map<string, any>).entries());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'linkedContainers') {
|
||||||
|
return Array.from(value as Set<string>);
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
|
|
|
@ -4,3 +4,7 @@ export function truncateString(str: string, num: number): string {
|
||||||
}
|
}
|
||||||
return `${str.slice(0, num)}...`;
|
return `${str.slice(0, num)}...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function camelize(str: string): any {
|
||||||
|
return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join('');
|
||||||
|
}
|
||||||
|
|
21
src/utils/svg.ts
Normal file
21
src/utils/svg.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { XPositionReference } from '../Enums/XPositionReference';
|
||||||
|
|
||||||
|
export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
||||||
|
let transformedX = x;
|
||||||
|
if (xPositionReference === XPositionReference.Center) {
|
||||||
|
transformedX += width / 2;
|
||||||
|
} else if (xPositionReference === XPositionReference.Right) {
|
||||||
|
transformedX += width;
|
||||||
|
}
|
||||||
|
return transformedX;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function restoreX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
|
||||||
|
let transformedX = x;
|
||||||
|
if (xPositionReference === XPositionReference.Center) {
|
||||||
|
transformedX -= width / 2;
|
||||||
|
} else if (xPositionReference === XPositionReference.Right) {
|
||||||
|
transformedX -= width;
|
||||||
|
}
|
||||||
|
return transformedX;
|
||||||
|
}
|
|
@ -112,7 +112,8 @@ const GetSVGLayoutConfiguration = () => {
|
||||||
],
|
],
|
||||||
AvailableSymbols: [
|
AvailableSymbols: [
|
||||||
{
|
{
|
||||||
Height: 0,
|
Width: 32,
|
||||||
|
Height: 32,
|
||||||
Image: {
|
Image: {
|
||||||
Base64Image: null,
|
Base64Image: null,
|
||||||
Name: null,
|
Name: null,
|
||||||
|
@ -120,11 +121,11 @@ const GetSVGLayoutConfiguration = () => {
|
||||||
Url: 'https://www.manutan.fr/img/S/GRP/ST/AIG3930272.jpg'
|
Url: 'https://www.manutan.fr/img/S/GRP/ST/AIG3930272.jpg'
|
||||||
},
|
},
|
||||||
Name: 'Poteau structure',
|
Name: 'Poteau structure',
|
||||||
Width: 0,
|
|
||||||
XPositionReference: 1
|
XPositionReference: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Height: 0,
|
Width: 32,
|
||||||
|
Height: 32,
|
||||||
Image: {
|
Image: {
|
||||||
Base64Image: null,
|
Base64Image: null,
|
||||||
Name: null,
|
Name: null,
|
||||||
|
@ -132,7 +133,6 @@ const GetSVGLayoutConfiguration = () => {
|
||||||
Url: 'https://e7.pngegg.com/pngimages/647/127/png-clipart-svg-working-group-information-world-wide-web-internet-structure.png'
|
Url: 'https://e7.pngegg.com/pngimages/647/127/png-clipart-svg-working-group-information-world-wide-web-internet-structure.png'
|
||||||
},
|
},
|
||||||
Name: 'Joint de structure',
|
Name: 'Joint de structure',
|
||||||
Width: 0,
|
|
||||||
XPositionReference: 0
|
XPositionReference: 0
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue