Merge branch 'dev'
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Siklos 2022-08-18 16:40:33 +02:00
commit 3221d97eff
39 changed files with 30948 additions and 795 deletions

View file

@ -7,7 +7,7 @@ steps:
image: node:16
commands:
- node ./test-server/node-http.js &
- npm ci
- npm i
- npm run test:nowatch
- npm run build
@ -20,6 +20,6 @@ steps:
image: node
commands:
- node ./test-server/node-http.js &
- npm ci
- npm i
- npm run test:nowatch
- npm run build

View file

@ -31,6 +31,8 @@ module.exports = {
'@typescript-eslint/semi': ['warn', 'always'],
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/ban-types': ['error'],
'@typescript-eslint/no-floating-promises': 'off', // disabled cuz troublesome for SweetAlert since they never reject
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies
}

View file

@ -13,8 +13,8 @@ It depends on Vite in order to build the project.
Others dependencies:
- [react-dom](https://reactjs.org/docs/react-dom.html): library used to inject the app to `#root` html element.
- [react-window](https://www.npmjs.com/package/react-windows): component that offers component dynamic loading over scroll (very useful++)
- [react-svg-pan-zoom](https://www.npmjs.com/package/react-svg-pan-zoom): component that offers pan + zoom to a svg element
- [react-window](https://www.npmjs.com/package/react-window): component that offers component dynamic loading over scroll (very useful++)
- [react-svg-pan-zoom](https://www.npmjs.com/package/react-svg-pan-zoom): component that offers pan + zoom to a svg element (if this gets deprecated, please try to migrate to HTML Canvas before trying a new library)
# [Vite](https://vitejs.dev/)
@ -49,6 +49,12 @@ Other dependencies:
SVG Icons that can be used as JSX elements with Tailwind CSS
# [Interweave](https://interweave.dev/)
React library to render HTML from string.
In this project, it is particularly used for the CustomSVG property.
If this dependencies gets deprecated please revert [PR#18 e96e4f12](https://dev.azure.com/enguyen0660/SVGLayoutDesignerReact/_git/SVGLayoutDesignerReact/commit/e96e4f123b4aa4c9cdb327d4d617ab0e63dc4d0f?refName=refs%2Fheads%2Fdev)
# Testing

14
docs/Hardcoded.md Normal file
View file

@ -0,0 +1,14 @@
> Here you will find the documentation of desastrous stuff that I made
# XPositionReference
XPositionReference is used as a fake horizontal offset indicator.
The truth is that the svg will always take the left for its transformations and the best for us is to do the same.
That's why everything that is shown to the user about XPositionReference is an illusion. Like for example:
- The inputs, see `PropertiesOperations.ts`, `StaticForm`, `DynamicForm`.
- Child dimensions, see `Container.ts`.
Look for use of `transformX()` and `restoreX()`.

View file

@ -6,7 +6,8 @@ The project is structured this way
.
├── docs/ Documentation folder
├── public/ Public folder in which the index.html
│ import its resources
│ │ import its resources
│ └── workers/ Webworkers folder
├── src/ Source folder for the react app
│ ├── assets/ Assets folder in which the react app
│ │ import its resources
@ -17,7 +18,6 @@ The project is structured this way
│ ├── test/ Setup folder for the tests
│ ├── tests/ Other tests + resources
│ ├── utils/ Utilities folder
│ ├── workers/ Webworkers folder
│ ├── index.scss Tailwind CSS extends
│ ├── main.tsx Entrypoint for App injection
│ └── vite-env.d.ts Types for .env files

View file

@ -14,10 +14,13 @@
},
"dependencies": {
"@heroicons/react": "^1.0.6",
"interweave": "^13.0.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-svg-pan-zoom": "^3.11.0",
"react-window": "^1.8.7"
"react-window": "^1.8.7",
"sweetalert2": "^11.4.28",
"sweetalert2-react-content": "^5.0.3"
},
"devDependencies": {
"@testing-library/dom": "^8.16.1",

35
pnpm-lock.yaml generated
View file

@ -24,6 +24,7 @@ specifiers:
eslint-plugin-promise: ^6.0.0
eslint-plugin-react: ^7.30.1
eslint-plugin-react-hooks: ^4.6.0
interweave: ^13.0.0
jsdom: ^20.0.0
postcss: ^8.4.14
react: ^18.2.0
@ -31,6 +32,8 @@ specifiers:
react-svg-pan-zoom: ^3.11.0
react-window: ^1.8.7
sass: ^1.54.0
sweetalert2: ^11.4.28
sweetalert2-react-content: ^5.0.3
tailwindcss: ^3.1.7
typescript: ^4.6.4
vite: ^3.0.0
@ -38,10 +41,13 @@ specifiers:
dependencies:
'@heroicons/react': 1.0.6_react@18.2.0
interweave: 13.0.0_react@18.2.0
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-svg-pan-zoom: 3.11.0_react@18.2.0
react-window: 1.8.7_biqbaboplfbrettd7655fr4n2y
sweetalert2: 11.4.28
sweetalert2-react-content: 5.0.3_m2nzudmh75bb5iwknw4em6omxi
devDependencies:
'@testing-library/dom': 8.16.1
@ -1522,6 +1528,10 @@ packages:
engines: {node: '>=6'}
dev: true
/escape-html/1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
dev: false
/escape-string-regexp/1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
@ -2177,6 +2187,15 @@ packages:
side-channel: 1.0.4
dev: true
/interweave/13.0.0_react@18.2.0:
resolution: {integrity: sha512-Mckwj+ix/VtrZu1bRBIIohwrsXj12ZTvJCoYUMZlJmgtvIaQCj0i77eSZ63ckbA1TsPrz2VOvLW9/kTgm5d+mw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
escape-html: 1.0.3
react: 18.2.0
dev: false
/is-bigint/1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
dependencies:
@ -3183,6 +3202,22 @@ packages:
engines: {node: '>= 0.4'}
dev: true
/sweetalert2-react-content/5.0.3_m2nzudmh75bb5iwknw4em6omxi:
resolution: {integrity: sha512-DQXblZn0LHTvmaZquNQncZIE3Ljox85sAKKbXjYlDyFejyOibHwprAVvtQQpAUG3bgvyDUeAOE/BDFcVx6KUow==}
peerDependencies:
react: ^18.0.0
react-dom: ^18.0.0
sweetalert2: ^11.0.0
dependencies:
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
sweetalert2: 11.4.28
dev: false
/sweetalert2/11.4.28:
resolution: {integrity: sha512-leCf8Kc/o+R0LNWmLjWXI7l0roMchEHg6X+XibmfTYaOMvOoHXmoxmegHl0it+8vvvZlPIjzyfM6bYBOKTFnRg==}
dev: false
/symbol-tree/3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
dev: true

View file

@ -5,6 +5,7 @@ import { fetchConfiguration } from '../API/api';
import { IEditorState } from '../../Interfaces/IEditorState';
import { LoadState } from './Load';
import { XPositionReference } from '../../Enums/XPositionReference';
import { DEFAULT_MAINCONTAINER_PROPS } from '../../utils/default';
export function NewEditor(
setEditorState: Dispatch<SetStateAction<IEditorState>>,
@ -17,19 +18,9 @@ export function NewEditor(
const MainContainer = new ContainerModel(
null,
{
id: 'main',
parentId: 'null',
x: 0,
y: 0,
...DEFAULT_MAINCONTAINER_PROPS,
width: Number(configuration.MainContainer.Width),
height: Number(configuration.MainContainer.Height),
isRigidBody: false,
isAnchor: false,
XPositionReference: XPositionReference.Left,
style: {
fillOpacity: 0,
stroke: 'black'
}
height: Number(configuration.MainContainer.Height)
}
);

View file

@ -0,0 +1,21 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ImposePosition } from './AnchorBehaviors';
import { RecalculatePhysics } from './RigidBodyBehaviors';
/**
* Recalculate the position of the container and its neighbors
* Mutate and returns the updated container
* @param container Container to recalculate its positions
* @returns Updated container
*/
export function ApplyBehaviors(container: IContainerModel): IContainerModel {
if (container.properties.isAnchor) {
ImposePosition(container);
}
if (container.properties.isRigidBody) {
RecalculatePhysics(container);
}
return container;
}

View file

@ -6,6 +6,7 @@
* If the contraints fails, an error message will be returned
*/
import Swal from 'sweetalert2';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISizePointer } from '../../../Interfaces/ISizePointer';
@ -126,8 +127,13 @@ export function constraintBodyInsideUnallocatedWidth(
// Check if there is still some space
if (availableWidths.length === 0) {
Swal.fire({
icon: 'error',
title: 'Not enough space!',
text: 'Try to free the parent a little bit!'
});
throw new Error(
'No available space found on the parent container. Try to free the parent a little before placing it inside.'
'No available space found on the parent container. Try to free the parent a bit.'
);
}
@ -152,7 +158,7 @@ export function constraintBodyInsideUnallocatedWidth(
// Check if the container actually fit inside
// It will usually fit if it was alrady fitting
const availableWidthFound = availableWidths.find((width) =>
isFitting(container, width)
isFitting(container.properties.width, width)
);
if (availableWidthFound === undefined) {
@ -163,12 +169,26 @@ export function constraintBodyInsideUnallocatedWidth(
// We want the container to fit automatically inside the available space
// even if it means to resize the container
// The end goal is that the code never show the error message no matter what action is done
// TODO: Actually give an option to not fit and show the error message shown below
const availableWidth = availableWidths[0];
const availableWidth: ISizePointer | undefined = availableWidths.find((width) => {
return isFitting(container.properties.minWidth, width);
});
if (availableWidth === undefined) {
console.warn(`Container ${container.properties.id} cannot fit in any space due to its minimum width being to large. Consequently, its rigid body property is disabled.`);
Swal.fire({
position: 'top-end',
title: `Container ${container.properties.id} cannot fit!`,
text: 'Its rigid body property is now disabled. Change its the minimum width or free the parent container.',
timerProgressBar: true,
showConfirmButton: false,
timer: 5000
});
container.properties.isRigidBody = false;
return container;
}
container.properties.x = availableWidth.x;
container.properties.width = availableWidth.width;
// throw new Error('[constraintBodyInsideUnallocatedWidth] BIGERR: No available space found on the parent container, even though there is some.');
return container;
}
@ -181,6 +201,17 @@ export function constraintBodyInsideUnallocatedWidth(
);
}
/**
* Check if a container can fit inside a size space
* @param container Container to check
* @param sizePointer Size space to check
* @returns
*/
const isFitting = (
containerWidth: number,
sizePointer: ISizePointer
): boolean => containerWidth <= sizePointer.width;
/**
* Get the unallocated widths inside a container
* An allocated width is defined by its the widths of the children that are rigid bodies.
@ -222,10 +253,9 @@ function getAvailableWidths(
// We need to calculate the overlap between the two containers
// We only works with widths meaning in 1D (with lines)
const newUnallocatedWidths = getAvailableWidthsTwoLines(
unallocatedSpace.x,
unallocatedSpace.x + unallocatedSpace.width,
unallocatedSpace,
childX,
childX + childWidth
childWidth
);
// Concat the new list of SizePointer pointing to availables spaces
@ -240,35 +270,48 @@ function getAvailableWidths(
/**
* Returns the unallocated widths between two lines in 1D
* @param unalloctedSpaceLeft left of the first line
* @param unallocatedSpaceRight rigth of the first line
* @param rectLeft left of the second line
* @param rectRight right of the second line
* @param unalloctedSpace unallocated space
* @param rectX left of the second line
* @param rectWidth width of the second line
* @returns Available widths
*/
function getAvailableWidthsTwoLines(
unalloctedSpaceLeft: number,
unallocatedSpaceRight: number,
rectLeft: number,
rectRight: number
unallocatedSpace: ISizePointer,
rectX: number,
rectWidth: number
): ISizePointer[] {
if (unallocatedSpaceRight < rectLeft ||
unalloctedSpaceLeft > rectRight
) {
// object 1 and 2 are not overlapping
return [{
x: unalloctedSpaceLeft,
width: unallocatedSpaceRight - unalloctedSpaceLeft
}];
const unallocatedSpaceRight = unallocatedSpace.x + unallocatedSpace.width;
const rectRight = rectX + rectWidth;
const isNotOverlapping = unallocatedSpaceRight < rectX ||
unallocatedSpace.x > rectRight;
if (isNotOverlapping) {
return [unallocatedSpace];
}
if (rectLeft < unalloctedSpaceLeft && rectRight > unallocatedSpaceRight) {
// object 2 is overlapping full width
const isOverlappingFullWidth = rectX < unallocatedSpace.x && rectRight > unallocatedSpaceRight;
if (isOverlappingFullWidth) {
return [];
}
const isOverlappingOnTheLeft = unallocatedSpace.x >= rectX;
if (isOverlappingOnTheLeft) {
return getAvailableWidthsLeft(unallocatedSpaceRight, rectRight);
}
const isOverlappingOnTheRight = rectRight >= unallocatedSpaceRight;
if (isOverlappingOnTheRight) {
return getAvailableWidthsRight(unallocatedSpace.x, rectX);
}
return getAvailableWidthsMiddle(unallocatedSpace.x, unallocatedSpaceRight, rectX, rectRight);
}
function getAvailableWidthsLeft(unallocatedSpaceRight: number, rectRight: number): ISizePointer[] {
if (unallocatedSpaceRight - rectRight <= 0) {
return [];
}
if (unalloctedSpaceLeft >= rectLeft) {
// object 2 is partially overlapping on the left
return [
{
x: rectRight,
@ -277,36 +320,40 @@ function getAvailableWidthsTwoLines(
];
}
if (rectRight >= unallocatedSpaceRight) {
// object 2 is partially overlapping on the right
function getAvailableWidthsRight(unallocatedSpaceX: number, rectX: number): ISizePointer[] {
if (rectX - unallocatedSpaceX <= 0) {
return [];
}
return [
{
x: unalloctedSpaceLeft,
width: rectRight - unalloctedSpaceLeft
x: unallocatedSpaceX,
width: rectX - unallocatedSpaceX
}
];
}
// object 2 is overlapping in the middle
return [
{
x: unalloctedSpaceLeft,
width: rectLeft - unalloctedSpaceLeft
},
{
function getAvailableWidthsMiddle(
unallocatedSpaceX: number,
unallocatedSpaceRight: number,
rectX: number,
rectRight: number
): ISizePointer[] {
const sizePointers: ISizePointer[] = [];
if (rectX - unallocatedSpaceX > 0) {
sizePointers.push({
x: unallocatedSpaceX,
width: rectX - unallocatedSpaceX
});
}
if (unallocatedSpaceRight - rectRight > 0) {
sizePointers.push({
x: rectRight,
width: unallocatedSpaceRight - rectRight
}
];
});
}
/**
* Check if a container can fit inside a size space
* @param container Container to check
* @param sizePointer Size space to check
* @returns
*/
const isFitting = (
container: IContainerModel,
sizePointer: ISizePointer
): boolean => container.properties.width <= sizePointer.width;
return sizePointers;
}

View file

@ -4,10 +4,10 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor';
import IProperties from '../../Interfaces/IProperties';
import { AddMethod } from '../../Enums/AddMethod';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { XPositionReference } from '../../Enums/XPositionReference';
import { GetDefaultContainerProps, DEFAULTCHILDTYPE_ALLOW_CYCLIC, DEFAULTCHILDTYPE_MAX_DEPTH } from '../../utils/default';
import { ApplyBehaviors } from './Behaviors/Behaviors';
/**
* Select a container
@ -181,12 +181,7 @@ export function AddContainer(
// Set the counter of the object type in order to assign an unique id
const newCounters = Object.assign({}, current.TypeCounters);
if (newCounters[type] === null ||
newCounters[type] === undefined) {
newCounters[type] = 0;
} else {
newCounters[type]++;
}
UpdateCounters(newCounters, type);
const count = newCounters[type];
// Create maincontainer model
@ -203,23 +198,17 @@ export function AddContainer(
let x = containerConfig.DefaultX ?? 0;
const y = containerConfig.DefaultY ?? 0;
const width = containerConfig.Width ?? parentClone.properties.width;
const height = containerConfig.Height ?? parentClone.properties.height;
x = ApplyAddMethod(index, containerConfig, parentClone, x);
const defaultProperties: IProperties = {
id: `${type}-${count}`,
parentId: parentClone.properties.id,
const defaultProperties = GetDefaultContainerProps(
type,
count,
parentClone,
x,
y,
width,
height,
isRigidBody: false,
isAnchor: false,
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
style: containerConfig.Style
};
containerConfig
);
// Create the container
const newContainer = new ContainerModel(
@ -231,6 +220,8 @@ export function AddContainer(
}
);
ApplyBehaviors(newContainer);
// And push it the the parent children
if (index === parentClone.children.length) {
parentClone.children.push(newContainer);
@ -238,9 +229,11 @@ export function AddContainer(
parentClone.children.splice(index, 0, newContainer);
}
InitializeDefaultChild(configuration, containerConfig, newContainer, newCounters);
// Update the state
history.push({
LastAction: 'Add container',
LastAction: `Add ${newContainer.properties.id} in ${parentClone.properties.id}`,
MainContainer: clone,
SelectedContainer: parentClone,
SelectedContainerId: parentClone.properties.id,
@ -250,6 +243,74 @@ export function AddContainer(
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(
configuration: IConfiguration,
containerConfig: IAvailableContainer,
newContainer: ContainerModel,
newCounters: Record<string, number>
): void {
if (containerConfig.DefaultChildType === undefined) {
return;
}
let currentConfig = configuration.AvailableContainers
.find(option => option.Type === containerConfig.DefaultChildType);
let parent = newContainer;
let depth = 0;
const seen = new Set<string>([containerConfig.Type]);
while (currentConfig !== undefined &&
depth <= DEFAULTCHILDTYPE_MAX_DEPTH
) {
if (!DEFAULTCHILDTYPE_ALLOW_CYCLIC && seen.has(currentConfig.Type)) {
return;
}
seen.add(currentConfig.Type);
const x = currentConfig.DefaultX ?? 0;
const y = currentConfig.DefaultY ?? 0;
UpdateCounters(newCounters, currentConfig.Type);
const count = newCounters[currentConfig.Type];
const defaultChildProperties = GetDefaultContainerProps(
currentConfig.Type,
count,
parent,
x,
y,
currentConfig
);
// Create the container
const newChildContainer = new ContainerModel(
parent,
defaultChildProperties,
[],
{
type: currentConfig.Type
}
);
// And push it the the parent children
parent.children.push(newChildContainer);
// iterate
depth++;
parent = newChildContainer;
currentConfig = configuration.AvailableContainers
.find(option => option.Type === (currentConfig as IAvailableContainer).DefaultChildType);
}
}
/**
* Returns a new offset by applying an Add method (append, insert etc.)
* See AddMethod
@ -263,6 +324,7 @@ function ApplyAddMethod(index: number, containerConfig: IAvailableContainer, par
if (index > 0 && (
containerConfig.AddMethod === undefined ||
containerConfig.AddMethod === AddMethod.Append)) {
// Append method (default)
const lastChild: IContainerModel | undefined = parent.children.at(index - 1);
if (lastChild !== undefined) {

View file

@ -3,10 +3,8 @@ import { IContainerModel, ContainerModel } from '../../Interfaces/IContainerMode
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { findContainerById } from '../../utils/itertools';
import { getCurrentHistory } from './Editor';
import { constraintBodyInsideUnallocatedWidth, RecalculatePhysics } from './Behaviors/RigidBodyBehaviors';
import { INPUT_TYPES } from '../Properties/PropertiesInputTypes';
import { ImposePosition } from './Behaviors/AnchorBehaviors';
import { restoreX } from '../SVG/Elements/Container';
import { ApplyBehaviors } from './Behaviors/Behaviors';
/**
* Handled the property change event in the properties form
@ -44,13 +42,7 @@ export function OnPropertyChange(
(container.properties as any)[key] = value;
}
if (container.properties.isAnchor) {
ImposePosition(container);
}
if (container.properties.isRigidBody) {
RecalculatePhysics(container);
}
ApplyBehaviors(container);
history.push({
LastAction: `Change ${key} of ${container.properties.id}`,
@ -117,9 +109,8 @@ export function OnPropertiesSubmit(
submitCSSForm(form, styleProperty, container);
}
if (container.properties.isRigidBody) {
RecalculatePhysics(container);
}
// Apply the behaviors
ApplyBehaviors(container);
history.push({
LastAction: `Change properties of ${container.properties.id}`,
@ -138,8 +129,9 @@ const submitHTMLInput = (
property: string,
form: HTMLFormElement
): void => {
if (INPUT_TYPES[property] !== 'number') {
if (input.type !== 'number') {
(container.properties as any)[property] = input.value;
return;
}
if (property === 'x') {
@ -188,7 +180,7 @@ const submitRadioButtons = (
return;
}
if (INPUT_TYPES[property] === 'number') {
if (radiobutton.type === 'radio') {
(container.properties as any)[property] = Number(radiobutton.value);
return;
}

View file

@ -3,7 +3,6 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
import { getCircularReplacer } from '../../utils/saveload';
import { ID } from '../SVG/SVG';
import { IEditorState } from '../../Interfaces/IEditorState';
import Worker from '../../workers/worker?worker';
export function SaveEditorAsJSON(
history: IHistoryState[],
@ -21,7 +20,7 @@ export function SaveEditorAsJSON(
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (window.Worker) {
// use webworker for the stringify to avoid freezing
const myWorker = new Worker();
const myWorker = new Worker('workers/worker.js');
myWorker.postMessage({ editorState, spaces });
myWorker.onmessage = (event) => {
const data = event.data;

View file

@ -1,5 +1,6 @@
import { Dispatch, SetStateAction } from 'react';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { ENABLE_SHORTCUTS } from '../../utils/default';
export function onKeyDown(
event: KeyboardEvent,
@ -7,6 +8,10 @@ export function onKeyDown(
historyCurrentStep: number,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
if (!ENABLE_SHORTCUTS) {
return;
}
event.preventDefault();
if (event.isComposing || event.keyCode === 229) {
return;

View file

@ -14,10 +14,12 @@ describe.concurrent('Elements sidebar', () => {
properties: {
id: 'main',
parentId: null,
displayedText: 'main',
x: 0,
y: 0,
width: 2000,
height: 100,
minWidth: 1,
XPositionReference: XPositionReference.Left,
isRigidBody: false,
isAnchor: false
@ -46,10 +48,12 @@ describe.concurrent('Elements sidebar', () => {
properties: {
id: 'main',
parentId: '',
displayedText: 'main',
x: 0,
y: 0,
width: 2000,
height: 100,
minWidth: 1,
isRigidBody: false,
isAnchor: false,
XPositionReference: XPositionReference.Left
@ -104,8 +108,10 @@ describe.concurrent('Elements sidebar', () => {
properties: {
id: 'main',
parentId: '',
displayedText: 'main',
x: 0,
y: 0,
minWidth: 1,
width: 2000,
height: 100,
XPositionReference: XPositionReference.Left,
@ -122,8 +128,10 @@ describe.concurrent('Elements sidebar', () => {
properties: {
id: 'child-1',
parentId: 'main',
displayedText: 'child-1',
x: 0,
y: 0,
minWidth: 1,
width: 0,
height: 0,
isRigidBody: false,
@ -141,8 +149,10 @@ describe.concurrent('Elements sidebar', () => {
properties: {
id: 'child-2',
parentId: 'main',
displayedText: 'child-2',
x: 0,
y: 0,
minWidth: 1,
width: 0,
height: 0,
XPositionReference: XPositionReference.Left,
@ -180,8 +190,10 @@ describe.concurrent('Elements sidebar', () => {
properties: {
id: 'main',
parentId: '',
displayedText: 'main',
x: 0,
y: 0,
minWidth: 1,
width: 2000,
height: 100,
XPositionReference: XPositionReference.Left,
@ -197,8 +209,10 @@ describe.concurrent('Elements sidebar', () => {
properties: {
id: 'child-1',
parentId: 'main',
displayedText: 'child-1',
x: 0,
y: 0,
minWidth: 1,
width: 0,
height: 0,
XPositionReference: XPositionReference.Left,

View file

@ -83,7 +83,9 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
const container = containers[index];
const depth: number = getDepth(container);
const key = container.properties.id.toString();
const text = '|\t'.repeat(depth) + key;
const text = container.properties.displayedText === key
? `${'|\t'.repeat(depth)} ${key}`
: `${'|\t'.repeat(depth)} ${container.properties.displayedText} (${key})`;
const selectedClass: string = props.SelectedContainer !== undefined &&
props.SelectedContainer !== null &&
props.SelectedContainer.properties.id === container.properties.id
@ -116,7 +118,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
</div>
<div ref={elementRef} className='h-96 text-gray-800'>
<List
className='List'
className='List divide-y divide-black'
itemCount={containers.length}
itemSize={35}
height={384}

View file

@ -11,6 +11,7 @@ interface IInputGroupProps {
checked?: boolean
defaultValue?: string
defaultChecked?: boolean
min?: number
isDisabled?: boolean
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
@ -42,6 +43,7 @@ export const InputGroup: React.FunctionComponent<IInputGroupProps> = (props) =>
checked={props.checked}
defaultChecked={props.defaultChecked}
onChange={props.onChange}
min={props.min}
disabled={props.isDisabled}
/>
</>;

View file

@ -52,6 +52,15 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
value={props.properties.parentId?.toString()}
isDisabled={true}
/>
<InputGroup
labelText='Displayed text'
inputKey='displayedText'
labelClassName=''
inputClassName=''
type='string'
value={props.properties.displayedText?.toString()}
onChange={(event) => props.onChange('displayedText', event.target.value)}
/>
<InputGroup
labelText='x'
inputKey='x'
@ -70,12 +79,23 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
value={props.properties.y.toString()}
onChange={(event) => props.onChange('y', Number(event.target.value))}
/>
<InputGroup
labelText='Minimum width'
inputKey='minWidth'
labelClassName=''
inputClassName=''
type='number'
min={1}
value={props.properties.minWidth.toString()}
onChange={(event) => props.onChange('minWidth', Number(event.target.value))}
/>
<InputGroup
labelText='Width'
inputKey='width'
labelClassName=''
inputClassName=''
type='number'
min={props.properties.minWidth}
value={props.properties.width.toString()}
onChange={(event) => props.onChange('width', Number(event.target.value))}
/>
@ -85,6 +105,7 @@ const DynamicForm: React.FunctionComponent<IDynamicFormProps> = (props) => {
labelClassName=''
inputClassName=''
type='number'
min={0}
value={props.properties.height.toString()}
onChange={(event) => props.onChange('height', Number(event.target.value))}
/>

View file

@ -23,10 +23,12 @@ describe.concurrent('Properties', () => {
const prop: IProperties = {
id: 'stuff',
parentId: 'parentId',
displayedText: 'stuff',
x: 1,
y: 1,
width: 1,
height: 1,
minWidth: 1,
XPositionReference: XPositionReference.Left,
isRigidBody: false,
isAnchor: false

View file

@ -1,9 +0,0 @@
export const INPUT_TYPES: Record<string, string> = {
x: 'number',
y: 'number',
width: 'number',
height: 'number',
isRigidBody: 'checkbox',
isAnchor: 'checkbox',
XPositionReference: 'number'
};

View file

@ -52,6 +52,14 @@ const StaticForm: React.FunctionComponent<IStaticFormProps> = (props) => {
defaultValue={props.properties.parentId?.toString()}
isDisabled={true}
/>
<InputGroup
labelText='Displayed text'
inputKey='displayedText'
labelClassName=''
inputClassName=''
type='string'
defaultValue={props.properties.displayedText?.toString()}
/>
<InputGroup
labelText='x'
inputKey='x'
@ -68,12 +76,22 @@ const StaticForm: React.FunctionComponent<IStaticFormProps> = (props) => {
type='number'
defaultValue={props.properties.y.toString()}
/>
<InputGroup
labelText='Minimum width'
inputKey='minWidth'
labelClassName=''
inputClassName=''
type='number'
min={0}
defaultValue={props.properties.minWidth.toString()}
/>
<InputGroup
labelText='Width'
inputKey='width'
labelClassName=''
inputClassName=''
type='number'
min={props.properties.minWidth}
defaultValue={props.properties.width.toString()}
/>
<InputGroup
@ -82,6 +100,7 @@ const StaticForm: React.FunctionComponent<IStaticFormProps> = (props) => {
labelClassName=''
inputClassName=''
type='number'
min={1}
defaultValue={props.properties.height.toString()}
/>
<InputGroup

View file

@ -1,9 +1,11 @@
import * as React from 'react';
import { Interweave, Node } from 'interweave';
import { XPositionReference } from '../../../Enums/XPositionReference';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN } from '../../../utils/default';
import { DIMENSION_MARGIN, SHOW_CHILDREN_DIMENSIONS, SHOW_PARENT_DIMENSION, SHOW_TEXT } from '../../../utils/default';
import { getDepth } from '../../../utils/itertools';
import { Dimension } from './Dimension';
import IProperties from '../../../Interfaces/IProperties';
interface IContainerProps {
model: IContainerModel
@ -33,6 +35,14 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
props.model.properties.style
);
const svg = (props.model.properties.customSVG != null)
? CreateReactCustomSVG(props.model.properties.customSVG, props.model.properties)
: (<rect
width={props.model.properties.width}
height={props.model.properties.height}
style={style}
>
</rect>);
// Dimension props
const depth = getDepth(props.model);
const dimensionMargin = DIMENSION_MARGIN * (depth + 1);
@ -44,7 +54,7 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
const text = (props.model.properties.width ?? 0).toString();
let dimensionChildren: JSX.Element | null = null;
if (props.model.children.length > 1) {
if (props.model.children.length > 1 && SHOW_CHILDREN_DIMENSIONS) {
const {
childrenId,
xChildrenStart,
@ -69,7 +79,8 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
transform={transform}
key={`container-${props.model.properties.id}`}
>
<Dimension
{ SHOW_PARENT_DIMENSION
? <Dimension
id={id}
xStart={xStart}
xEnd={xEnd}
@ -78,19 +89,18 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
strokeWidth={strokeWidth}
text={text}
/>
: null
}
{ dimensionChildren }
<rect
width={props.model.properties.width}
height={props.model.properties.height}
style={style}
>
</rect>
<text
{ svg }
{ SHOW_TEXT
? <text
x={xText}
y={yText}
>
{props.model.properties.id}
{props.model.properties.displayedText}
</text>
: null }
{ containersElements }
</g>
);
@ -141,3 +151,63 @@ export function restoreX(x: number, width: number, xPositionReference = XPositio
}
return transformedX;
}
function CreateReactCustomSVG(customSVG: string, props: IProperties): React.ReactNode {
return <Interweave
tagName='g'
disableLineBreaks={true}
content={customSVG}
allowElements={true}
transform={(node, children) => transform(node, children, props)}
/>;
}
function transform(node: HTMLElement, children: Node[], props: IProperties): React.ReactNode {
const supportedTags = ['line', 'path', 'rect'];
if (supportedTags.includes(node.tagName.toLowerCase())) {
const attributes: {[att: string]: string | object | null} = {};
node.getAttributeNames().forEach(attName => {
const attributeValue = node.getAttribute(attName);
if (attributeValue === null) {
attributes[attName] = attributeValue;
return;
}
if (attributeValue.startsWith('{userData.') && attributeValue.endsWith('}')) {
// support for userData
if (props.userData === undefined) {
return undefined;
}
const userDataKey = attributeValue.replace(/userData\./, '');
const prop = Object.entries(props.userData).find(([key]) => `{${key}}` === userDataKey);
if (prop !== undefined) {
attributes[camelize(attName)] = prop[1];
return;
}
}
if (attributeValue.startsWith('{{') && attributeValue.endsWith('}}')) {
// support for object
const stringObject = attributeValue.slice(1, -1);
const object: JSON = JSON.parse(stringObject);
attributes[camelize(attName)] = object;
return;
}
const prop = Object.entries(props).find(([key]) => `{${key}}` === attributeValue);
if (prop !== undefined) {
attributes[camelize(attName)] = prop[1];
return;
}
attributes[camelize(attName)] = attributeValue;
});
return React.createElement(node.tagName.toLowerCase(), attributes, children);
}
return undefined;
}
function camelize(str: string): any {
return str.split('-').map((word, index) => index > 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word).join('');
}

View file

@ -0,0 +1,90 @@
import * as React from 'react';
import { ContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN } from '../../../utils/default';
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
import { transformX } from './Container';
import { Dimension } from './Dimension';
interface IDimensionLayerProps {
roots: ContainerModel | ContainerModel[] | null
}
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
const it = MakeBFSIterator(root);
const dimensions: React.ReactNode[] = [];
let currentDepth = 0;
let min = Infinity;
let max = -Infinity;
let lastY = 0;
for (const { container, depth } of it) {
if (currentDepth !== depth) {
AddNewDimension(currentDepth, min, max, lastY, dimensions);
currentDepth = depth;
min = Infinity;
max = -Infinity;
}
const absoluteX = getAbsolutePosition(container)[0];
const x = transformX(absoluteX, container.properties.width, container.properties.XPositionReference);
lastY = container.properties.y + container.properties.height;
if (x < min) {
min = x;
}
if (x > max) {
max = x;
}
}
AddNewDimension(currentDepth, min, max, lastY, dimensions);
return dimensions;
};
/**
* A layer containing all dimension
* @param props
* @returns
*/
export const DepthDimensionLayer: React.FC<IDimensionLayerProps> = (props: IDimensionLayerProps) => {
let dimensions: React.ReactNode[] = [];
if (Array.isArray(props.roots)) {
props.roots.forEach(child => {
dimensions.concat(getDimensionsNodes(child));
});
} else if (props.roots !== null) {
dimensions = getDimensionsNodes(props.roots);
}
return (
<g>
{ dimensions }
</g>
);
};
function AddNewDimension(currentDepth: number, min: number, max: number, lastY: number, dimensions: React.ReactNode[]): void {
const id = `dim-depth-${currentDepth}`;
const xStart = min;
const xEnd = max;
const y = lastY + (DIMENSION_MARGIN * (currentDepth + 1));
const strokeWidth = 1;
const width = xEnd - xStart;
const text = width.toString();
if (width === 0) {
return;
}
dimensions.push(
<Dimension
key={id}
id={id}
xStart={xStart}
yStart={y}
xEnd={xEnd}
yEnd={y}
strokeWidth={strokeWidth}
text={text} />
);
}

View file

@ -0,0 +1,57 @@
import * as React from 'react';
import { ContainerModel } from '../../../Interfaces/IContainerModel';
import { DIMENSION_MARGIN } from '../../../utils/default';
import { getAbsolutePosition, MakeBFSIterator } from '../../../utils/itertools';
import { Dimension } from './Dimension';
interface IDimensionLayerProps {
roots: ContainerModel | ContainerModel[] | null
}
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
const it = MakeBFSIterator(root);
const dimensions: React.ReactNode[] = [];
for (const { container, depth } of it) {
const width = container.properties.width;
const id = `dim-${container.properties.id}`;
const xStart = getAbsolutePosition(container)[0];
const xEnd = xStart + width;
const y = (container.properties.y + container.properties.height) + (DIMENSION_MARGIN * (depth + 1));
const strokeWidth = 1;
const text = width.toString();
dimensions.push(
<Dimension
key={id}
id={id}
xStart={xStart}
yStart={y}
xEnd={xEnd}
yEnd={y}
strokeWidth={strokeWidth}
text={text}
/>
);
}
return dimensions;
};
/**
* A layer containing all dimension
* @param props
* @returns
*/
export const DimensionLayer: React.FC<IDimensionLayerProps> = (props: IDimensionLayerProps) => {
let dimensions: React.ReactNode[] = [];
if (Array.isArray(props.roots)) {
props.roots.forEach(child => {
dimensions.concat(getDimensionsNodes(child));
});
} else if (props.roots !== null) {
dimensions = getDimensionsNodes(props.roots);
}
return (
<g>
{ dimensions }
</g>
);
};

View file

@ -4,6 +4,9 @@ import { Container } from './Elements/Container';
import { ContainerModel } from '../../Interfaces/IContainerModel';
import { Selector } from './Elements/Selector';
import { BAR_WIDTH } from '../Bar/Bar';
import { DimensionLayer } from './Elements/DimensionLayer';
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
import { SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
interface ISVGProps {
width: number
@ -74,6 +77,11 @@ export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
<svg {...properties}>
{ children }
<Selector selected={props.selected} />
{
SHOW_DIMENSIONS_PER_DEPTH
? <DepthDimensionLayer roots={props.children}/>
: null
}
</svg>
</UncontrolledReactSVGPanZoom>
</div>

View file

@ -5,11 +5,15 @@ import { XPositionReference } from '../Enums/XPositionReference';
/** Model of available container used in application configuration */
export interface IAvailableContainer {
Type: string
Width?: number
Height?: number
DefaultX?: number
DefaultY?: number
Width?: number
Height?: number
MinWidth?: number
AddMethod?: AddMethod
XPositionReference?: XPositionReference
Style: React.CSSProperties
CustomSVG?: string
DefaultChildType?: string
Style?: React.CSSProperties
UserData?: object
}

View file

@ -3,22 +3,70 @@ import { XPositionReference } from '../Enums/XPositionReference';
/**
* Properties of a container
* @property id id of the container
* @property parentId id of the parent container
* @property x horizontal offset of the container
* @property y vertical offset of the container
* @property isRigidBody if true apply rigid body behaviors
* @property isAnchor if true apply anchor behaviors
*/
export default interface IProperties {
/** id of the container */
id: string
/** id of the parent container (null when there is no parent) */
parentId: string | null
/** Text displayed in the container */
displayedText: string
/** horizontal offset */
x: number
/** vertical offset */
y: number
/**
* Minimum width (min=1)
* Allows the container to set isRigidBody to false when it gets squeezed
* by an anchor
*/
minWidth: number
/** width */
width: number
/** height */
height: number
/** true if rigid, false otherwise */
isRigidBody: boolean
/** true if anchor, false otherwise */
isAnchor: boolean
/** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */
XPositionReference: XPositionReference
/**
* (optional)
* Replace a <rect> by a customized "SVG". It is not really an svg but it at least allows
* to draw some patterns that can be bind to the properties of the container
* Use {prop} to bind a property. Use {{ styleProp }} to use an object.
* Example :
* ```
* `<rect width="{width}" height="{height}" style="{style}"></rect>
* <rect width="{width}" height="{height}" stroke="black" fill-opacity="0"></rect>
* <line x1="0" y1="0" x2="{width}" y2="{height}" stroke="black" style='{{ "transform":"scaleY(0.5)"}}'></line>
* <line x1="{width}" y1="0" x2="0" y2="{height}" stroke="black" style='{userData.styleLine}'></line>
* `
* ```
*/
customSVG?: string
/**
* (optional)
* Style of the <rect>
*/
style?: React.CSSProperties
/**
* (optional)
* User data that can be used for data storage or custom SVG
*/
userData?: object
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,436 +0,0 @@
{
"history": [
{
"MainContainer": {
"children": [],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "main",
"TypeCounters": {}
},
{
"LastAction": "Add container",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": 75,
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "main",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Select container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": 75,
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "7",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "2",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "20",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "200",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "2000",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": "20000",
"height": 100,
"isRigidBody": false,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
},
{
"LastAction": "Change property of container Container-0",
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Container-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"isRigidBody": true,
"fillOpacity": 0,
"stroke": "green"
},
"userData": {
"type": "Container"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 2000,
"height": 100,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"SelectedContainerId": "Container-0",
"TypeCounters": {
"Container": 0
}
}
],
"historyCurrentStep": 10,
"configuration": {
"AvailableContainers": [
{
"Type": "Container",
"Width": 75,
"Height": 100,
"Style": {
"fillOpacity": 0,
"stroke": "green"
}
}
],
"AvailableSymbols": [],
"MainContainer": {
"Type": "Container",
"Width": 2000,
"Height": 100,
"Style": {
"fillOpacity": 0,
"stroke": "black"
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,182 +0,0 @@
{
"isSidebarOpen": true,
"isElementsSidebarOpen": false,
"isHistoryOpen": false,
"configuration": {
"AvailableContainers": [
{
"Type": "Chassis",
"Width": 500,
"Style": {
"fillOpacity": 0,
"borderWidth": 2,
"stroke": "red"
}
},
{
"Type": "Trou",
"Width": 300,
"Style": {
"fillOpacity": 0,
"borderWidth": 2,
"stroke": "green"
}
},
{
"Type": "Montant",
"Width": 100,
"Style": {
"fillOpacity": 0,
"borderWidth": 2,
"stroke": "blue",
"transform": "translateX(-50%)",
"transformOrigin": "center",
"transformBox": "fill-box"
}
}
],
"AvailableSymbols": [
{
"Height": 0,
"Image": {
"Base64Image": null,
"Name": null,
"Svg": null,
"Url": "https://www.manutan.fr/img/S/GRP/ST/AIG3930272.jpg"
},
"Name": "Poteau structure",
"Width": 0,
"XPositionReference": 1
},
{
"Height": 0,
"Image": {
"Base64Image": null,
"Name": null,
"Svg": null,
"Url": "https://e7.pngegg.com/pngimages/647/127/png-clipart-svg-working-group-information-world-wide-web-internet-structure.png"
},
"Name": "Joint de structure",
"Width": 0,
"XPositionReference": 0
}
],
"MainContainer": {
"Height": 200,
"Width": 1000
}
},
"history": [
{
"MainContainer": {
"children": [],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 1000,
"height": 200,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"TypeCounters": {}
},
{
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Chassis-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": 500,
"height": 200,
"fillOpacity": 0,
"borderWidth": 2,
"stroke": "red"
},
"userData": {
"type": "Chassis"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 1000,
"height": 200,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"TypeCounters": {
"Chassis": 0
},
"SelectedContainerId": "main"
},
{
"MainContainer": {
"children": [
{
"children": [],
"properties": {
"id": "Chassis-0",
"parentId": "main",
"x": 0,
"y": 0,
"width": 500,
"height": 200,
"fillOpacity": 0,
"borderWidth": 2,
"stroke": "red"
},
"userData": {
"type": "Chassis"
}
},
{
"children": [],
"properties": {
"id": "Chassis-1",
"parentId": "main",
"x": 500,
"y": 0,
"width": 500,
"height": 200,
"fillOpacity": 0,
"borderWidth": 2,
"stroke": "red"
},
"userData": {
"type": "Chassis"
}
}
],
"properties": {
"id": "main",
"parentId": "null",
"x": 0,
"y": 0,
"width": 1000,
"height": 200,
"fillOpacity": 0,
"stroke": "black"
},
"userData": {}
},
"TypeCounters": {
"Chassis": 1
},
"SelectedContainerId": "main"
}
],
"historyCurrentStep": 2
}

View file

@ -1,7 +1,28 @@
import { XPositionReference } from '../Enums/XPositionReference';
import { IAvailableContainer } from '../Interfaces/IAvailableContainer';
import { IConfiguration } from '../Interfaces/IConfiguration';
import { IContainerModel } from '../Interfaces/IContainerModel';
import IProperties from '../Interfaces/IProperties';
/// CONTAINRE DEFAULTS ///
export const SHOW_TEXT = true;
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;
export const DEFAULTCHILDTYPE_MAX_DEPTH = 10;
/// DIMENSIONS DEFAULTS ///
export const SHOW_PARENT_DIMENSION = true;
export const SHOW_CHILDREN_DIMENSIONS = false;
export const SHOW_DIMENSIONS_PER_DEPTH = true;
export const DIMENSION_MARGIN = 50;
export const NOTCHES_LENGTH = 4;
/// EDITOR DEFAULTS ///
export const ENABLE_SHORTCUTS = true;
export const MAX_HISTORY = 200;
export const DEFAULT_CONFIG: IConfiguration = {
AvailableContainers: [
{
@ -29,8 +50,10 @@ export const DEFAULT_CONFIG: IConfiguration = {
export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
id: 'main',
parentId: 'null',
displayedText: 'main',
x: 0,
y: 0,
minWidth: 1,
width: Number(DEFAULT_CONFIG.MainContainer.Width),
height: Number(DEFAULT_CONFIG.MainContainer.Height),
isRigidBody: false,
@ -42,7 +65,26 @@ export const DEFAULT_MAINCONTAINER_PROPS: IProperties = {
}
};
export const DIMENSION_MARGIN = 50;
export const NOTCHES_LENGTH = 4;
export const MAX_HISTORY = 200;
export const GetDefaultContainerProps = (
type: string,
typeCount: number,
parent: IContainerModel,
x: number,
y: number,
containerConfig: IAvailableContainer
): IProperties => ({
id: `${type}-${typeCount}`,
parentId: parent.properties.id,
displayedText: `${type}-${typeCount}`,
x,
y,
width: containerConfig.Width ?? containerConfig.MinWidth ?? parent.properties.width,
height: containerConfig.Height ?? parent.properties.height,
isRigidBody: false, // set this to true to replicate Florian's project
isAnchor: false,
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
minWidth: containerConfig.MinWidth ?? 0,
customSVG: containerConfig.CustomSVG,
style: structuredClone(containerConfig.Style),
userData: structuredClone(containerConfig.UserData)
});

View file

@ -22,6 +22,34 @@ export function * MakeIterator(root: IContainerModel): Generator<IContainerModel
}
}
export interface ContainerAndDepth {
container: IContainerModel
depth: number
}
/**
* Returns a Generator iterating of over the children depth-first
*/
export function * MakeBFSIterator(root: IContainerModel): Generator<ContainerAndDepth, void, unknown> {
const queue: IContainerModel[] = [root];
let depth = 0;
while (queue.length > 0) {
let levelSize = queue.length;
while (levelSize-- !== 0) {
const container = queue.shift() as IContainerModel;
yield {
container,
depth
};
for (let i = container.children.length - 1; i >= 0; i--) {
const child = container.children[i];
queue.push(child);
}
}
depth++;
}
}
/**
* Returns the depth of the container
* @returns The depth of the container

View file

@ -42,7 +42,6 @@ export function Revive(editorState: IEditorState): void {
}
export const getCircularReplacer = (): (key: any, value: object | null) => object | null | undefined => {
const seen = new WeakSet();
return (key: any, value: object | null) => {
if (key === 'parent') {
return;
@ -52,12 +51,6 @@ export const getCircularReplacer = (): (key: any, value: object | null) => objec
return;
}
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return;
}
seen.add(value);
}
return value;
};
};

View file

@ -54,12 +54,13 @@ const GetSVGLayoutConfiguration = () => {
{
Type: 'Chassis',
Width: 500,
MinWidth: 200,
DefaultChildType: 'Trou',
Style: {
fillOpacity: 1,
borderWidth: 2,
strokeWidth: 2,
stroke: 'red',
fill: '#78350F',
stroke: 'red'
fill: '#d3c9b7',
}
},
{
@ -68,20 +69,33 @@ const GetSVGLayoutConfiguration = () => {
DefaultY: 10,
Width: 480,
Height: 180,
DefaultChildType: 'Remplissage',
Style: {
fillOpacity: 1,
borderWidth: 2,
strokeWidth: 2,
stroke: 'green',
fill: 'white'
}
},
{
Type: 'Remplissage',
CustomSVG: `
<rect width="{width}" height="{height}" style="{style}"></rect>
<rect width="{width}" height="{height}" stroke="black" fill-opacity="0"></rect>
<line x1="0" y1="0" x2="{width}" y2="{height}" stroke="black" style='{{ "transform":"scaleY(0.5)"}}'></line>
<line x1="{width}" y1="0" x2="0" y2="{height}" stroke="black" style='{userData.styleLine}'></line>
`
,
Style: {
fillOpacity: 1,
borderWidth: 2,
stroke: '#bfdbfe',
strokeWidth: 1,
fill: '#bfdbfe'
},
UserData: {
styleLine: {
transform: "scaleY(0.5) translateY(100%)",
transformBox: "fill-box"
}
}
},
{
@ -90,7 +104,7 @@ const GetSVGLayoutConfiguration = () => {
XPositionReference: 1,
Style: {
fillOpacity: 0,
borderWidth: 2,
strokeWidth: 2,
stroke: '#713f12',
fill: '#713f12',
}

View file

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