Merge branch 'dev'
This commit is contained in:
commit
36526dcb27
29 changed files with 555 additions and 132 deletions
37
README.md
37
README.md
|
@ -13,8 +13,9 @@ An svg layout designer.
|
||||||
Requierements :
|
Requierements :
|
||||||
- NodeJS
|
- NodeJS
|
||||||
- npm
|
- npm
|
||||||
- pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory)
|
|
||||||
- Chrome > 98
|
- Chrome > 98
|
||||||
|
- pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory)
|
||||||
|
- [`git-lfs`](https://git-lfs.github.com/) (in order to clone the documentation)
|
||||||
|
|
||||||
# Developping
|
# Developping
|
||||||
|
|
||||||
|
@ -22,9 +23,6 @@ Run `npm ci`
|
||||||
|
|
||||||
Run `npm run dev`
|
Run `npm run dev`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Deploy
|
# Deploy
|
||||||
|
|
||||||
Run `npm ci`
|
Run `npm ci`
|
||||||
|
@ -73,3 +71,34 @@ bun run http.js
|
||||||
The web server will be running at `http://localhost:5000`
|
The web server will be running at `http://localhost:5000`
|
||||||
|
|
||||||
Configure the file `.env.development` with the url
|
Configure the file `.env.development` with the url
|
||||||
|
|
||||||
|
# Recommanded tools
|
||||||
|
|
||||||
|
- [VSCode](https://code.visualstudio.com/)
|
||||||
|
- [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
|
||||||
|
- [vscode-tailwindcss](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)
|
||||||
|
- [vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||||
|
|
||||||
|
# Setup debuggin with chrome
|
||||||
|
|
||||||
|
Inside `.vscode/settings.json`, set the following :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost:5173",
|
||||||
|
"webRoot": "${workspaceFolder}",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Change the `url` to the dev server url. Set the `runtimeExecutable` to you favorite chromium browser.
|
|
@ -1,5 +1,4 @@
|
||||||
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
import { IConfiguration } from '../../Interfaces/IConfiguration';
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
|
||||||
import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest';
|
import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest';
|
||||||
import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse';
|
import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse';
|
||||||
import { GetCircularReplacer } from '../../utils/saveload';
|
import { GetCircularReplacer } from '../../utils/saveload';
|
||||||
|
@ -42,6 +41,10 @@ export async function SetContainerList(request: ISetContainerListRequest): Promi
|
||||||
if (window.fetch) {
|
if (window.fetch) {
|
||||||
return await fetch(url, {
|
return await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
headers: new Headers({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}),
|
||||||
body: dataParsed
|
body: dataParsed
|
||||||
})
|
})
|
||||||
.then(async(response) =>
|
.then(async(response) =>
|
||||||
|
@ -56,6 +59,7 @@ export async function SetContainerList(request: ISetContainerListRequest): Promi
|
||||||
resolve(JSON.parse(this.responseText));
|
resolve(JSON.parse(this.responseText));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
xhr.setRequestHeader('Content-type', 'application/json');
|
||||||
xhr.send(dataParsed);
|
xhr.send(dataParsed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export function NewEditor(
|
||||||
setEditorState(editorState);
|
setEditorState(editorState);
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
console.warn('[NewEditor] Could not fetch resource from API. Using default.', error);
|
console.debug('[NewEditor] Could not fetch resource from API. Using default.', error);
|
||||||
setLoaded(true);
|
setLoaded(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
import './App.scss';
|
|
||||||
import { MainMenu } from '../MainMenu/MainMenu';
|
import { MainMenu } from '../MainMenu/MainMenu';
|
||||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { Editor } from '../Editor/Editor';
|
import { Editor } from '../Editor/Editor';
|
||||||
|
@ -78,7 +77,7 @@ export function App(props: IAppProps): JSX.Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='bg-blue-100 h-full w-full'>
|
<div className='mainmenu-bg'>
|
||||||
<MainMenu
|
<MainMenu
|
||||||
newEditor={() => NewEditor(
|
newEditor={() => NewEditor(
|
||||||
setEditorState, setLoaded
|
setEditorState, setLoaded
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const BAR_WIDTH = 64; // 4rem
|
||||||
|
|
||||||
export function Bar(props: IBarProps): JSX.Element {
|
export function Bar(props: IBarProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className='fixed z-20 flex flex-col top-0 left-0 h-full w-16 bg-slate-100'>
|
<div className='bar'>
|
||||||
<BarIcon
|
<BarIcon
|
||||||
isActive={props.isSidebarOpen}
|
isActive={props.isSidebarOpen}
|
||||||
title='Components'
|
title='Components'
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { PropertyType } from '../../Enums/PropertyType';
|
||||||
import { XPositionReference } from '../../Enums/XPositionReference';
|
import { XPositionReference } from '../../Enums/XPositionReference';
|
||||||
import { IContainerProperties } from '../../Interfaces/IContainerProperties';
|
import { IContainerProperties } from '../../Interfaces/IContainerProperties';
|
||||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
import { SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, SHOW_SELF_DIMENSIONS } from '../../utils/default';
|
||||||
import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, RestoreX, TransformX } from '../../utils/svg';
|
import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, 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';
|
||||||
|
@ -34,7 +35,7 @@ function GetCSSInputs(properties: IContainerProperties,
|
||||||
|
|
||||||
export function ContainerForm(props: IContainerFormProps): JSX.Element {
|
export function ContainerForm(props: IContainerFormProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<div className='grid grid-cols-2 gap-y-4'>
|
<div className='grid grid-cols-2 gap-y-4 items-center'>
|
||||||
<InputGroup
|
<InputGroup
|
||||||
labelText='Name'
|
labelText='Name'
|
||||||
inputKey='id'
|
inputKey='id'
|
||||||
|
@ -119,6 +120,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
|
||||||
inputClassName=''
|
inputClassName=''
|
||||||
type='number'
|
type='number'
|
||||||
min={props.properties.minWidth}
|
min={props.properties.minWidth}
|
||||||
|
max={props.properties.maxWidth}
|
||||||
value={(RemoveWidthMargin(props.properties.width, props.properties.margin.left, props.properties.margin.right)).toString()}
|
value={(RemoveWidthMargin(props.properties.width, props.properties.margin.left, props.properties.margin.right)).toString()}
|
||||||
onChange={(event) => props.onChange('width', ApplyWidthMargin(Number(event.target.value), props.properties.margin.left, props.properties.margin.right))}
|
onChange={(event) => props.onChange('width', ApplyWidthMargin(Number(event.target.value), props.properties.margin.left, props.properties.margin.right))}
|
||||||
isDisabled={props.properties.isFlex} />
|
isDisabled={props.properties.isFlex} />
|
||||||
|
@ -227,6 +229,49 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
|
||||||
value={props.properties.linkedSymbolId ?? ''}
|
value={props.properties.linkedSymbolId ?? ''}
|
||||||
onChange={(event) => props.onChange('linkedSymbolId', event.target.value)} />
|
onChange={(event) => props.onChange('linkedSymbolId', event.target.value)} />
|
||||||
{GetCSSInputs(props.properties, props.onChange)}
|
{GetCSSInputs(props.properties, props.onChange)}
|
||||||
|
{
|
||||||
|
SHOW_SELF_DIMENSIONS &&
|
||||||
|
<InputGroup
|
||||||
|
labelText='Show dimension'
|
||||||
|
inputKey='showSelfDimensions'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='checkbox'
|
||||||
|
checked={props.properties.showSelfDimensions}
|
||||||
|
onChange={(event) => props.onChange('showSelfDimensions', event.target.checked)} />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
SHOW_CHILDREN_DIMENSIONS &&
|
||||||
|
<InputGroup
|
||||||
|
labelText='Show overall dimension of its children'
|
||||||
|
inputKey='showChildrenDimensions'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='checkbox'
|
||||||
|
checked={props.properties.showChildrenDimensions}
|
||||||
|
onChange={(event) => props.onChange('showChildrenDimensions', event.target.checked)} />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
SHOW_BORROWER_DIMENSIONS &&
|
||||||
|
<>
|
||||||
|
<InputGroup
|
||||||
|
labelText='Mark the position'
|
||||||
|
inputKey='markPositionToDimensionBorrower'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='checkbox'
|
||||||
|
checked={props.properties.markPositionToDimensionBorrower}
|
||||||
|
onChange={(event) => props.onChange('markPositionToDimensionBorrower', event.target.checked)} />
|
||||||
|
<InputGroup
|
||||||
|
labelText='Show dimension with marked children'
|
||||||
|
inputKey='isDimensionBorrower'
|
||||||
|
labelClassName=''
|
||||||
|
inputClassName=''
|
||||||
|
type='checkbox'
|
||||||
|
checked={props.properties.isDimensionBorrower}
|
||||||
|
onChange={(event) => props.onChange('isDimensionBorrower', event.target.checked)} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ 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, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors';
|
||||||
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
import { ApplyMargin, TransformX } from '../../../utils/svg';
|
import { ApplyMargin, TransformX } from '../../../utils/svg';
|
||||||
|
@ -83,7 +83,7 @@ export function DeleteContainer(
|
||||||
throw new Error('[DeleteContainer] Could not find container among parent\'s children');
|
throw new Error('[DeleteContainer] Could not find container among parent\'s children');
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyBehaviorsOnSiblings(container, current.symbols);
|
ApplyBehaviorsOnSiblingsChildren(container, current.symbols);
|
||||||
|
|
||||||
// Select the previous container
|
// Select the previous container
|
||||||
// or select the one above
|
// or select the one above
|
||||||
|
@ -234,8 +234,8 @@ export function AddContainers(
|
||||||
const right: number = containerConfig.Margin?.right ?? 0;
|
const right: number = containerConfig.Margin?.right ?? 0;
|
||||||
|
|
||||||
// Default coordinates
|
// Default coordinates
|
||||||
let x = containerConfig.DefaultX ?? 0;
|
let x = containerConfig.X ?? 0;
|
||||||
let y = containerConfig.DefaultY ?? 0;
|
let y = containerConfig.Y ?? 0;
|
||||||
let width = containerConfig.Width ?? containerConfig.MaxWidth ?? containerConfig.MinWidth ?? parentClone.properties.width;
|
let width = containerConfig.Width ?? containerConfig.MaxWidth ?? containerConfig.MinWidth ?? parentClone.properties.width;
|
||||||
let height = containerConfig.Height ?? parentClone.properties.height;
|
let height = containerConfig.Height ?? parentClone.properties.height;
|
||||||
|
|
||||||
|
@ -276,6 +276,9 @@ export function AddContainers(
|
||||||
parentClone.children.splice(index, 0, newContainer);
|
parentClone.children.splice(index, 0, newContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort the parent children by x
|
||||||
|
UpdateParentChildrenList(parentClone);
|
||||||
|
|
||||||
/// Handle behaviors here ///
|
/// Handle behaviors here ///
|
||||||
|
|
||||||
// Initialize default children of the container
|
// Initialize default children of the container
|
||||||
|
@ -285,10 +288,7 @@ export function AddContainers(
|
||||||
ApplyBehaviors(newContainer, current.symbols);
|
ApplyBehaviors(newContainer, current.symbols);
|
||||||
|
|
||||||
// Then, apply the behaviors on its siblings (mostly for flex)
|
// Then, apply the behaviors on its siblings (mostly for flex)
|
||||||
ApplyBehaviorsOnSiblings(newContainer, current.symbols);
|
ApplyBehaviorsOnSiblingsChildren(newContainer, current.symbols);
|
||||||
|
|
||||||
// Sort the parent children by x
|
|
||||||
UpdateParentChildrenList(parentClone);
|
|
||||||
|
|
||||||
// Add to the list of container id for logging purpose
|
// Add to the list of container id for logging purpose
|
||||||
containerIds.push(newContainer.properties.id);
|
containerIds.push(newContainer.properties.id);
|
||||||
|
@ -403,8 +403,8 @@ function InitializeDefaultChild(
|
||||||
const bottom: number = currentConfig.Margin?.bottom ?? 0;
|
const bottom: number = currentConfig.Margin?.bottom ?? 0;
|
||||||
const top: number = currentConfig.Margin?.top ?? 0;
|
const top: number = currentConfig.Margin?.top ?? 0;
|
||||||
const right: number = currentConfig.Margin?.right ?? 0;
|
const right: number = currentConfig.Margin?.right ?? 0;
|
||||||
let x = currentConfig.DefaultX ?? 0;
|
let x = currentConfig.X ?? 0;
|
||||||
let y = currentConfig.DefaultY ?? 0;
|
let y = currentConfig.Y ?? 0;
|
||||||
let width = currentConfig.Width ?? currentConfig.MaxWidth ?? currentConfig.MinWidth ?? parent.properties.width;
|
let width = currentConfig.Width ?? currentConfig.MaxWidth ?? currentConfig.MinWidth ?? parent.properties.width;
|
||||||
let height = currentConfig.Height ?? parent.properties.height;
|
let height = currentConfig.Height ?? parent.properties.height;
|
||||||
|
|
||||||
|
@ -547,7 +547,7 @@ function SetContainer(
|
||||||
ApplyBehaviors(container, symbols);
|
ApplyBehaviors(container, symbols);
|
||||||
|
|
||||||
// Apply special behaviors on siblings
|
// Apply special behaviors on siblings
|
||||||
ApplyBehaviorsOnSiblings(container, symbols);
|
ApplyBehaviorsOnSiblingsChildren(container, symbols);
|
||||||
|
|
||||||
// sort the children list by their position
|
// sort the children list by their position
|
||||||
UpdateParentChildrenList(container.parent);
|
UpdateParentChildrenList(container.parent);
|
||||||
|
@ -626,19 +626,3 @@ function LinkSymbol(
|
||||||
|
|
||||||
newSymbol.linkedContainers.add(containerId);
|
newSymbol.linkedContainers.add(containerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterate over the siblings of newContainer and apply the behaviors
|
|
||||||
* @param newContainer
|
|
||||||
* @param symbols
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function ApplyBehaviorsOnSiblings(newContainer: ContainerModel, symbols: Map<string, ISymbolModel>): void {
|
|
||||||
if (newContainer.parent === null || newContainer.parent === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
newContainer.parent.children
|
|
||||||
.filter(container => newContainer !== container)
|
|
||||||
.forEach(container => ApplyBehaviors(container, symbols));
|
|
||||||
}
|
|
||||||
|
|
|
@ -68,7 +68,7 @@ function HandleSetContainerList(
|
||||||
setNewHistory(
|
setNewHistory(
|
||||||
AddContainers(
|
AddContainers(
|
||||||
selectedContainer.children.length,
|
selectedContainer.children.length,
|
||||||
response.Containers.map(container => container.properties.type),
|
response.Containers.map(container => container.Type),
|
||||||
selectedContainer.properties.id,
|
selectedContainer.properties.id,
|
||||||
configuration,
|
configuration,
|
||||||
history,
|
history,
|
||||||
|
@ -104,28 +104,31 @@ function HandleReplace(
|
||||||
|
|
||||||
const index = selectedContainer.parent.children.indexOf(selectedContainer);
|
const index = selectedContainer.parent.children.indexOf(selectedContainer);
|
||||||
|
|
||||||
const types = response.Containers.map(container => container.properties.type);
|
const types = response.Containers.map(container => container.Type);
|
||||||
const newHistoryBeforeDelete = AddContainers(
|
|
||||||
index + 1,
|
const newHistoryAfterDelete = DeleteContainer(
|
||||||
types,
|
selectedContainer.properties.id,
|
||||||
selectedContainer.properties.parentId,
|
|
||||||
configuration,
|
|
||||||
history,
|
history,
|
||||||
historyCurrentStep
|
historyCurrentStep
|
||||||
);
|
);
|
||||||
|
|
||||||
const newHistoryAfterDelete = DeleteContainer(
|
const newHistoryBeforeDelete = AddContainers(
|
||||||
selectedContainer.properties.id,
|
index,
|
||||||
newHistoryBeforeDelete,
|
types,
|
||||||
newHistoryBeforeDelete.length - 1
|
selectedContainer.properties.parentId,
|
||||||
|
configuration,
|
||||||
|
newHistoryAfterDelete,
|
||||||
|
newHistoryAfterDelete.length - 1
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove AddContainers from history
|
// Remove AddContainers from history
|
||||||
newHistoryAfterDelete.splice(newHistoryAfterDelete.length - 2, 1);
|
if (import.meta.env.PROD) {
|
||||||
|
newHistoryBeforeDelete.splice(newHistoryBeforeDelete.length - 2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
// Rename the last action by Replace
|
// Rename the last action by Replace
|
||||||
newHistoryAfterDelete[newHistoryAfterDelete.length - 1].lastAction =
|
newHistoryBeforeDelete[newHistoryBeforeDelete.length - 1].lastAction =
|
||||||
`Replace ${selectedContainer.properties.id} by [${types.join(', ')}]`;
|
`Replace ${selectedContainer.properties.id} by [${types.join(', ')}]`;
|
||||||
|
|
||||||
return newHistoryAfterDelete;
|
return newHistoryBeforeDelete;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { Dispatch, SetStateAction } from 'react';
|
|
||||||
import { IConfiguration } from '../../../Interfaces/IConfiguration';
|
import { IConfiguration } from '../../../Interfaces/IConfiguration';
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { IHistoryState } from '../../../Interfaces/IHistoryState';
|
import { IHistoryState } from '../../../Interfaces/IHistoryState';
|
||||||
|
@ -6,7 +5,7 @@ import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
import { GetDefaultSymbolModel } from '../../../utils/default';
|
import { GetDefaultSymbolModel } from '../../../utils/default';
|
||||||
import { FindContainerById } from '../../../utils/itertools';
|
import { FindContainerById } from '../../../utils/itertools';
|
||||||
import { RestoreX } from '../../../utils/svg';
|
import { RestoreX } from '../../../utils/svg';
|
||||||
import { ApplyBehaviors } from '../Behaviors/Behaviors';
|
import { ApplyBehaviors, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors';
|
||||||
import { GetCurrentHistory, UpdateCounters } from '../Editor';
|
import { GetCurrentHistory, UpdateCounters } from '../Editor';
|
||||||
|
|
||||||
export function AddSymbol(
|
export function AddSymbol(
|
||||||
|
@ -150,6 +149,8 @@ export function OnPropertyChange(
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyBehaviors(container, newSymbols);
|
ApplyBehaviors(container, newSymbols);
|
||||||
|
|
||||||
|
ApplyBehaviorsOnSiblingsChildren(container, newSymbols);
|
||||||
});
|
});
|
||||||
|
|
||||||
history.push({
|
history.push({
|
||||||
|
|
|
@ -44,7 +44,7 @@ export function ApplyAnchor(container: IContainerModel): IContainerModel {
|
||||||
* @param containers A list of containers
|
* @param containers A list of containers
|
||||||
* @returns A list of overlapping containers
|
* @returns A list of overlapping containers
|
||||||
*/
|
*/
|
||||||
function GetOverlappingContainers(
|
export function GetOverlappingContainers(
|
||||||
container: IContainerModel,
|
container: IContainerModel,
|
||||||
containers: IContainerModel[]
|
containers: IContainerModel[]
|
||||||
): IContainerModel[] {
|
): IContainerModel[] {
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
|
||||||
import { APPLY_BEHAVIORS_ON_CHILDREN } from '../../../utils/default';
|
import { APPLY_BEHAVIORS_ON_CHILDREN, ENABLE_RIGID, ENABLE_SWAP } from '../../../utils/default';
|
||||||
import { ApplyAnchor } from './AnchorBehaviors';
|
import { ApplyAnchor } from './AnchorBehaviors';
|
||||||
import { Flex } from './FlexBehaviors';
|
import { Flex } from './FlexBehaviors';
|
||||||
import { ApplyRigidBody } from './RigidBodyBehaviors';
|
import { ApplyRigidBody } from './RigidBodyBehaviors';
|
||||||
|
import { ApplySwap } from './SwapBehaviors';
|
||||||
import { ApplySymbol } from './SymbolBehaviors';
|
import { ApplySymbol } from './SymbolBehaviors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -13,17 +14,23 @@ import { ApplySymbol } from './SymbolBehaviors';
|
||||||
* @returns Updated container
|
* @returns Updated container
|
||||||
*/
|
*/
|
||||||
export function ApplyBehaviors(container: IContainerModel, symbols: Map<string, ISymbolModel>): IContainerModel {
|
export function ApplyBehaviors(container: IContainerModel, symbols: Map<string, ISymbolModel>): IContainerModel {
|
||||||
|
const symbol = symbols.get(container.properties.linkedSymbolId);
|
||||||
|
if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
|
||||||
|
ApplySymbol(container, symbol);
|
||||||
|
}
|
||||||
|
|
||||||
if (container.properties.isAnchor) {
|
if (container.properties.isAnchor) {
|
||||||
ApplyAnchor(container);
|
ApplyAnchor(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ENABLE_SWAP) {
|
||||||
|
ApplySwap(container);
|
||||||
|
}
|
||||||
|
|
||||||
Flex(container);
|
Flex(container);
|
||||||
|
|
||||||
|
if (ENABLE_RIGID) {
|
||||||
ApplyRigidBody(container);
|
ApplyRigidBody(container);
|
||||||
|
|
||||||
const symbol = symbols.get(container.properties.linkedSymbolId);
|
|
||||||
if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
|
|
||||||
ApplySymbol(container, symbol);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (APPLY_BEHAVIORS_ON_CHILDREN) {
|
if (APPLY_BEHAVIORS_ON_CHILDREN) {
|
||||||
|
@ -35,3 +42,26 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map<string,
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterate over the siblings of newContainer and apply the behaviors
|
||||||
|
* @param newContainer
|
||||||
|
* @param symbols
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function ApplyBehaviorsOnSiblingsChildren(newContainer: IContainerModel, symbols: Map<string, ISymbolModel>): void {
|
||||||
|
if (newContainer.parent === null || newContainer.parent === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newContainer.parent.children
|
||||||
|
.forEach((container: IContainerModel) => {
|
||||||
|
if (container === newContainer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of container.children) {
|
||||||
|
ApplyBehaviors(child, symbols);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { Simplex } from '../../../utils/simplex';
|
import { Simplex } from '../../../utils/simplex';
|
||||||
import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg';
|
import { ApplyWidthMargin, ApplyXMargin } from '../../../utils/svg';
|
||||||
|
@ -25,34 +26,40 @@ export function Flex(container: IContainerModel): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply flex to the group
|
||||||
|
* @param flexibleGroup Group that contains a list of flexible containers
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
function FlexGroup(flexibleGroup: IFlexibleGroup): void {
|
function FlexGroup(flexibleGroup: IFlexibleGroup): void {
|
||||||
const children = flexibleGroup.group;
|
const children = flexibleGroup.group;
|
||||||
const flexibleContainers = children
|
const {
|
||||||
.filter(sibling => sibling.properties.isFlex);
|
flexibleContainers,
|
||||||
|
nonFlexibleContainers
|
||||||
|
} = SeparateFlexibleContainers(children);
|
||||||
|
|
||||||
|
if (flexibleContainers.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const minWidths = flexibleContainers
|
const minWidths = flexibleContainers
|
||||||
.map(sibling => sibling.properties.minWidth);
|
.map(sibling => sibling.properties.minWidth);
|
||||||
|
|
||||||
const fixedWidth = children
|
const fixedWidth = nonFlexibleContainers
|
||||||
.filter(sibling => !sibling.properties.isFlex)
|
|
||||||
.map(sibling => sibling.properties.width)
|
.map(sibling => sibling.properties.width)
|
||||||
.reduce((partialSum, a) => partialSum + a, 0);
|
.reduce((widthSum, a) => widthSum + a, 0);
|
||||||
|
|
||||||
const requiredMaxWidth = flexibleGroup.size - fixedWidth;
|
const requiredMaxWidth = flexibleGroup.size - fixedWidth;
|
||||||
|
const minimumPossibleWidth = minWidths.reduce((widthSum, a) => widthSum + a, 0); // sum(minWidths)
|
||||||
|
|
||||||
const minimumPossibleWidth = minWidths.reduce((partialSum, a) => partialSum + a, 0);
|
const checkSumMinWidthsIsFitting = minimumPossibleWidth > requiredMaxWidth;
|
||||||
if (minimumPossibleWidth > requiredMaxWidth) {
|
if (checkSumMinWidthsIsFitting) {
|
||||||
// Swal.fire({
|
console.warn('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.');
|
||||||
// icon: 'error',
|
|
||||||
// title: 'Cannot fit!',
|
|
||||||
// text: 'Cannot fit at all even when squeezing all flex containers to the minimum.'
|
|
||||||
// });
|
|
||||||
console.error('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxMinWidths = Math.max(...minWidths);
|
const maxMinWidths = Math.max(...minWidths);
|
||||||
if (maxMinWidths * minWidths.length < requiredMaxWidth) {
|
if (maxMinWidths * minWidths.length <= requiredMaxWidth) {
|
||||||
const wantedWidth = requiredMaxWidth / minWidths.length;
|
const wantedWidth = requiredMaxWidth / minWidths.length;
|
||||||
// it fits, flex with maxMinWidths and fixed width
|
// it fits, flex with maxMinWidths and fixed width
|
||||||
let right = flexibleGroup.offset;
|
let right = flexibleGroup.offset;
|
||||||
|
@ -73,7 +80,9 @@ function FlexGroup(flexibleGroup: IFlexibleGroup): void {
|
||||||
// does not fit
|
// does not fit
|
||||||
|
|
||||||
/// SIMPLEX ///
|
/// SIMPLEX ///
|
||||||
const solutions: number[] = Simplex(minWidths, requiredMaxWidth);
|
const maxWidths = flexibleContainers
|
||||||
|
.map(sibling => sibling.properties.maxWidth);
|
||||||
|
const solutions: number[] = Simplex(minWidths, maxWidths, requiredMaxWidth);
|
||||||
|
|
||||||
// apply the solutions
|
// apply the solutions
|
||||||
for (let i = 0; i < flexibleContainers.length; i++) {
|
for (let i = 0; i < flexibleContainers.length; i++) {
|
||||||
|
@ -88,6 +97,30 @@ function FlexGroup(flexibleGroup: IFlexibleGroup): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SeparateFlexibleContainers(
|
||||||
|
containers: IContainerModel[]
|
||||||
|
): { flexibleContainers: IContainerModel[], nonFlexibleContainers: IContainerModel[] } {
|
||||||
|
const flexibleContainers: IContainerModel[] = [];
|
||||||
|
const nonFlexibleContainers: IContainerModel[] = [];
|
||||||
|
containers.forEach((container) => {
|
||||||
|
if (container.properties.isFlex) {
|
||||||
|
flexibleContainers.push(container);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nonFlexibleContainers.push(container);
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
flexibleContainers,
|
||||||
|
nonFlexibleContainers
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of groups of flexible containers
|
||||||
|
* @param parent Parent in which the flexible children will be set in groups
|
||||||
|
* @returns a list of groups of flexible containers
|
||||||
|
*/
|
||||||
export function GetFlexibleGroups(parent: IContainerModel): IFlexibleGroup[] {
|
export function GetFlexibleGroups(parent: IContainerModel): IFlexibleGroup[] {
|
||||||
const flexibleGroups: IFlexibleGroup[] = [];
|
const flexibleGroups: IFlexibleGroup[] = [];
|
||||||
let group: IContainerModel[] = [];
|
let group: IContainerModel[] = [];
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import Swal from 'sweetalert2';
|
import Swal from 'sweetalert2';
|
||||||
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
import { ISizePointer } from '../../../Interfaces/ISizePointer';
|
import { ISizePointer } from '../../../Interfaces/ISizePointer';
|
||||||
|
import { ENABLE_HARD_RIGID } from '../../../utils/default';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Transform the container into a rigid body"
|
* "Transform the container into a rigid body"
|
||||||
|
@ -23,7 +24,11 @@ export function ApplyRigidBody(
|
||||||
container: IContainerModel
|
container: IContainerModel
|
||||||
): IContainerModel {
|
): IContainerModel {
|
||||||
container = ConstraintBodyInsideParent(container);
|
container = ConstraintBodyInsideParent(container);
|
||||||
|
|
||||||
|
if (ENABLE_HARD_RIGID) {
|
||||||
container = ConstraintBodyInsideUnallocatedWidth(container);
|
container = ConstraintBodyInsideUnallocatedWidth(container);
|
||||||
|
}
|
||||||
|
|
||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +179,7 @@ export function ConstraintBodyInsideUnallocatedWidth(
|
||||||
});
|
});
|
||||||
|
|
||||||
if (availableWidth === undefined) {
|
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.`);
|
console.debug(`Container ${container.properties.id} cannot fit in any space due to its minimum width being to large.`);
|
||||||
// Swal.fire({
|
// Swal.fire({
|
||||||
// position: 'top-end',
|
// position: 'top-end',
|
||||||
// title: `Container ${container.properties.id} cannot fit!`,
|
// title: `Container ${container.properties.id} cannot fit!`,
|
||||||
|
|
31
src/Components/Editor/Behaviors/SwapBehaviors.ts
Normal file
31
src/Components/Editor/Behaviors/SwapBehaviors.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
/**
|
||||||
|
* Swap two flex container when one is overlapping another
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IContainerModel } from '../../../Interfaces/IContainerModel';
|
||||||
|
import { GetOverlappingContainers } from './AnchorBehaviors';
|
||||||
|
|
||||||
|
export function ApplySwap(container: IContainerModel): void {
|
||||||
|
if (container.parent === null || container.parent === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = container.parent.children;
|
||||||
|
const overlappingContainers = GetOverlappingContainers(container, children);
|
||||||
|
|
||||||
|
if (overlappingContainers.length > 1 || overlappingContainers.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlappingContainer = overlappingContainers.pop();
|
||||||
|
|
||||||
|
if (overlappingContainer === null || overlappingContainer === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// swap positions
|
||||||
|
[overlappingContainer.properties.x, container.properties.x] = [container.properties.x, overlappingContainer.properties.x];
|
||||||
|
const indexContainer = children.indexOf(container);
|
||||||
|
const indexOverlapping = children.indexOf(overlappingContainer);
|
||||||
|
[children[indexContainer], children[indexOverlapping]] = [children[indexOverlapping], children[indexContainer]];
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
|
||||||
import { SVG } from '../SVG/SVG';
|
import { SVG } from '../SVG/SVG';
|
||||||
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
import { IHistoryState } from '../../Interfaces/IHistoryState';
|
||||||
import { UI } from '../UI/UI';
|
import { UI } from '../UI/UI';
|
||||||
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange, AddContainers } from './Actions/ContainerOperations';
|
import { SelectContainer, DeleteContainer, AddContainerToSelectedContainer, OnPropertyChange, AddContainer } from './Actions/ContainerOperations';
|
||||||
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
|
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
|
||||||
import { OnKey } from './Actions/Shortcuts';
|
import { OnKey } from './Actions/Shortcuts';
|
||||||
import { events as EVENTS } from '../../Events/EditorEvents';
|
import { events as EVENTS } from '../../Events/EditorEvents';
|
||||||
|
@ -62,7 +62,7 @@ function InitActions(
|
||||||
|
|
||||||
// API Actions
|
// API Actions
|
||||||
for (const availableContainer of configuration.AvailableContainers) {
|
for (const availableContainer of configuration.AvailableContainers) {
|
||||||
if (availableContainer.Actions === undefined) {
|
if (availableContainer.Actions === undefined || availableContainer.Actions === null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +240,16 @@ export function Editor(props: IEditorProps): JSX.Element {
|
||||||
setNewHistory(newHistory);
|
setNewHistory(newHistory);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
addContainerAt={(index, type, parent) => setNewHistory(
|
||||||
|
AddContainer(
|
||||||
|
index,
|
||||||
|
type,
|
||||||
|
parent,
|
||||||
|
configuration,
|
||||||
|
history,
|
||||||
|
historyCurrentStep
|
||||||
|
)
|
||||||
|
)}
|
||||||
addSymbol={(type) => setNewHistory(
|
addSymbol={(type) => setNewHistory(
|
||||||
AddSymbol(
|
AddSymbol(
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -22,6 +22,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
selectedContainer={undefined}
|
selectedContainer={undefined}
|
||||||
onPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
selectContainer={() => {}}
|
selectContainer={() => {}}
|
||||||
|
addContainer={() => {}}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.getByText(/Elements/i));
|
expect(screen.getByText(/Elements/i));
|
||||||
|
@ -45,6 +46,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
selectedContainer={mainContainer}
|
selectedContainer={mainContainer}
|
||||||
onPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
selectContainer={() => {}}
|
selectContainer={() => {}}
|
||||||
|
addContainer={() => {}}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.getByText(/Elements/i));
|
expect(screen.getByText(/Elements/i));
|
||||||
|
@ -149,6 +151,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
selectedContainer={mainContainer}
|
selectedContainer={mainContainer}
|
||||||
onPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
selectContainer={() => {}}
|
selectContainer={() => {}}
|
||||||
|
addContainer={() => {}}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.getByText(/Elements/i));
|
expect(screen.getByText(/Elements/i));
|
||||||
|
@ -208,6 +211,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
selectedContainer={selectedContainer}
|
selectedContainer={selectedContainer}
|
||||||
onPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
selectContainer={selectContainer}
|
selectContainer={selectContainer}
|
||||||
|
addContainer={() => {}}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect(screen.getByText(/Elements/i));
|
expect(screen.getByText(/Elements/i));
|
||||||
|
@ -230,6 +234,7 @@ describe.concurrent('Elements sidebar', () => {
|
||||||
selectedContainer={selectedContainer}
|
selectedContainer={selectedContainer}
|
||||||
onPropertyChange={() => {}}
|
onPropertyChange={() => {}}
|
||||||
selectContainer={selectContainer}
|
selectContainer={selectContainer}
|
||||||
|
addContainer={() => {}}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();
|
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
||||||
import { FixedSizeList as List } from 'react-window';
|
import { FixedSizeList as List } from 'react-window';
|
||||||
import { Properties } from '../ContainerProperties/ContainerProperties';
|
import { Properties } from '../ContainerProperties/ContainerProperties';
|
||||||
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
import { IContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { GetDepth, MakeIterator } from '../../utils/itertools';
|
import { FindContainerById, GetDepth, MakeIterator } from '../../utils/itertools';
|
||||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
import { PropertyType } from '../../Enums/PropertyType';
|
import { PropertyType } from '../../Enums/PropertyType';
|
||||||
|
|
||||||
|
@ -18,6 +18,102 @@ interface IElementsSidebarProps {
|
||||||
type?: PropertyType
|
type?: PropertyType
|
||||||
) => void
|
) => void
|
||||||
selectContainer: (containerId: string) => void
|
selectContainer: (containerId: string) => void
|
||||||
|
addContainer: (index: number, type: string, parent: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function RemoveBorderClasses(target: HTMLButtonElement, exception: string = ''): void {
|
||||||
|
const bordersClasses = ['border-t-8', 'border-8', 'border-b-8'].filter(className => className !== exception);
|
||||||
|
target.classList.remove(...bordersClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HandleDragLeave(event: React.DragEvent): void {
|
||||||
|
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||||
|
RemoveBorderClasses(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HandleDragOver(
|
||||||
|
event: React.DragEvent,
|
||||||
|
mainContainer: IContainerModel
|
||||||
|
): void {
|
||||||
|
event.preventDefault();
|
||||||
|
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const y = event.clientY - rect.top; // y position within the element.
|
||||||
|
|
||||||
|
if (target.id === mainContainer.properties.id) {
|
||||||
|
target.classList.add('border-8');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (y < 12) {
|
||||||
|
RemoveBorderClasses(target, 'border-t-8');
|
||||||
|
target.classList.add('border-t-8');
|
||||||
|
} else if (y < 24) {
|
||||||
|
RemoveBorderClasses(target, 'border-8');
|
||||||
|
target.classList.add('border-8');
|
||||||
|
} else {
|
||||||
|
RemoveBorderClasses(target, 'border-b-8');
|
||||||
|
target.classList.add('border-b-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function HandleOnDrop(
|
||||||
|
event: React.DragEvent,
|
||||||
|
mainContainer: IContainerModel,
|
||||||
|
addContainer: (index: number, type: string, parent: string) => void
|
||||||
|
): void {
|
||||||
|
event.preventDefault();
|
||||||
|
const type = event.dataTransfer.getData('type');
|
||||||
|
const target: HTMLButtonElement = event.target as HTMLButtonElement;
|
||||||
|
RemoveBorderClasses(target);
|
||||||
|
|
||||||
|
const targetContainer: IContainerModel | undefined = FindContainerById(
|
||||||
|
mainContainer,
|
||||||
|
target.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (targetContainer === undefined) {
|
||||||
|
throw new Error('[handleOnDrop] Tried to drop onto a unknown container!');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetContainer === mainContainer) {
|
||||||
|
// if the container is the root, only add type as child
|
||||||
|
addContainer(
|
||||||
|
targetContainer.children.length,
|
||||||
|
type,
|
||||||
|
targetContainer.properties.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetContainer.parent === null ||
|
||||||
|
targetContainer.parent === undefined) {
|
||||||
|
throw new Error('[handleDrop] Tried to drop into a child container without a parent!');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = target.getBoundingClientRect();
|
||||||
|
const y = event.clientY - rect.top; // y position within the element.
|
||||||
|
|
||||||
|
// locate the hitboxes
|
||||||
|
if (y < 12) {
|
||||||
|
const index = targetContainer.parent.children.indexOf(targetContainer);
|
||||||
|
addContainer(
|
||||||
|
index,
|
||||||
|
type,
|
||||||
|
targetContainer.parent.properties.id
|
||||||
|
);
|
||||||
|
} else if (y < 24) {
|
||||||
|
addContainer(
|
||||||
|
targetContainer.children.length,
|
||||||
|
type,
|
||||||
|
targetContainer.properties.id);
|
||||||
|
} else {
|
||||||
|
const index = targetContainer.parent.children.indexOf(targetContainer);
|
||||||
|
addContainer(
|
||||||
|
index + 1,
|
||||||
|
type,
|
||||||
|
targetContainer.parent.properties.id
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
||||||
|
@ -55,6 +151,9 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
|
||||||
key={key}
|
key={key}
|
||||||
style={style}
|
style={style}
|
||||||
onClick={() => props.selectContainer(container.properties.id)}
|
onClick={() => props.selectContainer(container.properties.id)}
|
||||||
|
onDrop={(event) => HandleOnDrop(event, props.mainContainer, props.addContainer)}
|
||||||
|
onDragOver={(event) => HandleDragOver(event, props.mainContainer)}
|
||||||
|
onDragLeave={(event) => HandleDragLeave(event)}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -12,16 +12,12 @@ interface IInputGroupProps {
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
defaultChecked?: boolean
|
defaultChecked?: boolean
|
||||||
min?: number
|
min?: number
|
||||||
|
max?: number
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const className = `
|
const className = 'input-group';
|
||||||
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 function InputGroup(props: IInputGroupProps): JSX.Element {
|
export function InputGroup(props: IInputGroupProps): JSX.Element {
|
||||||
return <>
|
return <>
|
||||||
|
@ -44,6 +40,7 @@ export function InputGroup(props: IInputGroupProps): JSX.Element {
|
||||||
defaultChecked={props.defaultChecked}
|
defaultChecked={props.defaultChecked}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
min={props.min}
|
min={props.min}
|
||||||
|
max={props.max}
|
||||||
disabled={props.isDisabled} />
|
disabled={props.isDisabled} />
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import './SVG.scss';
|
import './SVG.scss';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { UncontrolledReactSVGPanZoom } from 'react-svg-pan-zoom';
|
import { ReactSVGPanZoom, Tool, TOOL_PAN, Value } from 'react-svg-pan-zoom';
|
||||||
import { Container } from './Elements/Container';
|
import { Container } from './Elements/Container';
|
||||||
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
import { ContainerModel } from '../../Interfaces/IContainerModel';
|
||||||
import { Selector } from './Elements/Selector/Selector';
|
import { Selector } from './Elements/Selector/Selector';
|
||||||
import { BAR_WIDTH } from '../Bar/Bar';
|
import { BAR_WIDTH } from '../Bar/Bar';
|
||||||
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
|
import { DepthDimensionLayer } from './Elements/DepthDimensionLayer';
|
||||||
import { SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
|
import { MAX_FRAMERATE, SHOW_DIMENSIONS_PER_DEPTH } from '../../utils/default';
|
||||||
import { SymbolLayer } from './Elements/SymbolLayer';
|
import { SymbolLayer } from './Elements/SymbolLayer';
|
||||||
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
|
||||||
|
|
||||||
|
@ -54,8 +54,20 @@ export function SVG(props: ISVGProps): JSX.Element {
|
||||||
viewerWidth: window.innerWidth - BAR_WIDTH,
|
viewerWidth: window.innerWidth - BAR_WIDTH,
|
||||||
viewerHeight: window.innerHeight
|
viewerHeight: window.innerHeight
|
||||||
});
|
});
|
||||||
|
const [tool, setTool] = React.useState<Tool>(TOOL_PAN);
|
||||||
|
const [value, setValue] = React.useState<Value>({} as Value);
|
||||||
|
const svgViewer = React.useRef<ReactSVGPanZoom>(null);
|
||||||
|
|
||||||
|
// Framerate limiter
|
||||||
|
const delta = React.useRef(0);
|
||||||
|
const timer = React.useRef(performance.now());
|
||||||
|
const renderCounter = React.useRef(0);
|
||||||
|
// Debug: FPS counter
|
||||||
|
// const startTimer = React.useRef(Date.now());
|
||||||
|
// console.log(renderCounter.current / ((Date.now() - startTimer.current) / 1000));
|
||||||
|
|
||||||
UseSVGAutoResizer(setViewer);
|
UseSVGAutoResizer(setViewer);
|
||||||
|
UseFitOnce(svgViewer);
|
||||||
|
|
||||||
const xmlns = '<http://www.w3.org/2000/svg>';
|
const xmlns = '<http://www.w3.org/2000/svg>';
|
||||||
const properties = {
|
const properties = {
|
||||||
|
@ -73,9 +85,24 @@ export function SVG(props: ISVGProps): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id={ID} className='ml-16'>
|
<div id={ID} className='ml-16'>
|
||||||
<UncontrolledReactSVGPanZoom
|
<ReactSVGPanZoom
|
||||||
|
ref={svgViewer}
|
||||||
width={viewer.viewerWidth}
|
width={viewer.viewerWidth}
|
||||||
height={viewer.viewerHeight}
|
height={viewer.viewerHeight}
|
||||||
|
tool={tool} onChangeTool={setTool}
|
||||||
|
value={value} onChangeValue={(value: Value) => {
|
||||||
|
// Framerate limiter
|
||||||
|
const newTimer = performance.now();
|
||||||
|
delta.current += (newTimer - timer.current) / 1000;
|
||||||
|
timer.current = newTimer;
|
||||||
|
if (delta.current <= (1 / MAX_FRAMERATE)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCounter.current = renderCounter.current + 1;
|
||||||
|
delta.current = delta.current % (1 / MAX_FRAMERATE);
|
||||||
|
setValue(value);
|
||||||
|
}}
|
||||||
background={'#ffffff'}
|
background={'#ffffff'}
|
||||||
defaultTool='pan'
|
defaultTool='pan'
|
||||||
miniatureProps={{
|
miniatureProps={{
|
||||||
|
@ -93,7 +120,14 @@ export function SVG(props: ISVGProps): JSX.Element {
|
||||||
<SymbolLayer symbols={props.symbols} />
|
<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>
|
</ReactSVGPanZoom>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function UseFitOnce(svgViewer: React.RefObject<ReactSVGPanZoom>): void {
|
||||||
|
React.useEffect(() => {
|
||||||
|
svgViewer?.current?.fitToViewer();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ interface IUIProps {
|
||||||
deleteContainer: (containerId: string) => void
|
deleteContainer: (containerId: string) => void
|
||||||
onPropertyChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
|
onPropertyChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
|
||||||
addContainer: (type: string) => void
|
addContainer: (type: string) => void
|
||||||
|
addContainerAt: (index: number, type: string, parent: string) => void
|
||||||
addSymbol: (type: string) => void
|
addSymbol: (type: string) => void
|
||||||
onSymbolPropertyChange: (key: string, value: string | number | boolean) => void
|
onSymbolPropertyChange: (key: string, value: string | number | boolean) => void
|
||||||
selectSymbol: (symbolId: string) => void
|
selectSymbol: (symbolId: string) => void
|
||||||
|
@ -47,10 +48,10 @@ export function UI(props: IUIProps): JSX.Element {
|
||||||
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
|
||||||
|
|
||||||
let buttonRightOffsetClasses = 'right-12';
|
let buttonRightOffsetClasses = 'right-12';
|
||||||
if (isSidebarOpen || isHistoryOpen) {
|
if (isSidebarOpen || isHistoryOpen || isSymbolsOpen) {
|
||||||
buttonRightOffsetClasses = 'right-72';
|
buttonRightOffsetClasses = 'right-72';
|
||||||
}
|
}
|
||||||
if (isHistoryOpen && isSidebarOpen) {
|
if (isHistoryOpen && (isSidebarOpen || isSymbolsOpen)) {
|
||||||
buttonRightOffsetClasses = 'right-[544px]';
|
buttonRightOffsetClasses = 'right-[544px]';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,6 +88,7 @@ export function UI(props: IUIProps): JSX.Element {
|
||||||
isHistoryOpen={isHistoryOpen}
|
isHistoryOpen={isHistoryOpen}
|
||||||
onPropertyChange={props.onPropertyChange}
|
onPropertyChange={props.onPropertyChange}
|
||||||
selectContainer={props.selectContainer}
|
selectContainer={props.selectContainer}
|
||||||
|
addContainer={props.addContainerAt}
|
||||||
/>
|
/>
|
||||||
<SymbolsSidebar
|
<SymbolsSidebar
|
||||||
selectedSymbolId={props.current.selectedSymbolId}
|
selectedSymbolId={props.current.selectedSymbolId}
|
||||||
|
|
|
@ -7,20 +7,80 @@ import { IMargin } from './IMargin';
|
||||||
|
|
||||||
/** Model of available container used in application configuration */
|
/** Model of available container used in application configuration */
|
||||||
export interface IAvailableContainer {
|
export interface IAvailableContainer {
|
||||||
|
/** type */
|
||||||
Type: string
|
Type: string
|
||||||
DefaultX?: number
|
|
||||||
DefaultY?: number
|
/** horizontal offset */
|
||||||
|
X?: number
|
||||||
|
|
||||||
|
/** vertical offset */
|
||||||
|
Y?: number
|
||||||
|
|
||||||
|
/** width */
|
||||||
Width?: number
|
Width?: number
|
||||||
|
|
||||||
|
/** height */
|
||||||
Height?: number
|
Height?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum width (min=1)
|
||||||
|
* Allows the container to set isRigidBody to false when it gets squeezed
|
||||||
|
* by an anchor
|
||||||
|
*/
|
||||||
MinWidth?: number
|
MinWidth?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum width
|
||||||
|
*/
|
||||||
MaxWidth?: number
|
MaxWidth?: number
|
||||||
|
|
||||||
|
/** margin */
|
||||||
Margin?: IMargin
|
Margin?: IMargin
|
||||||
|
|
||||||
|
/** true if anchor, false otherwise */
|
||||||
IsAnchor?: boolean
|
IsAnchor?: boolean
|
||||||
|
|
||||||
|
/** true if flex, false otherwise */
|
||||||
IsFlex?: boolean
|
IsFlex?: boolean
|
||||||
|
|
||||||
|
/** Method used on container add */
|
||||||
AddMethod?: AddMethod
|
AddMethod?: AddMethod
|
||||||
|
|
||||||
|
/** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */
|
||||||
XPositionReference?: XPositionReference
|
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
|
CustomSVG?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (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>
|
||||||
|
* `
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
DefaultChildType?: string
|
DefaultChildType?: string
|
||||||
|
|
||||||
/** if true, show the dimension of the container */
|
/** if true, show the dimension of the container */
|
||||||
ShowSelfDimensions?: boolean
|
ShowSelfDimensions?: boolean
|
||||||
|
|
||||||
|
@ -37,7 +97,21 @@ export interface IAvailableContainer {
|
||||||
* and insert dimensions marks at lift up children (see liftDimensionToBorrower)
|
* and insert dimensions marks at lift up children (see liftDimensionToBorrower)
|
||||||
*/
|
*/
|
||||||
IsDimensionBorrower?: boolean
|
IsDimensionBorrower?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional)
|
||||||
|
* Style of the <rect>
|
||||||
|
*/
|
||||||
Style?: React.CSSProperties
|
Style?: React.CSSProperties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of possible actions shown on right-click
|
||||||
|
*/
|
||||||
Actions?: IAction[]
|
Actions?: IAction[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* (optional)
|
||||||
|
* User data that can be used for data storage or custom SVG
|
||||||
|
*/
|
||||||
UserData?: object
|
UserData?: object
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/naming-convention */
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
import { IContainerModel } from './IContainerModel';
|
import { IAvailableContainer } from './IAvailableContainer';
|
||||||
|
|
||||||
export interface ISetContainerListResponse {
|
export interface ISetContainerListResponse {
|
||||||
Containers: IContainerModel[]
|
Containers: IAvailableContainer[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,6 @@
|
||||||
.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
|
||||||
}
|
}
|
||||||
|
|
||||||
.symbols-sidebar-row {
|
.symbols-sidebar-row {
|
||||||
@apply elements-sidebar-row
|
@apply elements-sidebar-row
|
||||||
}
|
}
|
||||||
|
@ -27,6 +26,10 @@
|
||||||
@apply transition-all w-full h-auto p-4 flex
|
@apply transition-all w-full h-auto p-4 flex
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mainmenu-bg {
|
||||||
|
@apply bg-blue-100 h-full w-full
|
||||||
|
}
|
||||||
|
|
||||||
.mainmenu-btn {
|
.mainmenu-btn {
|
||||||
@apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg
|
@apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg
|
||||||
}
|
}
|
||||||
|
@ -42,7 +45,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-btn {
|
.floating-btn {
|
||||||
@apply h-full w-full text-white align-middle items-center justify-center
|
@apply h-full w-full text-white align-middle
|
||||||
|
items-center justify-center
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar {
|
||||||
|
@apply fixed z-20 flex flex-col top-0 left-0
|
||||||
|
h-full w-16 bg-slate-100
|
||||||
}
|
}
|
||||||
|
|
||||||
.bar-btn {
|
.bar-btn {
|
||||||
|
@ -64,10 +73,18 @@
|
||||||
text-gray-800 bg-slate-100
|
text-gray-800 bg-slate-100
|
||||||
dark:text-white dark:bg-gray-800
|
dark:text-white dark:bg-gray-800
|
||||||
text-xs font-bold
|
text-xs font-bold
|
||||||
transition-all duration-100 scale-0 origin-left;
|
transition-all duration-100 scale-0 origin-left
|
||||||
}
|
}
|
||||||
|
|
||||||
.contextmenu-item {
|
.contextmenu-item {
|
||||||
@apply px-2 py-1 hover:bg-slate-300 text-left
|
@apply px-2 py-1 hover:bg-slate-300 text-left
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
@apply 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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -11,7 +11,10 @@ function RenderRoot(root: Element | Document): void {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specific for Modeler apps
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace SVGLayoutDesigner {
|
namespace SVGLayoutDesigner {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
export const Render = RenderRoot;
|
export const Render = RenderRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,19 @@ import { ISymbolModel } from '../Interfaces/ISymbolModel';
|
||||||
|
|
||||||
/// CONTAINER DEFAULTS ///
|
/// CONTAINER DEFAULTS ///
|
||||||
|
|
||||||
|
/** Enable the swap behavior */
|
||||||
|
export const ENABLE_SWAP = false;
|
||||||
|
|
||||||
|
/** Enable the rigid behavior */
|
||||||
|
export const ENABLE_RIGID = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the hard rigid behavior
|
||||||
|
* disallowing the container to overlap (ENABLE_RIGID must be true)
|
||||||
|
*/
|
||||||
|
export const ENABLE_HARD_RIGID = false;
|
||||||
|
|
||||||
|
/** Enalbe the text in the containers */
|
||||||
export const SHOW_TEXT = false;
|
export const SHOW_TEXT = false;
|
||||||
export const SHOW_SELECTOR_TEXT = true;
|
export const SHOW_SELECTOR_TEXT = true;
|
||||||
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;
|
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;
|
||||||
|
@ -33,6 +46,7 @@ export const DEFAULT_SYMBOL_HEIGHT = 32;
|
||||||
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 APPLY_BEHAVIORS_ON_CHILDREN = true;
|
||||||
|
export const MAX_FRAMERATE = 60;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the default editor state given the configuration
|
* Returns the default editor state given the configuration
|
||||||
|
|
|
@ -17,20 +17,22 @@
|
||||||
* @param requiredMaxWidth
|
* @param requiredMaxWidth
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function Simplex(minWidths: number[], requiredMaxWidth: number): number[] {
|
export function Simplex(minWidths: number[], maxWidths: number[], requiredMaxWidth: number): number[] {
|
||||||
/// 1) standardized the equations
|
/// 1) standardized the equations
|
||||||
// add the min widths constraints
|
// add the min widths constraints
|
||||||
const constraints = minWidths.map(minWidth => minWidth * -1);
|
const constraints = minWidths.map(minWidth => minWidth * -1);
|
||||||
|
|
||||||
// add the max widths constraint
|
|
||||||
constraints.push(requiredMaxWidth);
|
|
||||||
|
|
||||||
/// 2) Create the initial matrix
|
/// 2) Create the initial matrix
|
||||||
// get row length (nVariables + nConstraints + 1 (z) + 1 (b))
|
// get row length (nVariables + nConstraints + 1 (z) + 1 (b))
|
||||||
const nVariables = minWidths.length;
|
const nVariables = minWidths.length;
|
||||||
const nConstraints = constraints.length;
|
const nConstraints = constraints.length;
|
||||||
const rowlength = nVariables + nConstraints + 2;
|
const rowlength =
|
||||||
const matrix = GetInitialMatrix(constraints, rowlength, nVariables);
|
minWidths.length + // min constraints
|
||||||
|
maxWidths.length + // max constraints
|
||||||
|
nConstraints + 1 + // slack variables
|
||||||
|
1 + // z
|
||||||
|
1; // b
|
||||||
|
const matrix = GetInitialMatrix(constraints, maxWidths, requiredMaxWidth, rowlength);
|
||||||
|
|
||||||
/// Apply the algorithm
|
/// Apply the algorithm
|
||||||
const finalMatrix = ApplyMainLoop(matrix, rowlength);
|
const finalMatrix = ApplyMainLoop(matrix, rowlength);
|
||||||
|
@ -40,32 +42,35 @@ export function Simplex(minWidths: number[], requiredMaxWidth: number): number[]
|
||||||
return solutions;
|
return solutions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_TRIES = 10;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specific to min widths algorithm
|
* Specific to min widths algorithm
|
||||||
* Get the initial matrix from the maximum constraints
|
* Get the initial matrix from the maximum constraints
|
||||||
* and the number of variables
|
* and the number of variables
|
||||||
* @param maximumConstraints
|
* @param minConstraints
|
||||||
* @param rowlength
|
* @param rowlength
|
||||||
* @param nVariables
|
* @param nVariables
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
function GetInitialMatrix(
|
function GetInitialMatrix(
|
||||||
maximumConstraints: number[],
|
minConstraints: number[],
|
||||||
rowlength: number,
|
maxConstraints: number[],
|
||||||
nVariables: number
|
objectiveConstraint: number,
|
||||||
|
rowlength: number
|
||||||
): number[][] {
|
): number[][] {
|
||||||
const nConstraints = maximumConstraints.length;
|
const nVariables = maxConstraints.length;
|
||||||
const matrix = maximumConstraints.map((maximumConstraint, index) => {
|
const constraints = minConstraints.concat(maxConstraints);
|
||||||
|
constraints.push(objectiveConstraint);
|
||||||
|
const matrix = constraints.map((constraint, index) => {
|
||||||
const row: number[] = Array(rowlength).fill(0);
|
const row: number[] = Array(rowlength).fill(0);
|
||||||
|
|
||||||
// insert the variable coefficient a of a*x
|
// insert the variable coefficient a of a*x
|
||||||
if (index <= nConstraints - 2) {
|
if (index < nVariables) {
|
||||||
// insert the the variable coefficient of the minimum widths constraints (negative identity matrix)
|
// insert the the variable coefficient of the minimum/maximum widths constraints (negative identity matrix)
|
||||||
row[index] = -1;
|
row[index] = -1;
|
||||||
|
} else if (index < (2 * nVariables)) {
|
||||||
|
row[index - (nVariables)] = 1;
|
||||||
} else {
|
} else {
|
||||||
// insert the the variable coefficient of the maximum width constraint
|
// insert the the variable coefficient of the maximum desired width constraint
|
||||||
row.fill(1, 0, nVariables);
|
row.fill(1, 0, nVariables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +78,7 @@ function GetInitialMatrix(
|
||||||
row[index + nVariables] = 1;
|
row[index + nVariables] = 1;
|
||||||
|
|
||||||
// insert the constraint coefficient (b)
|
// insert the constraint coefficient (b)
|
||||||
row[rowlength - 1] = maximumConstraint;
|
row[rowlength - 1] = constraint;
|
||||||
return row;
|
return row;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -119,7 +124,8 @@ function GetAllIndexes(arr: number[], val: number): number[] {
|
||||||
*/
|
*/
|
||||||
function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] {
|
function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] {
|
||||||
let matrix = oldMatrix;
|
let matrix = oldMatrix;
|
||||||
let tries = MAX_TRIES;
|
const maxTries = oldMatrix.length * 2;
|
||||||
|
let tries = maxTries;
|
||||||
const indexesTried: Record<number, number> = {};
|
const indexesTried: Record<number, number> = {};
|
||||||
while (matrix[matrix.length - 1].some((v: number) => v < 0) && tries > 0) {
|
while (matrix[matrix.length - 1].some((v: number) => v < 0) && tries > 0) {
|
||||||
// 1) find the index with smallest coefficient (O(n)+)
|
// 1) find the index with smallest coefficient (O(n)+)
|
||||||
|
@ -183,9 +189,10 @@ function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tries === 0) {
|
if (tries === 0) {
|
||||||
|
console.table(matrix);
|
||||||
throw new Error('[Flex] Simplexe: Could not find a solution');
|
throw new Error('[Flex] Simplexe: Could not find a solution');
|
||||||
}
|
}
|
||||||
|
console.debug(`Simplex was solved in ${maxTries - tries} tries`);
|
||||||
return matrix;
|
return matrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ const GetSVGLayoutConfiguration = () => {
|
||||||
Type: 'Chassis',
|
Type: 'Chassis',
|
||||||
MaxWidth: 500,
|
MaxWidth: 500,
|
||||||
MinWidth: 200,
|
MinWidth: 200,
|
||||||
|
Width: 200,
|
||||||
DefaultChildType: 'Trou',
|
DefaultChildType: 'Trou',
|
||||||
Style: {
|
Style: {
|
||||||
fillOpacity: 1,
|
fillOpacity: 1,
|
||||||
|
@ -63,7 +64,7 @@ const GetSVGLayoutConfiguration = () => {
|
||||||
fill: '#d3c9b7',
|
fill: '#d3c9b7',
|
||||||
},
|
},
|
||||||
ShowSelfDimensions: true,
|
ShowSelfDimensions: true,
|
||||||
IsDimensionBorrower: true
|
IsDimensionBorrower: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Type: 'Trou',
|
Type: 'Trou',
|
||||||
|
@ -219,19 +220,13 @@ const FillHoleWithChassis = (request) => {
|
||||||
const SplitRemplissage = (request) => {
|
const SplitRemplissage = (request) => {
|
||||||
const lstModels = [
|
const lstModels = [
|
||||||
{
|
{
|
||||||
properties: {
|
Type: 'Remplissage'
|
||||||
type: 'Remplissage'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
properties: {
|
Type: 'Montant'
|
||||||
type: 'Montant'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
properties: {
|
Type: 'Remplissage'
|
||||||
type: 'Remplissage'
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -3,5 +3,7 @@ import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()]
|
plugins: [
|
||||||
|
react()
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue