diff --git a/README.md b/README.md
index a3d003d..e413c05 100644
--- a/README.md
+++ b/README.md
@@ -13,8 +13,9 @@ An svg layout designer.
Requierements :
- NodeJS
- npm
-- pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory)
- 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
@@ -22,9 +23,6 @@ Run `npm ci`
Run `npm run dev`
-
-
-
# Deploy
Run `npm ci`
@@ -72,4 +70,35 @@ bun run http.js
The web server will be running at `http://localhost:5000`
-Configure the file `.env.development` with the url
\ No newline at end of file
+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.
\ No newline at end of file
diff --git a/src/Components/API/api.ts b/src/Components/API/api.ts
index 72a4947..6a43c72 100644
--- a/src/Components/API/api.ts
+++ b/src/Components/API/api.ts
@@ -1,5 +1,4 @@
import { IConfiguration } from '../../Interfaces/IConfiguration';
-import { IHistoryState } from '../../Interfaces/IHistoryState';
import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest';
import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse';
import { GetCircularReplacer } from '../../utils/saveload';
@@ -42,6 +41,10 @@ export async function SetContainerList(request: ISetContainerListRequest): Promi
if (window.fetch) {
return await fetch(url, {
method: 'POST',
+ headers: new Headers({
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'Content-Type': 'application/json'
+ }),
body: dataParsed
})
.then(async(response) =>
@@ -56,6 +59,7 @@ export async function SetContainerList(request: ISetContainerListRequest): Promi
resolve(JSON.parse(this.responseText));
}
};
+ xhr.setRequestHeader('Content-type', 'application/json');
xhr.send(dataParsed);
});
}
diff --git a/src/Components/App/Actions/MenuActions.ts b/src/Components/App/Actions/MenuActions.ts
index 6bbf751..9d074f6 100644
--- a/src/Components/App/Actions/MenuActions.ts
+++ b/src/Components/App/Actions/MenuActions.ts
@@ -18,7 +18,7 @@ export function NewEditor(
setEditorState(editorState);
setLoaded(true);
}, (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);
});
}
diff --git a/src/Components/App/App.scss b/src/Components/App/App.scss
deleted file mode 100644
index e69de29..0000000
diff --git a/src/Components/App/App.tsx b/src/Components/App/App.tsx
index b0a2374..3b1771b 100644
--- a/src/Components/App/App.tsx
+++ b/src/Components/App/App.tsx
@@ -1,5 +1,4 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
-import './App.scss';
import { MainMenu } from '../MainMenu/MainMenu';
import { ContainerModel } from '../../Interfaces/IContainerModel';
import { Editor } from '../Editor/Editor';
@@ -78,7 +77,7 @@ export function App(props: IAppProps): JSX.Element {
}
return (
-
+
NewEditor(
setEditorState, setLoaded
diff --git a/src/Components/Bar/Bar.tsx b/src/Components/Bar/Bar.tsx
index a8ed605..7125f30 100644
--- a/src/Components/Bar/Bar.tsx
+++ b/src/Components/Bar/Bar.tsx
@@ -16,7 +16,7 @@ export const BAR_WIDTH = 64; // 4rem
export function Bar(props: IBarProps): JSX.Element {
return (
-
+
+
props.onChange('width', ApplyWidthMargin(Number(event.target.value), props.properties.margin.left, props.properties.margin.right))}
isDisabled={props.properties.isFlex} />
@@ -227,6 +229,49 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
value={props.properties.linkedSymbolId ?? ''}
onChange={(event) => props.onChange('linkedSymbolId', event.target.value)} />
{GetCSSInputs(props.properties, props.onChange)}
+ {
+ SHOW_SELF_DIMENSIONS &&
+ props.onChange('showSelfDimensions', event.target.checked)} />
+ }
+ {
+ SHOW_CHILDREN_DIMENSIONS &&
+ props.onChange('showChildrenDimensions', event.target.checked)} />
+ }
+ {
+ SHOW_BORROWER_DIMENSIONS &&
+ <>
+ props.onChange('markPositionToDimensionBorrower', event.target.checked)} />
+ props.onChange('isDimensionBorrower', event.target.checked)} />
+ >
+ }
);
}
diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts
index 9b2b460..d0dc7e4 100644
--- a/src/Components/Editor/Actions/ContainerOperations.ts
+++ b/src/Components/Editor/Actions/ContainerOperations.ts
@@ -6,7 +6,7 @@ import { GetCurrentHistory, UpdateCounters } from '../Editor';
import { AddMethod } from '../../../Enums/AddMethod';
import { IAvailableContainer } from '../../../Interfaces/IAvailableContainer';
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 Swal from 'sweetalert2';
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');
}
- ApplyBehaviorsOnSiblings(container, current.symbols);
+ ApplyBehaviorsOnSiblingsChildren(container, current.symbols);
// Select the previous container
// or select the one above
@@ -234,8 +234,8 @@ export function AddContainers(
const right: number = containerConfig.Margin?.right ?? 0;
// Default coordinates
- let x = containerConfig.DefaultX ?? 0;
- let y = containerConfig.DefaultY ?? 0;
+ let x = containerConfig.X ?? 0;
+ let y = containerConfig.Y ?? 0;
let width = containerConfig.Width ?? containerConfig.MaxWidth ?? containerConfig.MinWidth ?? parentClone.properties.width;
let height = containerConfig.Height ?? parentClone.properties.height;
@@ -276,6 +276,9 @@ export function AddContainers(
parentClone.children.splice(index, 0, newContainer);
}
+ // Sort the parent children by x
+ UpdateParentChildrenList(parentClone);
+
/// Handle behaviors here ///
// Initialize default children of the container
@@ -285,10 +288,7 @@ export function AddContainers(
ApplyBehaviors(newContainer, current.symbols);
// Then, apply the behaviors on its siblings (mostly for flex)
- ApplyBehaviorsOnSiblings(newContainer, current.symbols);
-
- // Sort the parent children by x
- UpdateParentChildrenList(parentClone);
+ ApplyBehaviorsOnSiblingsChildren(newContainer, current.symbols);
// Add to the list of container id for logging purpose
containerIds.push(newContainer.properties.id);
@@ -403,8 +403,8 @@ function InitializeDefaultChild(
const bottom: number = currentConfig.Margin?.bottom ?? 0;
const top: number = currentConfig.Margin?.top ?? 0;
const right: number = currentConfig.Margin?.right ?? 0;
- let x = currentConfig.DefaultX ?? 0;
- let y = currentConfig.DefaultY ?? 0;
+ let x = currentConfig.X ?? 0;
+ let y = currentConfig.Y ?? 0;
let width = currentConfig.Width ?? currentConfig.MaxWidth ?? currentConfig.MinWidth ?? parent.properties.width;
let height = currentConfig.Height ?? parent.properties.height;
@@ -547,7 +547,7 @@ function SetContainer(
ApplyBehaviors(container, symbols);
// Apply special behaviors on siblings
- ApplyBehaviorsOnSiblings(container, symbols);
+ ApplyBehaviorsOnSiblingsChildren(container, symbols);
// sort the children list by their position
UpdateParentChildrenList(container.parent);
@@ -626,19 +626,3 @@ function LinkSymbol(
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): void {
- if (newContainer.parent === null || newContainer.parent === undefined) {
- return;
- }
-
- newContainer.parent.children
- .filter(container => newContainer !== container)
- .forEach(container => ApplyBehaviors(container, symbols));
-}
diff --git a/src/Components/Editor/Actions/ContextMenuActions.ts b/src/Components/Editor/Actions/ContextMenuActions.ts
index 91659f8..64f37fc 100644
--- a/src/Components/Editor/Actions/ContextMenuActions.ts
+++ b/src/Components/Editor/Actions/ContextMenuActions.ts
@@ -68,7 +68,7 @@ function HandleSetContainerList(
setNewHistory(
AddContainers(
selectedContainer.children.length,
- response.Containers.map(container => container.properties.type),
+ response.Containers.map(container => container.Type),
selectedContainer.properties.id,
configuration,
history,
@@ -104,28 +104,31 @@ function HandleReplace(
const index = selectedContainer.parent.children.indexOf(selectedContainer);
- const types = response.Containers.map(container => container.properties.type);
- const newHistoryBeforeDelete = AddContainers(
- index + 1,
- types,
- selectedContainer.properties.parentId,
- configuration,
+ const types = response.Containers.map(container => container.Type);
+
+ const newHistoryAfterDelete = DeleteContainer(
+ selectedContainer.properties.id,
history,
historyCurrentStep
);
- const newHistoryAfterDelete = DeleteContainer(
- selectedContainer.properties.id,
- newHistoryBeforeDelete,
- newHistoryBeforeDelete.length - 1
+ const newHistoryBeforeDelete = AddContainers(
+ index,
+ types,
+ selectedContainer.properties.parentId,
+ configuration,
+ newHistoryAfterDelete,
+ newHistoryAfterDelete.length - 1
);
// 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
- newHistoryAfterDelete[newHistoryAfterDelete.length - 1].lastAction =
+ newHistoryBeforeDelete[newHistoryBeforeDelete.length - 1].lastAction =
`Replace ${selectedContainer.properties.id} by [${types.join(', ')}]`;
- return newHistoryAfterDelete;
+ return newHistoryBeforeDelete;
}
diff --git a/src/Components/Editor/Actions/SymbolOperations.ts b/src/Components/Editor/Actions/SymbolOperations.ts
index e3c89fa..a187e11 100644
--- a/src/Components/Editor/Actions/SymbolOperations.ts
+++ b/src/Components/Editor/Actions/SymbolOperations.ts
@@ -1,4 +1,3 @@
-import { Dispatch, SetStateAction } from 'react';
import { IConfiguration } from '../../../Interfaces/IConfiguration';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { IHistoryState } from '../../../Interfaces/IHistoryState';
@@ -6,7 +5,7 @@ import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { GetDefaultSymbolModel } from '../../../utils/default';
import { FindContainerById } from '../../../utils/itertools';
import { RestoreX } from '../../../utils/svg';
-import { ApplyBehaviors } from '../Behaviors/Behaviors';
+import { ApplyBehaviors, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors';
import { GetCurrentHistory, UpdateCounters } from '../Editor';
export function AddSymbol(
@@ -150,6 +149,8 @@ export function OnPropertyChange(
}
ApplyBehaviors(container, newSymbols);
+
+ ApplyBehaviorsOnSiblingsChildren(container, newSymbols);
});
history.push({
diff --git a/src/Components/Editor/Behaviors/AnchorBehaviors.ts b/src/Components/Editor/Behaviors/AnchorBehaviors.ts
index 5c51623..67fafb7 100644
--- a/src/Components/Editor/Behaviors/AnchorBehaviors.ts
+++ b/src/Components/Editor/Behaviors/AnchorBehaviors.ts
@@ -44,7 +44,7 @@ export function ApplyAnchor(container: IContainerModel): IContainerModel {
* @param containers A list of containers
* @returns A list of overlapping containers
*/
-function GetOverlappingContainers(
+export function GetOverlappingContainers(
container: IContainerModel,
containers: IContainerModel[]
): IContainerModel[] {
diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts
index 928f25d..116917b 100644
--- a/src/Components/Editor/Behaviors/Behaviors.ts
+++ b/src/Components/Editor/Behaviors/Behaviors.ts
@@ -1,9 +1,10 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel';
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 { Flex } from './FlexBehaviors';
import { ApplyRigidBody } from './RigidBodyBehaviors';
+import { ApplySwap } from './SwapBehaviors';
import { ApplySymbol } from './SymbolBehaviors';
/**
@@ -13,17 +14,23 @@ import { ApplySymbol } from './SymbolBehaviors';
* @returns Updated container
*/
export function ApplyBehaviors(container: IContainerModel, symbols: Map): IContainerModel {
+ const symbol = symbols.get(container.properties.linkedSymbolId);
+ if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
+ ApplySymbol(container, symbol);
+ }
+
if (container.properties.isAnchor) {
ApplyAnchor(container);
}
+ if (ENABLE_SWAP) {
+ ApplySwap(container);
+ }
+
Flex(container);
- ApplyRigidBody(container);
-
- const symbol = symbols.get(container.properties.linkedSymbolId);
- if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
- ApplySymbol(container, symbol);
+ if (ENABLE_RIGID) {
+ ApplyRigidBody(container);
}
if (APPLY_BEHAVIORS_ON_CHILDREN) {
@@ -35,3 +42,26 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map): 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);
+ }
+ });
+}
diff --git a/src/Components/Editor/Behaviors/FlexBehaviors.ts b/src/Components/Editor/Behaviors/FlexBehaviors.ts
index 3b8e34e..10f6cf0 100644
--- a/src/Components/Editor/Behaviors/FlexBehaviors.ts
+++ b/src/Components/Editor/Behaviors/FlexBehaviors.ts
@@ -1,3 +1,4 @@
+import Swal from 'sweetalert2';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { Simplex } from '../../../utils/simplex';
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 {
const children = flexibleGroup.group;
- const flexibleContainers = children
- .filter(sibling => sibling.properties.isFlex);
+ const {
+ flexibleContainers,
+ nonFlexibleContainers
+ } = SeparateFlexibleContainers(children);
+
+ if (flexibleContainers.length === 0) {
+ return;
+ }
const minWidths = flexibleContainers
.map(sibling => sibling.properties.minWidth);
- const fixedWidth = children
- .filter(sibling => !sibling.properties.isFlex)
+ const fixedWidth = nonFlexibleContainers
.map(sibling => sibling.properties.width)
- .reduce((partialSum, a) => partialSum + a, 0);
+ .reduce((widthSum, a) => widthSum + a, 0);
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);
- if (minimumPossibleWidth > requiredMaxWidth) {
- // Swal.fire({
- // 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.');
+ const checkSumMinWidthsIsFitting = minimumPossibleWidth > requiredMaxWidth;
+ if (checkSumMinWidthsIsFitting) {
+ console.warn('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.');
return;
}
const maxMinWidths = Math.max(...minWidths);
- if (maxMinWidths * minWidths.length < requiredMaxWidth) {
+ if (maxMinWidths * minWidths.length <= requiredMaxWidth) {
const wantedWidth = requiredMaxWidth / minWidths.length;
// it fits, flex with maxMinWidths and fixed width
let right = flexibleGroup.offset;
@@ -73,7 +80,9 @@ function FlexGroup(flexibleGroup: IFlexibleGroup): void {
// does not fit
/// 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
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[] {
const flexibleGroups: IFlexibleGroup[] = [];
let group: IContainerModel[] = [];
diff --git a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts
index 46a973f..90e4ac9 100644
--- a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts
+++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts
@@ -9,6 +9,7 @@
import Swal from 'sweetalert2';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISizePointer } from '../../../Interfaces/ISizePointer';
+import { ENABLE_HARD_RIGID } from '../../../utils/default';
/**
* "Transform the container into a rigid body"
@@ -23,7 +24,11 @@ export function ApplyRigidBody(
container: IContainerModel
): IContainerModel {
container = ConstraintBodyInsideParent(container);
- container = ConstraintBodyInsideUnallocatedWidth(container);
+
+ if (ENABLE_HARD_RIGID) {
+ container = ConstraintBodyInsideUnallocatedWidth(container);
+ }
+
return container;
}
@@ -174,7 +179,7 @@ export function ConstraintBodyInsideUnallocatedWidth(
});
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({
// position: 'top-end',
// title: `Container ${container.properties.id} cannot fit!`,
diff --git a/src/Components/Editor/Behaviors/SwapBehaviors.ts b/src/Components/Editor/Behaviors/SwapBehaviors.ts
new file mode 100644
index 0000000..2432fe3
--- /dev/null
+++ b/src/Components/Editor/Behaviors/SwapBehaviors.ts
@@ -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]];
+}
diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx
index 618967f..91c919c 100644
--- a/src/Components/Editor/Editor.tsx
+++ b/src/Components/Editor/Editor.tsx
@@ -4,7 +4,7 @@ import { IConfiguration } from '../../Interfaces/IConfiguration';
import { SVG } from '../SVG/SVG';
import { IHistoryState } from '../../Interfaces/IHistoryState';
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 { OnKey } from './Actions/Shortcuts';
import { events as EVENTS } from '../../Events/EditorEvents';
@@ -62,7 +62,7 @@ function InitActions(
// API Actions
for (const availableContainer of configuration.AvailableContainers) {
- if (availableContainer.Actions === undefined) {
+ if (availableContainer.Actions === undefined || availableContainer.Actions === null) {
continue;
}
@@ -240,6 +240,16 @@ export function Editor(props: IEditorProps): JSX.Element {
setNewHistory(newHistory);
}
}}
+ addContainerAt={(index, type, parent) => setNewHistory(
+ AddContainer(
+ index,
+ type,
+ parent,
+ configuration,
+ history,
+ historyCurrentStep
+ )
+ )}
addSymbol={(type) => setNewHistory(
AddSymbol(
type,
diff --git a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx
index 853f80a..14159e0 100644
--- a/src/Components/ElementsSidebar/ElementsSidebar.test.tsx
+++ b/src/Components/ElementsSidebar/ElementsSidebar.test.tsx
@@ -22,6 +22,7 @@ describe.concurrent('Elements sidebar', () => {
selectedContainer={undefined}
onPropertyChange={() => {}}
selectContainer={() => {}}
+ addContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@@ -45,6 +46,7 @@ describe.concurrent('Elements sidebar', () => {
selectedContainer={mainContainer}
onPropertyChange={() => {}}
selectContainer={() => {}}
+ addContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@@ -149,6 +151,7 @@ describe.concurrent('Elements sidebar', () => {
selectedContainer={mainContainer}
onPropertyChange={() => {}}
selectContainer={() => {}}
+ addContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@@ -208,6 +211,7 @@ describe.concurrent('Elements sidebar', () => {
selectedContainer={selectedContainer}
onPropertyChange={() => {}}
selectContainer={selectContainer}
+ addContainer={() => {}}
/>);
expect(screen.getByText(/Elements/i));
@@ -230,6 +234,7 @@ describe.concurrent('Elements sidebar', () => {
selectedContainer={selectedContainer}
onPropertyChange={() => {}}
selectContainer={selectContainer}
+ addContainer={() => {}}
/>);
expect((propertyId as HTMLInputElement).value === 'main').toBeFalsy();
diff --git a/src/Components/ElementsSidebar/ElementsSidebar.tsx b/src/Components/ElementsSidebar/ElementsSidebar.tsx
index f3a2371..c124758 100644
--- a/src/Components/ElementsSidebar/ElementsSidebar.tsx
+++ b/src/Components/ElementsSidebar/ElementsSidebar.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { FixedSizeList as List } from 'react-window';
import { Properties } from '../ContainerProperties/ContainerProperties';
import { IContainerModel } from '../../Interfaces/IContainerModel';
-import { GetDepth, MakeIterator } from '../../utils/itertools';
+import { FindContainerById, GetDepth, MakeIterator } from '../../utils/itertools';
import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { PropertyType } from '../../Enums/PropertyType';
@@ -18,6 +18,102 @@ interface IElementsSidebarProps {
type?: PropertyType
) => 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 {
@@ -55,6 +151,9 @@ export function ElementsSidebar(props: IElementsSidebarProps): JSX.Element {
key={key}
style={style}
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}
diff --git a/src/Components/InputGroup/InputGroup.tsx b/src/Components/InputGroup/InputGroup.tsx
index 808fae7..68b5910 100644
--- a/src/Components/InputGroup/InputGroup.tsx
+++ b/src/Components/InputGroup/InputGroup.tsx
@@ -12,16 +12,12 @@ interface IInputGroupProps {
defaultValue?: string
defaultChecked?: boolean
min?: number
+ max?: number
isDisabled?: boolean
onChange?: (event: React.ChangeEvent) => void
}
-const className = `
- w-full
- text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
- bg-white border-2 border-white rounded-lg placeholder-gray-800
- focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
- disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
+const className = 'input-group';
export function InputGroup(props: IInputGroupProps): JSX.Element {
return <>
@@ -44,6 +40,7 @@ export function InputGroup(props: IInputGroupProps): JSX.Element {
defaultChecked={props.defaultChecked}
onChange={props.onChange}
min={props.min}
+ max={props.max}
disabled={props.isDisabled} />
>;
}
diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx
index e79827c..0cc6311 100644
--- a/src/Components/SVG/SVG.tsx
+++ b/src/Components/SVG/SVG.tsx
@@ -1,12 +1,12 @@
import './SVG.scss';
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 { ContainerModel } from '../../Interfaces/IContainerModel';
import { Selector } from './Elements/Selector/Selector';
import { BAR_WIDTH } from '../Bar/Bar';
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 { ISymbolModel } from '../../Interfaces/ISymbolModel';
@@ -54,8 +54,20 @@ export function SVG(props: ISVGProps): JSX.Element {
viewerWidth: window.innerWidth - BAR_WIDTH,
viewerHeight: window.innerHeight
});
+ const [tool, setTool] = React.useState(TOOL_PAN);
+ const [value, setValue] = React.useState({} as Value);
+ const svgViewer = React.useRef(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);
+ UseFitOnce(svgViewer);
const xmlns = '';
const properties = {
@@ -73,9 +85,24 @@ export function SVG(props: ISVGProps): JSX.Element {
return (
- {
+ // 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'}
defaultTool='pan'
miniatureProps={{
@@ -93,7 +120,14 @@ export function SVG(props: ISVGProps): JSX.Element {
{/* leave this at the end so it can be removed during the svg export */}
-
+
);
}
+
+function UseFitOnce(svgViewer: React.RefObject): void {
+ React.useEffect(() => {
+ svgViewer?.current?.fitToViewer();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+}
diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx
index eab588a..f232434 100644
--- a/src/Components/UI/UI.tsx
+++ b/src/Components/UI/UI.tsx
@@ -24,6 +24,7 @@ interface IUIProps {
deleteContainer: (containerId: string) => void
onPropertyChange: (key: string, value: string | number | boolean, type?: PropertyType) => void
addContainer: (type: string) => void
+ addContainerAt: (index: number, type: string, parent: string) => void
addSymbol: (type: string) => void
onSymbolPropertyChange: (key: string, value: string | number | boolean) => void
selectSymbol: (symbolId: string) => void
@@ -47,10 +48,10 @@ export function UI(props: IUIProps): JSX.Element {
const [isHistoryOpen, setIsHistoryOpen] = React.useState(false);
let buttonRightOffsetClasses = 'right-12';
- if (isSidebarOpen || isHistoryOpen) {
+ if (isSidebarOpen || isHistoryOpen || isSymbolsOpen) {
buttonRightOffsetClasses = 'right-72';
}
- if (isHistoryOpen && isSidebarOpen) {
+ if (isHistoryOpen && (isSidebarOpen || isSymbolsOpen)) {
buttonRightOffsetClasses = 'right-[544px]';
}
@@ -87,6 +88,7 @@ export function UI(props: IUIProps): JSX.Element {
isHistoryOpen={isHistoryOpen}
onPropertyChange={props.onPropertyChange}
selectContainer={props.selectContainer}
+ addContainer={props.addContainerAt}
/>
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 :
+ * ```
+ * `
+ *
+ *
+ *
+ * `
+ * ```
+ */
CustomSVG?: string
+
+ /**
+ * (optional)
+ * Replace a 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 :
+ * ```
+ * `
+ *
+ *
+ *
+ * `
+ * ```
+ */
DefaultChildType?: string
+
/** if true, show the dimension of the container */
ShowSelfDimensions?: boolean
@@ -37,7 +97,21 @@ export interface IAvailableContainer {
* and insert dimensions marks at lift up children (see liftDimensionToBorrower)
*/
IsDimensionBorrower?: boolean
+
+ /**
+ * (optional)
+ * Style of the
+ */
Style?: React.CSSProperties
+
+ /**
+ * List of possible actions shown on right-click
+ */
Actions?: IAction[]
+
+ /**
+ * (optional)
+ * User data that can be used for data storage or custom SVG
+ */
UserData?: object
}
diff --git a/src/Interfaces/ISetContainerListResponse.ts b/src/Interfaces/ISetContainerListResponse.ts
index 53725e4..96cdccf 100644
--- a/src/Interfaces/ISetContainerListResponse.ts
+++ b/src/Interfaces/ISetContainerListResponse.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
-import { IContainerModel } from './IContainerModel';
+import { IAvailableContainer } from './IAvailableContainer';
export interface ISetContainerListResponse {
- Containers: IContainerModel[]
+ Containers: IAvailableContainer[]
}
diff --git a/src/index.scss b/src/index.scss
index c9b1963..4237259 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -18,7 +18,6 @@
.elements-sidebar-row {
@apply pl-6 pr-6 pt-2 pb-2 w-full
}
-
.symbols-sidebar-row {
@apply elements-sidebar-row
}
@@ -27,6 +26,10 @@
@apply transition-all w-full h-auto p-4 flex
}
+ .mainmenu-bg {
+ @apply bg-blue-100 h-full w-full
+ }
+
.mainmenu-btn {
@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 {
- @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 {
@@ -64,10 +73,18 @@
text-gray-800 bg-slate-100
dark:text-white dark:bg-gray-800
text-xs font-bold
- transition-all duration-100 scale-0 origin-left;
+ transition-all duration-100 scale-0 origin-left
}
.contextmenu-item {
@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;
+ }
}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
index 264c622..21c0771 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -11,7 +11,10 @@ function RenderRoot(root: Element | Document): void {
);
}
+// Specific for Modeler apps
+// eslint-disable-next-line @typescript-eslint/no-namespace
namespace SVGLayoutDesigner {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
export const Render = RenderRoot;
}
diff --git a/src/utils/default.ts b/src/utils/default.ts
index 49e7c56..0488fe4 100644
--- a/src/utils/default.ts
+++ b/src/utils/default.ts
@@ -9,6 +9,19 @@ import { ISymbolModel } from '../Interfaces/ISymbolModel';
/// 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_SELECTOR_TEXT = true;
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;
@@ -33,6 +46,7 @@ export const DEFAULT_SYMBOL_HEIGHT = 32;
export const ENABLE_SHORTCUTS = true;
export const MAX_HISTORY = 200;
export const APPLY_BEHAVIORS_ON_CHILDREN = true;
+export const MAX_FRAMERATE = 60;
/**
* Returns the default editor state given the configuration
diff --git a/src/utils/simplex.ts b/src/utils/simplex.ts
index ea30731..161a699 100644
--- a/src/utils/simplex.ts
+++ b/src/utils/simplex.ts
@@ -17,20 +17,22 @@
* @param requiredMaxWidth
* @returns
*/
-export function Simplex(minWidths: number[], requiredMaxWidth: number): number[] {
+export function Simplex(minWidths: number[], maxWidths: number[], requiredMaxWidth: number): number[] {
/// 1) standardized the equations
// add the min widths constraints
const constraints = minWidths.map(minWidth => minWidth * -1);
- // add the max widths constraint
- constraints.push(requiredMaxWidth);
-
/// 2) Create the initial matrix
// get row length (nVariables + nConstraints + 1 (z) + 1 (b))
const nVariables = minWidths.length;
const nConstraints = constraints.length;
- const rowlength = nVariables + nConstraints + 2;
- const matrix = GetInitialMatrix(constraints, rowlength, nVariables);
+ const rowlength =
+ 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
const finalMatrix = ApplyMainLoop(matrix, rowlength);
@@ -40,32 +42,35 @@ export function Simplex(minWidths: number[], requiredMaxWidth: number): number[]
return solutions;
}
-const MAX_TRIES = 10;
-
/**
* Specific to min widths algorithm
* Get the initial matrix from the maximum constraints
* and the number of variables
- * @param maximumConstraints
+ * @param minConstraints
* @param rowlength
* @param nVariables
* @returns
*/
function GetInitialMatrix(
- maximumConstraints: number[],
- rowlength: number,
- nVariables: number
+ minConstraints: number[],
+ maxConstraints: number[],
+ objectiveConstraint: number,
+ rowlength: number
): number[][] {
- const nConstraints = maximumConstraints.length;
- const matrix = maximumConstraints.map((maximumConstraint, index) => {
+ const nVariables = maxConstraints.length;
+ const constraints = minConstraints.concat(maxConstraints);
+ constraints.push(objectiveConstraint);
+ const matrix = constraints.map((constraint, index) => {
const row: number[] = Array(rowlength).fill(0);
// insert the variable coefficient a of a*x
- if (index <= nConstraints - 2) {
- // insert the the variable coefficient of the minimum widths constraints (negative identity matrix)
+ if (index < nVariables) {
+ // insert the the variable coefficient of the minimum/maximum widths constraints (negative identity matrix)
row[index] = -1;
+ } else if (index < (2 * nVariables)) {
+ row[index - (nVariables)] = 1;
} 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);
}
@@ -73,7 +78,7 @@ function GetInitialMatrix(
row[index + nVariables] = 1;
// insert the constraint coefficient (b)
- row[rowlength - 1] = maximumConstraint;
+ row[rowlength - 1] = constraint;
return row;
});
@@ -119,7 +124,8 @@ function GetAllIndexes(arr: number[], val: number): number[] {
*/
function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] {
let matrix = oldMatrix;
- let tries = MAX_TRIES;
+ const maxTries = oldMatrix.length * 2;
+ let tries = maxTries;
const indexesTried: Record = {};
while (matrix[matrix.length - 1].some((v: number) => v < 0) && tries > 0) {
// 1) find the index with smallest coefficient (O(n)+)
@@ -183,9 +189,10 @@ function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] {
}
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;
}
diff --git a/test-server/http.js b/test-server/http.js
index 650c48a..be800ae 100644
--- a/test-server/http.js
+++ b/test-server/http.js
@@ -55,6 +55,7 @@ const GetSVGLayoutConfiguration = () => {
Type: 'Chassis',
MaxWidth: 500,
MinWidth: 200,
+ Width: 200,
DefaultChildType: 'Trou',
Style: {
fillOpacity: 1,
@@ -63,7 +64,7 @@ const GetSVGLayoutConfiguration = () => {
fill: '#d3c9b7',
},
ShowSelfDimensions: true,
- IsDimensionBorrower: true
+ IsDimensionBorrower: true,
},
{
Type: 'Trou',
@@ -219,19 +220,13 @@ const FillHoleWithChassis = (request) => {
const SplitRemplissage = (request) => {
const lstModels = [
{
- properties: {
- type: 'Remplissage'
- }
+ Type: 'Remplissage'
},
{
- properties: {
- type: 'Montant'
- }
+ Type: 'Montant'
},
{
- properties: {
- type: 'Remplissage'
- }
+ Type: 'Remplissage'
},
];
diff --git a/vite.config.ts b/vite.config.ts
index 6549182..f4b4d95 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -3,5 +3,7 @@ import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()]
+ plugins: [
+ react()
+ ]
});