From 08f98bd40f230900d6de11fce8f18aa3c251246d Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 10:44:40 +0200 Subject: [PATCH 01/14] Fix Floating button not shifting when symbols is open --- src/Components/UI/UI.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Components/UI/UI.tsx b/src/Components/UI/UI.tsx index eab588a..bbf9b27 100644 --- a/src/Components/UI/UI.tsx +++ b/src/Components/UI/UI.tsx @@ -47,10 +47,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]'; } From 191fa9cfa2e82f261a723c61e2eedfafdfbbe063 Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 10:46:32 +0200 Subject: [PATCH 02/14] Fix flex not applying when moving symbols --- .../Editor/Actions/ContainerOperations.ts | 18 +----------------- .../Editor/Actions/SymbolOperations.ts | 5 +++-- src/Components/Editor/Behaviors/Behaviors.ts | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index 9b2b460..104eb04 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, ApplyBehaviorsOnSiblings } from '../Behaviors/Behaviors'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import Swal from 'sweetalert2'; import { ApplyMargin, TransformX } from '../../../utils/svg'; @@ -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/SymbolOperations.ts b/src/Components/Editor/Actions/SymbolOperations.ts index e3c89fa..9504c55 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, ApplyBehaviorsOnSiblings } from '../Behaviors/Behaviors'; import { GetCurrentHistory, UpdateCounters } from '../Editor'; export function AddSymbol( @@ -150,6 +149,8 @@ export function OnPropertyChange( } ApplyBehaviors(container, newSymbols); + + ApplyBehaviorsOnSiblings(container, newSymbols); }); history.push({ diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index 928f25d..feae69e 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -35,3 +35,19 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map): void { + if (newContainer.parent === null || newContainer.parent === undefined) { + return; + } + + newContainer.parent.children + .filter(container => newContainer !== container) + .forEach(container => ApplyBehaviors(container, symbols)); +} From 5fdee602f12fcd068fdb42e113db7e6afc6e624f Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 10:55:34 +0200 Subject: [PATCH 03/14] Fix ApplyBehaviorsOnSiblings type and performance --- src/Components/Editor/Behaviors/Behaviors.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index feae69e..f00bba7 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -42,12 +42,17 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map): void { +export function ApplyBehaviorsOnSiblings(newContainer: IContainerModel, symbols: Map): void { if (newContainer.parent === null || newContainer.parent === undefined) { return; } newContainer.parent.children - .filter(container => newContainer !== container) - .forEach(container => ApplyBehaviors(container, symbols)); + .forEach((container: IContainerModel) => { + if (container === newContainer) { + return; + } + + ApplyBehaviors(container, symbols); + }); } From 5fdbd771ff75adc71edf8f736c4165bbc9022ae2 Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 10:57:55 +0200 Subject: [PATCH 04/14] Add try catch to behaviors --- src/Components/Editor/Behaviors/Behaviors.ts | 39 +++++++++++-------- .../Editor/Behaviors/FlexBehaviors.ts | 3 +- src/utils/simplex.ts | 2 +- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index f00bba7..4dac2de 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -13,26 +13,31 @@ import { ApplySymbol } from './SymbolBehaviors'; * @returns Updated container */ export function ApplyBehaviors(container: IContainerModel, symbols: Map): IContainerModel { - if (container.properties.isAnchor) { - ApplyAnchor(container); - } - - Flex(container); - - ApplyRigidBody(container); - - const symbol = symbols.get(container.properties.linkedSymbolId); - if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { - ApplySymbol(container, symbol); - } - - if (APPLY_BEHAVIORS_ON_CHILDREN) { - // Apply DFS by recursion - for (const child of container.children) { - ApplyBehaviors(child, symbols); + try { + if (container.properties.isAnchor) { + ApplyAnchor(container); } + + Flex(container); + + ApplyRigidBody(container); + + const symbol = symbols.get(container.properties.linkedSymbolId); + if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { + ApplySymbol(container, symbol); + } + + if (APPLY_BEHAVIORS_ON_CHILDREN) { + // Apply DFS by recursion + for (const child of container.children) { + ApplyBehaviors(child, symbols); + } + } + } catch (error) { + console.warn(error); } + return container; } diff --git a/src/Components/Editor/Behaviors/FlexBehaviors.ts b/src/Components/Editor/Behaviors/FlexBehaviors.ts index 3b8e34e..498ca5a 100644 --- a/src/Components/Editor/Behaviors/FlexBehaviors.ts +++ b/src/Components/Editor/Behaviors/FlexBehaviors.ts @@ -47,8 +47,7 @@ function FlexGroup(flexibleGroup: IFlexibleGroup): void { // 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; + throw new Error('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.'); } const maxMinWidths = Math.max(...minWidths); diff --git a/src/utils/simplex.ts b/src/utils/simplex.ts index ea30731..27f54ba 100644 --- a/src/utils/simplex.ts +++ b/src/utils/simplex.ts @@ -183,7 +183,7 @@ function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] { } if (tries === 0) { - throw new Error('[Flex]Simplexe: Could not find a solution'); + throw new Error('[Flex] Simplexe: Could not find a solution'); } return matrix; From 3feae9367b9a959731c20619d22bab79064054f7 Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 11:32:19 +0200 Subject: [PATCH 05/14] Fix symbol behavior not imposing its position when anchor is enabled --- src/Components/Editor/Behaviors/Behaviors.ts | 10 ++--- .../Editor/Behaviors/FlexBehaviors.ts | 45 ++++++++++++++++--- vite.config.ts | 4 +- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index 4dac2de..c35a37f 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -14,6 +14,11 @@ import { ApplySymbol } from './SymbolBehaviors'; */ export function ApplyBehaviors(container: IContainerModel, symbols: Map): IContainerModel { try { + const symbol = symbols.get(container.properties.linkedSymbolId); + if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { + ApplySymbol(container, symbol); + } + if (container.properties.isAnchor) { ApplyAnchor(container); } @@ -22,11 +27,6 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map sibling.properties.isFlex); + const { + flexibleContainers, + nonFlexibleContainers + } = SeparateFlexibleContainers(children); 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) { + const checkSumMinWidthsIsFitting = minimumPossibleWidth > requiredMaxWidth; + if (checkSumMinWidthsIsFitting) { // Swal.fire({ // icon: 'error', // title: 'Cannot fit!', @@ -87,6 +94,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/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() + ] }); From 35e9421ae8c73d5c34dc2ce4fea15e159bac5c47 Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 11:33:45 +0200 Subject: [PATCH 06/14] Hide behaviors handled error messages --- src/Components/Editor/Behaviors/Behaviors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Components/Editor/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index c35a37f..a6ebb88 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -34,10 +34,9 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map Date: Wed, 31 Aug 2022 11:35:24 +0200 Subject: [PATCH 07/14] Hide console.warn --- src/Components/App/Actions/MenuActions.ts | 2 +- src/Components/Editor/Behaviors/RigidBodyBehaviors.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index 46a973f..bdf2e71 100644 --- a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts +++ b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts @@ -174,7 +174,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!`, From 4588aa944342dd704b367906fdeb0d18d1ee1bdc Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 15:34:25 +0200 Subject: [PATCH 08/14] Rename DefaultX, DefaultY to X and Y + add docs --- .../Editor/Actions/ContainerOperations.ts | 8 +- src/Interfaces/IAvailableContainer.ts | 78 ++++++++++++++++++- 2 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index 104eb04..60029cb 100644 --- a/src/Components/Editor/Actions/ContainerOperations.ts +++ b/src/Components/Editor/Actions/ContainerOperations.ts @@ -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; @@ -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; diff --git a/src/Interfaces/IAvailableContainer.ts b/src/Interfaces/IAvailableContainer.ts index 5818c21..767a52c 100644 --- a/src/Interfaces/IAvailableContainer.ts +++ b/src/Interfaces/IAvailableContainer.ts @@ -7,20 +7,80 @@ import { IMargin } from './IMargin'; /** Model of available container used in application configuration */ export interface IAvailableContainer { + /** type */ Type: string - DefaultX?: number - DefaultY?: number + + /** horizontal offset */ + X?: number + + /** vertical offset */ + Y?: number + + /** width */ Width?: number + + /** height */ Height?: number + + /** + * Minimum width (min=1) + * Allows the container to set isRigidBody to false when it gets squeezed + * by an anchor + */ MinWidth?: number + + /** + * Maximum width + */ MaxWidth?: number + + /** margin */ Margin?: IMargin + + /** true if anchor, false otherwise */ IsAnchor?: boolean + + /** true if flex, false otherwise */ IsFlex?: boolean + + /** Method used on container add */ AddMethod?: AddMethod + + /** Horizontal alignment, also determines the visual location of x {Left = 0, Center, Right } */ XPositionReference?: XPositionReference + + /** + * (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 : + * ``` + * ` + * + * + * + * ` + * ``` + */ 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 } From 8034652bdb024aafa91404977f3ff534103304fd Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 16:15:38 +0200 Subject: [PATCH 09/14] Changed api call body's container model to available container + Fix API nullable values --- src/Components/API/api.ts | 4 ++++ src/Components/Editor/Actions/ContextMenuActions.ts | 4 ++-- src/Components/Editor/Editor.tsx | 2 +- src/Interfaces/ISetContainerListResponse.ts | 4 ++-- test-server/http.js | 12 +++--------- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Components/API/api.ts b/src/Components/API/api.ts index 72a4947..510428d 100644 --- a/src/Components/API/api.ts +++ b/src/Components/API/api.ts @@ -42,6 +42,9 @@ export async function SetContainerList(request: ISetContainerListRequest): Promi if (window.fetch) { return await fetch(url, { method: 'POST', + headers: new Headers({ + '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/Editor/Actions/ContextMenuActions.ts b/src/Components/Editor/Actions/ContextMenuActions.ts index 91659f8..260ce73 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,7 +104,7 @@ function HandleReplace( 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, types, diff --git a/src/Components/Editor/Editor.tsx b/src/Components/Editor/Editor.tsx index 618967f..b1b6243 100644 --- a/src/Components/Editor/Editor.tsx +++ b/src/Components/Editor/Editor.tsx @@ -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; } 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/test-server/http.js b/test-server/http.js index 650c48a..4c84bfd 100644 --- a/test-server/http.js +++ b/test-server/http.js @@ -219,19 +219,13 @@ const FillHoleWithChassis = (request) => { const SplitRemplissage = (request) => { const lstModels = [ { - properties: { - type: 'Remplissage' - } + Type: 'Remplissage' }, { - properties: { - type: 'Montant' - } + Type: 'Montant' }, { - properties: { - type: 'Remplissage' - } + Type: 'Remplissage' }, ]; From f6953e42df03ad4b95e1c3cad9144f73c7ff23f2 Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 17:27:26 +0200 Subject: [PATCH 10/14] Implement framerate limiter --- src/Components/SVG/SVG.tsx | 40 ++++++++++++++++++++++++++++++++++---- src/utils/default.ts | 1 + 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index e79827c..7af91b1 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,12 @@ 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) { + React.useEffect(() => { + svgViewer?.current?.fitToViewer(); + }, []); +} diff --git a/src/utils/default.ts b/src/utils/default.ts index 49e7c56..792723a 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -33,6 +33,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 = 120; /** * Returns the default editor state given the configuration From cae628537c6a73ffaf8058ab1874fa16844bf0cc Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 17:47:37 +0200 Subject: [PATCH 11/14] Fix UseFitOnce --- src/Components/SVG/SVG.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Components/SVG/SVG.tsx b/src/Components/SVG/SVG.tsx index 7af91b1..0cc6311 100644 --- a/src/Components/SVG/SVG.tsx +++ b/src/Components/SVG/SVG.tsx @@ -124,8 +124,10 @@ export function SVG(props: ISVGProps): JSX.Element { ); } -function UseFitOnce(svgViewer: React.RefObject) { + +function UseFitOnce(svgViewer: React.RefObject): void { React.useEffect(() => { svgViewer?.current?.fitToViewer(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); } From 4d4ecd67d0e67b847b25e5e5f78673037c6fc1e5 Mon Sep 17 00:00:00 2001 From: Eric NGUYEN Date: Wed, 31 Aug 2022 18:08:57 +0200 Subject: [PATCH 12/14] Reduce framerate to 60fps --- src/utils/default.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/default.ts b/src/utils/default.ts index 792723a..5af68aa 100644 --- a/src/utils/default.ts +++ b/src/utils/default.ts @@ -33,7 +33,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 = 120; +export const MAX_FRAMERATE = 60; /** * Returns the default editor state given the configuration From 353f461f4b33740828222130ffcdea672e36f554 Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Mon, 5 Sep 2022 07:56:45 +0000 Subject: [PATCH 13/14] Merged PR 175: Implement drag drop - [x] Implement drag drop to create an element at a specific index - Add Swap behavrior - Implement max contraints with simplex ~~- [ ] Implement drag drop to swap two container that flex~~ - Fixes tries number for simplex it can now go up to 2 * number of containers - Fixes flex calling another flex behavior when not needed (remember that flex behavior is the only behavior that needs to communicate with siblings) - Fix max width being ignored in input group --- .../ContainerProperties/ContainerForm.tsx | 1 + .../Editor/Actions/ContainerOperations.ts | 8 +- .../Editor/Actions/SymbolOperations.ts | 4 +- .../Editor/Behaviors/AnchorBehaviors.ts | 2 +- src/Components/Editor/Behaviors/Behaviors.ts | 47 ++++---- .../Editor/Behaviors/FlexBehaviors.ts | 17 +-- .../Editor/Behaviors/SwapBehaviors.ts | 31 ++++++ src/Components/Editor/Editor.tsx | 12 ++- .../ElementsSidebar/ElementsSidebar.test.tsx | 5 + .../ElementsSidebar/ElementsSidebar.tsx | 101 +++++++++++++++++- src/Components/InputGroup/InputGroup.tsx | 2 + src/Components/UI/UI.tsx | 2 + src/utils/simplex.ts | 47 ++++---- 13 files changed, 220 insertions(+), 59 deletions(-) create mode 100644 src/Components/Editor/Behaviors/SwapBehaviors.ts diff --git a/src/Components/ContainerProperties/ContainerForm.tsx b/src/Components/ContainerProperties/ContainerForm.tsx index 711d3b2..b427484 100644 --- a/src/Components/ContainerProperties/ContainerForm.tsx +++ b/src/Components/ContainerProperties/ContainerForm.tsx @@ -119,6 +119,7 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element { inputClassName='' type='number' min={props.properties.minWidth} + max={props.properties.maxWidth} 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))} isDisabled={props.properties.isFlex} /> diff --git a/src/Components/Editor/Actions/ContainerOperations.ts b/src/Components/Editor/Actions/ContainerOperations.ts index 60029cb..b1d24a9 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, ApplyBehaviorsOnSiblings } 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 @@ -285,7 +285,7 @@ export function AddContainers( ApplyBehaviors(newContainer, current.symbols); // 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); @@ -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); diff --git a/src/Components/Editor/Actions/SymbolOperations.ts b/src/Components/Editor/Actions/SymbolOperations.ts index 9504c55..a187e11 100644 --- a/src/Components/Editor/Actions/SymbolOperations.ts +++ b/src/Components/Editor/Actions/SymbolOperations.ts @@ -5,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, ApplyBehaviorsOnSiblings } from '../Behaviors/Behaviors'; +import { ApplyBehaviors, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors'; import { GetCurrentHistory, UpdateCounters } from '../Editor'; export function AddSymbol( @@ -150,7 +150,7 @@ export function OnPropertyChange( ApplyBehaviors(container, newSymbols); - ApplyBehaviorsOnSiblings(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 a6ebb88..7dfc78a 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -4,6 +4,7 @@ import { APPLY_BEHAVIORS_ON_CHILDREN } from '../../../utils/default'; import { ApplyAnchor } from './AnchorBehaviors'; import { Flex } from './FlexBehaviors'; import { ApplyRigidBody } from './RigidBodyBehaviors'; +import { ApplySwap } from './SwapBehaviors'; import { ApplySymbol } from './SymbolBehaviors'; /** @@ -13,28 +14,26 @@ import { ApplySymbol } from './SymbolBehaviors'; * @returns Updated container */ export function ApplyBehaviors(container: IContainerModel, symbols: Map): IContainerModel { - try { - const symbol = symbols.get(container.properties.linkedSymbolId); - if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { - ApplySymbol(container, symbol); + const symbol = symbols.get(container.properties.linkedSymbolId); + if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { + ApplySymbol(container, symbol); + } + + if (container.properties.isAnchor) { + ApplyAnchor(container); + } + + ApplySwap(container); + + Flex(container); + + ApplyRigidBody(container); + + if (APPLY_BEHAVIORS_ON_CHILDREN) { + // Apply DFS by recursion + for (const child of container.children) { + ApplyBehaviors(child, symbols); } - - if (container.properties.isAnchor) { - ApplyAnchor(container); - } - - Flex(container); - - ApplyRigidBody(container); - - if (APPLY_BEHAVIORS_ON_CHILDREN) { - // Apply DFS by recursion - for (const child of container.children) { - ApplyBehaviors(child, symbols); - } - } - } catch (error) { - console.debug(error); } return container; @@ -46,7 +45,7 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map): void { +export function ApplyBehaviorsOnSiblingsChildren(newContainer: IContainerModel, symbols: Map): void { if (newContainer.parent === null || newContainer.parent === undefined) { return; } @@ -57,6 +56,8 @@ export function ApplyBehaviorsOnSiblings(newContainer: IContainerModel, symbols: return; } - ApplyBehaviors(container, symbols); + 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 08eabae..9e2a681 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'; @@ -49,16 +50,16 @@ function FlexGroup(flexibleGroup: IFlexibleGroup): void { const checkSumMinWidthsIsFitting = minimumPossibleWidth > requiredMaxWidth; if (checkSumMinWidthsIsFitting) { - // Swal.fire({ - // icon: 'error', - // title: 'Cannot fit!', - // text: 'Cannot fit at all even when squeezing all flex containers to the minimum.' - // }); + Swal.fire({ + icon: 'error', + title: 'Cannot fit!', + text: 'Cannot fit at all even when squeezing all flex containers to the minimum.' + }); throw new Error('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.'); } 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; @@ -79,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++) { 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 b1b6243..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'; @@ -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..8bb83f6 100644 --- a/src/Components/InputGroup/InputGroup.tsx +++ b/src/Components/InputGroup/InputGroup.tsx @@ -12,6 +12,7 @@ interface IInputGroupProps { defaultValue?: string defaultChecked?: boolean min?: number + max?: number isDisabled?: boolean onChange?: (event: React.ChangeEvent) => void } @@ -44,6 +45,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/UI/UI.tsx b/src/Components/UI/UI.tsx index bbf9b27..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 @@ -87,6 +88,7 @@ export function UI(props: IUIProps): JSX.Element { isHistoryOpen={isHistoryOpen} onPropertyChange={props.onPropertyChange} selectContainer={props.selectContainer} + addContainer={props.addContainerAt} /> 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) { + console.table(matrix); throw new Error('[Flex] Simplexe: Could not find a solution'); } - + console.debug(`Simplex was solved in ${maxTries - tries} tries`); return matrix; } From 443a15e150d2476536c7731415d78efc0dae077f Mon Sep 17 00:00:00 2001 From: Eric Nguyen Date: Thu, 8 Sep 2022 10:29:44 +0000 Subject: [PATCH 14/14] Merged PR 179: Fix bugs about flex and context menu (see desc) + disable hard rigid behavior + add missing properties to form + Clean up css - Clean up some css class - Fix wrong order when applying flex - Fix Replace behavior not working because previous container was still existing - Disable hard rigid behavior which disallow two container to overlap - Add ENABLE_FLEX, ENABLE_HARD_RIGID ENABLE_SWAP - Add missing form properties with dimensions - Update readme --- README.md | 39 ++++++++++++++-- src/Components/API/api.ts | 2 +- src/Components/App/App.scss | 0 src/Components/App/App.tsx | 3 +- src/Components/Bar/Bar.tsx | 2 +- .../ContainerProperties/ContainerForm.tsx | 46 ++++++++++++++++++- .../Editor/Actions/ContainerOperations.ts | 6 +-- .../Editor/Actions/ContextMenuActions.ts | 27 ++++++----- src/Components/Editor/Behaviors/Behaviors.ts | 10 ++-- .../Editor/Behaviors/FlexBehaviors.ts | 12 ++--- .../Editor/Behaviors/RigidBodyBehaviors.ts | 7 ++- src/Components/InputGroup/InputGroup.tsx | 7 +-- src/index.scss | 23 ++++++++-- src/main.tsx | 3 ++ src/utils/default.ts | 13 ++++++ test-server/http.js | 3 +- 16 files changed, 158 insertions(+), 45 deletions(-) delete mode 100644 src/Components/App/App.scss 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 510428d..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'; @@ -43,6 +42,7 @@ export async function SetContainerList(request: ISetContainerListRequest): Promi return await fetch(url, { method: 'POST', headers: new Headers({ + // eslint-disable-next-line @typescript-eslint/naming-convention 'Content-Type': 'application/json' }), body: dataParsed 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('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 b1d24a9..d0dc7e4 100644 --- a/src/Components/Editor/Actions/ContainerOperations.ts +++ b/src/Components/Editor/Actions/ContainerOperations.ts @@ -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 @@ -287,9 +290,6 @@ export function AddContainers( // Then, apply the behaviors on its siblings (mostly for flex) ApplyBehaviorsOnSiblingsChildren(newContainer, current.symbols); - // Sort the parent children by x - UpdateParentChildrenList(parentClone); - // Add to the list of container id for logging purpose containerIds.push(newContainer.properties.id); }); diff --git a/src/Components/Editor/Actions/ContextMenuActions.ts b/src/Components/Editor/Actions/ContextMenuActions.ts index 260ce73..64f37fc 100644 --- a/src/Components/Editor/Actions/ContextMenuActions.ts +++ b/src/Components/Editor/Actions/ContextMenuActions.ts @@ -105,27 +105,30 @@ function HandleReplace( const index = selectedContainer.parent.children.indexOf(selectedContainer); const types = response.Containers.map(container => container.Type); - const newHistoryBeforeDelete = AddContainers( - index + 1, - types, - selectedContainer.properties.parentId, - configuration, + + 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/Behaviors/Behaviors.ts b/src/Components/Editor/Behaviors/Behaviors.ts index 7dfc78a..116917b 100644 --- a/src/Components/Editor/Behaviors/Behaviors.ts +++ b/src/Components/Editor/Behaviors/Behaviors.ts @@ -1,6 +1,6 @@ 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'; @@ -23,11 +23,15 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map sibling.properties.minWidth); @@ -50,12 +54,8 @@ function FlexGroup(flexibleGroup: IFlexibleGroup): void { const checkSumMinWidthsIsFitting = minimumPossibleWidth > requiredMaxWidth; if (checkSumMinWidthsIsFitting) { - Swal.fire({ - icon: 'error', - title: 'Cannot fit!', - text: 'Cannot fit at all even when squeezing all flex containers to the minimum.' - }); - throw new Error('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.'); + console.warn('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.'); + return; } const maxMinWidths = Math.max(...minWidths); diff --git a/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts b/src/Components/Editor/Behaviors/RigidBodyBehaviors.ts index bdf2e71..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; } diff --git a/src/Components/InputGroup/InputGroup.tsx b/src/Components/InputGroup/InputGroup.tsx index 8bb83f6..68b5910 100644 --- a/src/Components/InputGroup/InputGroup.tsx +++ b/src/Components/InputGroup/InputGroup.tsx @@ -17,12 +17,7 @@ interface IInputGroupProps { 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 <> 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 5af68aa..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; diff --git a/test-server/http.js b/test-server/http.js index 4c84bfd..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',