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
This commit is contained in:
Eric Nguyen 2022-09-08 10:29:44 +00:00
parent 353f461f4b
commit 443a15e150
16 changed files with 158 additions and 45 deletions

View file

@ -13,8 +13,9 @@ An svg layout designer.
Requierements : Requierements :
- NodeJS - NodeJS
- npm - npm
- pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory)
- Chrome > 98 - Chrome > 98
- pnpm (optional but recommanded unless you prefer having a huge `node_modules` directory)
- [`git-lfs`](https://git-lfs.github.com/) (in order to clone the documentation)
# Developping # Developping
@ -22,9 +23,6 @@ Run `npm ci`
Run `npm run dev` Run `npm run dev`
# Deploy # Deploy
Run `npm ci` Run `npm ci`
@ -72,4 +70,35 @@ bun run http.js
The web server will be running at `http://localhost:5000` The web server will be running at `http://localhost:5000`
Configure the file `.env.development` with the url Configure the file `.env.development` with the url
# Recommanded tools
- [VSCode](https://code.visualstudio.com/)
- [React DevTools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi)
- [vscode-tailwindcss](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)
- [vscode-eslint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
# Setup debuggin with chrome
Inside `.vscode/settings.json`, set the following :
```json
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome against localhost",
"url": "http://localhost:5173",
"webRoot": "${workspaceFolder}",
}
]
}
```
Change the `url` to the dev server url. Set the `runtimeExecutable` to you favorite chromium browser.

View file

@ -1,5 +1,4 @@
import { IConfiguration } from '../../Interfaces/IConfiguration'; import { IConfiguration } from '../../Interfaces/IConfiguration';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest'; import { ISetContainerListRequest } from '../../Interfaces/ISetContainerListRequest';
import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse'; import { ISetContainerListResponse } from '../../Interfaces/ISetContainerListResponse';
import { GetCircularReplacer } from '../../utils/saveload'; import { GetCircularReplacer } from '../../utils/saveload';
@ -43,6 +42,7 @@ export async function SetContainerList(request: ISetContainerListRequest): Promi
return await fetch(url, { return await fetch(url, {
method: 'POST', method: 'POST',
headers: new Headers({ headers: new Headers({
// eslint-disable-next-line @typescript-eslint/naming-convention
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}), }),
body: dataParsed body: dataParsed

View file

@ -1,5 +1,4 @@
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import './App.scss';
import { MainMenu } from '../MainMenu/MainMenu'; import { MainMenu } from '../MainMenu/MainMenu';
import { ContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel } from '../../Interfaces/IContainerModel';
import { Editor } from '../Editor/Editor'; import { Editor } from '../Editor/Editor';
@ -78,7 +77,7 @@ export function App(props: IAppProps): JSX.Element {
} }
return ( return (
<div className='bg-blue-100 h-full w-full'> <div className='mainmenu-bg'>
<MainMenu <MainMenu
newEditor={() => NewEditor( newEditor={() => NewEditor(
setEditorState, setLoaded setEditorState, setLoaded

View file

@ -16,7 +16,7 @@ export const BAR_WIDTH = 64; // 4rem
export function Bar(props: IBarProps): JSX.Element { export function Bar(props: IBarProps): JSX.Element {
return ( return (
<div className='fixed z-20 flex flex-col top-0 left-0 h-full w-16 bg-slate-100'> <div className='bar'>
<BarIcon <BarIcon
isActive={props.isSidebarOpen} isActive={props.isSidebarOpen}
title='Components' title='Components'

View file

@ -4,6 +4,7 @@ import { PropertyType } from '../../Enums/PropertyType';
import { XPositionReference } from '../../Enums/XPositionReference'; import { XPositionReference } from '../../Enums/XPositionReference';
import { IContainerProperties } from '../../Interfaces/IContainerProperties'; import { IContainerProperties } from '../../Interfaces/IContainerProperties';
import { ISymbolModel } from '../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../Interfaces/ISymbolModel';
import { SHOW_BORROWER_DIMENSIONS, SHOW_CHILDREN_DIMENSIONS, SHOW_SELF_DIMENSIONS } from '../../utils/default';
import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, RestoreX, TransformX } from '../../utils/svg'; import { ApplyWidthMargin, ApplyXMargin, RemoveWidthMargin, RemoveXMargin, RestoreX, TransformX } from '../../utils/svg';
import { InputGroup } from '../InputGroup/InputGroup'; import { InputGroup } from '../InputGroup/InputGroup';
import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons'; import { RadioGroupButtons } from '../RadioGroupButtons/RadioGroupButtons';
@ -34,7 +35,7 @@ function GetCSSInputs(properties: IContainerProperties,
export function ContainerForm(props: IContainerFormProps): JSX.Element { export function ContainerForm(props: IContainerFormProps): JSX.Element {
return ( return (
<div className='grid grid-cols-2 gap-y-4'> <div className='grid grid-cols-2 gap-y-4 items-center'>
<InputGroup <InputGroup
labelText='Name' labelText='Name'
inputKey='id' inputKey='id'
@ -228,6 +229,49 @@ export function ContainerForm(props: IContainerFormProps): JSX.Element {
value={props.properties.linkedSymbolId ?? ''} value={props.properties.linkedSymbolId ?? ''}
onChange={(event) => props.onChange('linkedSymbolId', event.target.value)} /> onChange={(event) => props.onChange('linkedSymbolId', event.target.value)} />
{GetCSSInputs(props.properties, props.onChange)} {GetCSSInputs(props.properties, props.onChange)}
{
SHOW_SELF_DIMENSIONS &&
<InputGroup
labelText='Show dimension'
inputKey='showSelfDimensions'
labelClassName=''
inputClassName=''
type='checkbox'
checked={props.properties.showSelfDimensions}
onChange={(event) => props.onChange('showSelfDimensions', event.target.checked)} />
}
{
SHOW_CHILDREN_DIMENSIONS &&
<InputGroup
labelText='Show overall dimension of its children'
inputKey='showChildrenDimensions'
labelClassName=''
inputClassName=''
type='checkbox'
checked={props.properties.showChildrenDimensions}
onChange={(event) => props.onChange('showChildrenDimensions', event.target.checked)} />
}
{
SHOW_BORROWER_DIMENSIONS &&
<>
<InputGroup
labelText='Mark the position'
inputKey='markPositionToDimensionBorrower'
labelClassName=''
inputClassName=''
type='checkbox'
checked={props.properties.markPositionToDimensionBorrower}
onChange={(event) => props.onChange('markPositionToDimensionBorrower', event.target.checked)} />
<InputGroup
labelText='Show dimension with marked children'
inputKey='isDimensionBorrower'
labelClassName=''
inputClassName=''
type='checkbox'
checked={props.properties.isDimensionBorrower}
onChange={(event) => props.onChange('isDimensionBorrower', event.target.checked)} />
</>
}
</div> </div>
); );
} }

View file

@ -276,6 +276,9 @@ export function AddContainers(
parentClone.children.splice(index, 0, newContainer); parentClone.children.splice(index, 0, newContainer);
} }
// Sort the parent children by x
UpdateParentChildrenList(parentClone);
/// Handle behaviors here /// /// Handle behaviors here ///
// Initialize default children of the container // Initialize default children of the container
@ -287,9 +290,6 @@ export function AddContainers(
// Then, apply the behaviors on its siblings (mostly for flex) // Then, apply the behaviors on its siblings (mostly for flex)
ApplyBehaviorsOnSiblingsChildren(newContainer, current.symbols); ApplyBehaviorsOnSiblingsChildren(newContainer, current.symbols);
// Sort the parent children by x
UpdateParentChildrenList(parentClone);
// Add to the list of container id for logging purpose // Add to the list of container id for logging purpose
containerIds.push(newContainer.properties.id); containerIds.push(newContainer.properties.id);
}); });

View file

@ -105,27 +105,30 @@ function HandleReplace(
const index = selectedContainer.parent.children.indexOf(selectedContainer); const index = selectedContainer.parent.children.indexOf(selectedContainer);
const types = response.Containers.map(container => container.Type); const types = response.Containers.map(container => container.Type);
const newHistoryBeforeDelete = AddContainers(
index + 1, const newHistoryAfterDelete = DeleteContainer(
types, selectedContainer.properties.id,
selectedContainer.properties.parentId,
configuration,
history, history,
historyCurrentStep historyCurrentStep
); );
const newHistoryAfterDelete = DeleteContainer( const newHistoryBeforeDelete = AddContainers(
selectedContainer.properties.id, index,
newHistoryBeforeDelete, types,
newHistoryBeforeDelete.length - 1 selectedContainer.properties.parentId,
configuration,
newHistoryAfterDelete,
newHistoryAfterDelete.length - 1
); );
// Remove AddContainers from history // Remove AddContainers from history
newHistoryAfterDelete.splice(newHistoryAfterDelete.length - 2, 1); if (import.meta.env.PROD) {
newHistoryBeforeDelete.splice(newHistoryBeforeDelete.length - 2, 1);
}
// Rename the last action by Replace // Rename the last action by Replace
newHistoryAfterDelete[newHistoryAfterDelete.length - 1].lastAction = newHistoryBeforeDelete[newHistoryBeforeDelete.length - 1].lastAction =
`Replace ${selectedContainer.properties.id} by [${types.join(', ')}]`; `Replace ${selectedContainer.properties.id} by [${types.join(', ')}]`;
return newHistoryAfterDelete; return newHistoryBeforeDelete;
} }

View file

@ -1,6 +1,6 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { APPLY_BEHAVIORS_ON_CHILDREN } from '../../../utils/default'; import { APPLY_BEHAVIORS_ON_CHILDREN, ENABLE_RIGID, ENABLE_SWAP } from '../../../utils/default';
import { ApplyAnchor } from './AnchorBehaviors'; import { ApplyAnchor } from './AnchorBehaviors';
import { Flex } from './FlexBehaviors'; import { Flex } from './FlexBehaviors';
import { ApplyRigidBody } from './RigidBodyBehaviors'; import { ApplyRigidBody } from './RigidBodyBehaviors';
@ -23,11 +23,15 @@ export function ApplyBehaviors(container: IContainerModel, symbols: Map<string,
ApplyAnchor(container); ApplyAnchor(container);
} }
ApplySwap(container); if (ENABLE_SWAP) {
ApplySwap(container);
}
Flex(container); Flex(container);
ApplyRigidBody(container); if (ENABLE_RIGID) {
ApplyRigidBody(container);
}
if (APPLY_BEHAVIORS_ON_CHILDREN) { if (APPLY_BEHAVIORS_ON_CHILDREN) {
// Apply DFS by recursion // Apply DFS by recursion

View file

@ -38,6 +38,10 @@ function FlexGroup(flexibleGroup: IFlexibleGroup): void {
nonFlexibleContainers nonFlexibleContainers
} = SeparateFlexibleContainers(children); } = SeparateFlexibleContainers(children);
if (flexibleContainers.length === 0) {
return;
}
const minWidths = flexibleContainers const minWidths = flexibleContainers
.map(sibling => sibling.properties.minWidth); .map(sibling => sibling.properties.minWidth);
@ -50,12 +54,8 @@ function FlexGroup(flexibleGroup: IFlexibleGroup): void {
const checkSumMinWidthsIsFitting = minimumPossibleWidth > requiredMaxWidth; const checkSumMinWidthsIsFitting = minimumPossibleWidth > requiredMaxWidth;
if (checkSumMinWidthsIsFitting) { if (checkSumMinWidthsIsFitting) {
Swal.fire({ console.warn('[FlexBehavior] Cannot fit at all even when squeezing all flex containers to the minimum.');
icon: 'error', return;
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); const maxMinWidths = Math.max(...minWidths);

View file

@ -9,6 +9,7 @@
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISizePointer } from '../../../Interfaces/ISizePointer'; import { ISizePointer } from '../../../Interfaces/ISizePointer';
import { ENABLE_HARD_RIGID } from '../../../utils/default';
/** /**
* "Transform the container into a rigid body" * "Transform the container into a rigid body"
@ -23,7 +24,11 @@ export function ApplyRigidBody(
container: IContainerModel container: IContainerModel
): IContainerModel { ): IContainerModel {
container = ConstraintBodyInsideParent(container); container = ConstraintBodyInsideParent(container);
container = ConstraintBodyInsideUnallocatedWidth(container);
if (ENABLE_HARD_RIGID) {
container = ConstraintBodyInsideUnallocatedWidth(container);
}
return container; return container;
} }

View file

@ -17,12 +17,7 @@ interface IInputGroupProps {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
} }
const className = ` const className = 'input-group';
w-full
text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
bg-white border-2 border-white rounded-lg placeholder-gray-800
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none`;
export function InputGroup(props: IInputGroupProps): JSX.Element { export function InputGroup(props: IInputGroupProps): JSX.Element {
return <> return <>

View file

@ -18,7 +18,6 @@
.elements-sidebar-row { .elements-sidebar-row {
@apply pl-6 pr-6 pt-2 pb-2 w-full @apply pl-6 pr-6 pt-2 pb-2 w-full
} }
.symbols-sidebar-row { .symbols-sidebar-row {
@apply elements-sidebar-row @apply elements-sidebar-row
} }
@ -27,6 +26,10 @@
@apply transition-all w-full h-auto p-4 flex @apply transition-all w-full h-auto p-4 flex
} }
.mainmenu-bg {
@apply bg-blue-100 h-full w-full
}
.mainmenu-btn { .mainmenu-btn {
@apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg @apply transition-all bg-blue-100 hover:bg-blue-200 text-blue-700 text-lg font-semibold p-8 rounded-lg
} }
@ -42,7 +45,13 @@
} }
.floating-btn { .floating-btn {
@apply h-full w-full text-white align-middle items-center justify-center @apply h-full w-full text-white align-middle
items-center justify-center
}
.bar {
@apply fixed z-20 flex flex-col top-0 left-0
h-full w-16 bg-slate-100
} }
.bar-btn { .bar-btn {
@ -64,10 +73,18 @@
text-gray-800 bg-slate-100 text-gray-800 bg-slate-100
dark:text-white dark:bg-gray-800 dark:text-white dark:bg-gray-800
text-xs font-bold text-xs font-bold
transition-all duration-100 scale-0 origin-left; transition-all duration-100 scale-0 origin-left
} }
.contextmenu-item { .contextmenu-item {
@apply px-2 py-1 hover:bg-slate-300 text-left @apply px-2 py-1 hover:bg-slate-300 text-left
} }
.input-group {
@apply w-full
text-xs font-medium transition-all text-gray-800 mt-1 px-3 py-2
bg-white border-2 border-white rounded-lg placeholder-gray-800
focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500
disabled:bg-slate-300 disabled:text-gray-500 disabled:border-slate-300 disabled:shadow-none;
}
} }

View file

@ -11,7 +11,10 @@ function RenderRoot(root: Element | Document): void {
); );
} }
// Specific for Modeler apps
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace SVGLayoutDesigner { namespace SVGLayoutDesigner {
// eslint-disable-next-line @typescript-eslint/naming-convention
export const Render = RenderRoot; export const Render = RenderRoot;
} }

View file

@ -9,6 +9,19 @@ import { ISymbolModel } from '../Interfaces/ISymbolModel';
/// CONTAINER DEFAULTS /// /// CONTAINER DEFAULTS ///
/** Enable the swap behavior */
export const ENABLE_SWAP = false;
/** Enable the rigid behavior */
export const ENABLE_RIGID = true;
/**
* Enable the hard rigid behavior
* disallowing the container to overlap (ENABLE_RIGID must be true)
*/
export const ENABLE_HARD_RIGID = false;
/** Enalbe the text in the containers */
export const SHOW_TEXT = false; export const SHOW_TEXT = false;
export const SHOW_SELECTOR_TEXT = true; export const SHOW_SELECTOR_TEXT = true;
export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false; export const DEFAULTCHILDTYPE_ALLOW_CYCLIC = false;

View file

@ -55,6 +55,7 @@ const GetSVGLayoutConfiguration = () => {
Type: 'Chassis', Type: 'Chassis',
MaxWidth: 500, MaxWidth: 500,
MinWidth: 200, MinWidth: 200,
Width: 200,
DefaultChildType: 'Trou', DefaultChildType: 'Trou',
Style: { Style: {
fillOpacity: 1, fillOpacity: 1,
@ -63,7 +64,7 @@ const GetSVGLayoutConfiguration = () => {
fill: '#d3c9b7', fill: '#d3c9b7',
}, },
ShowSelfDimensions: true, ShowSelfDimensions: true,
IsDimensionBorrower: true IsDimensionBorrower: true,
}, },
{ {
Type: 'Trou', Type: 'Trou',