Merge branch 'dev'

This commit is contained in:
Siklos 2022-09-08 12:30:07 +02:00
commit 36526dcb27
29 changed files with 555 additions and 132 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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({

View file

@ -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[] {

View file

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

View file

@ -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[] = [];

View file

@ -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!`,

View 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]];
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}, []);
}

View file

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

View file

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

View file

@ -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[]
} }

View file

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

View file

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

View file

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

View file

@ -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) {
throw new Error('[Flex]Simplexe: Could not find a solution'); console.table(matrix);
throw new Error('[Flex] Simplexe: Could not find a solution');
} }
console.debug(`Simplex was solved in ${maxTries - tries} tries`);
return matrix; return matrix;
} }

View file

@ -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'
}
}, },
]; ];

View file

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