Implement new features for svg components + improve form properties (#25)
All checks were successful
continuous-integration/drone/push Build is passing

- Make Dimension an actual svg line
- Implement XPositionReference
- Select the container above after delete
- Remove DimensionLayer
- Improve form properties

Reviewed-on: https://git.siklos-chaneru.duckdns.org/Siklos/svg-layout-designer-react/pulls/25
This commit is contained in:
Siklos 2022-08-11 11:48:31 -04:00
parent d11dfec22b
commit faa058e57d
11 changed files with 119 additions and 113 deletions

View file

@ -55,7 +55,9 @@ export function DeleteContainer(
throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`); throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`);
} }
if (container === mainContainerClone) { if (container === mainContainerClone ||
container.parent === undefined ||
container.parent === null) {
// TODO: Implement alert // TODO: Implement alert
throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!'); throw new Error('[DeleteContainer] Tried to delete the main container! Deleting the main container is not allowed!');
} }
@ -64,18 +66,25 @@ export function DeleteContainer(
throw new Error('[DeleteContainer] Container model was not found among children of the main container!'); throw new Error('[DeleteContainer] Container model was not found among children of the main container!');
} }
if (container.parent != null) { const index = container.parent.children.indexOf(container);
const index = container.parent.children.indexOf(container); if (index > -1) {
if (index > -1) { container.parent.children.splice(index, 1);
container.parent.children.splice(index, 1); } else {
} throw new Error('[DeleteContainer] Could not find container among parent\'s children');
} }
// Select the previous container
// or select the one above
const SelectedContainer = findContainerById(mainContainerClone, current.SelectedContainerId) ??
container.parent.children.at(index - 1) ??
container.parent;
const SelectedContainerId = SelectedContainer.properties.id;
setHistory(history.concat([{ setHistory(history.concat([{
LastAction: `Delete container ${containerId}`, LastAction: `Delete container ${containerId}`,
MainContainer: mainContainerClone, MainContainer: mainContainerClone,
SelectedContainer: null, SelectedContainer,
SelectedContainerId: '', SelectedContainerId,
TypeCounters: Object.assign({}, current.TypeCounters) TypeCounters: Object.assign({}, current.TypeCounters)
}])); }]));
setHistoryCurrentStep(history.length); setHistoryCurrentStep(history.length);
@ -182,6 +191,7 @@ export function AddContainer(
width: properties?.Width, width: properties?.Width,
height: parentClone.properties.height, height: parentClone.properties.height,
isRigidBody: false, isRigidBody: false,
XPositionReference: properties.XPositionReference,
...properties.Style ...properties.Style
}, },
[], [],

View file

@ -1,4 +1,4 @@
import { HistoryState } from "../../Interfaces/HistoryState"; import { HistoryState } from '../../Interfaces/HistoryState';
import { Configuration } from '../../Interfaces/Configuration'; import { Configuration } from '../../Interfaces/Configuration';
import { getCircularReplacer } from '../../utils/saveload'; import { getCircularReplacer } from '../../utils/saveload';
import { ID } from '../SVG/SVG'; import { ID } from '../SVG/SVG';

View file

@ -9,7 +9,6 @@ import { MenuItem } from '../Menu/MenuItem';
import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers'; import { handleDragLeave, handleDragOver, handleLeftClick, handleOnDrop, handleRightClick } from './MouseEventHandlers';
import { Point } from '../../Interfaces/Point'; import { Point } from '../../Interfaces/Point';
interface IElementsSidebarProps { interface IElementsSidebarProps {
MainContainer: IContainerModel MainContainer: IContainerModel
isOpen: boolean isOpen: boolean
@ -108,7 +107,7 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
onLeftClick onLeftClick
); );
}; };
}, []); });
// Render // Render
let isOpenClasses = '-right-64'; let isOpenClasses = '-right-64';

View file

@ -22,15 +22,17 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
.forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange)); .forEach((pair) => handleProperties(pair, groupInput, isDynamicInput, props.onChange));
const form = isDynamicInput const form = isDynamicInput
? <div> ? <div className='grid grid-cols-2 gap-4'>
{ groupInput } { groupInput }
</div> </div>
: <form : <form
key={props.properties.id} key={props.properties.id}
onSubmit={(event) => props.onSubmit(event, props.properties as ContainerProperties)} onSubmit={(event) => props.onSubmit(event, props.properties as ContainerProperties)}
> >
<input type='submit' className='normal-btn block mx-auto' value='Submit'/> <input type='submit' className='normal-btn block mx-auto mb-4 border-2 border-blue-400 cursor-pointer' value='Submit'/>
{ groupInput } <div className='grid grid-cols-2 gap-y-4'>
{ groupInput }
</div>
</form> </form>
; ;
@ -67,16 +69,19 @@ const handleProperties = (
type = INPUT_TYPES[key]; type = INPUT_TYPES[key];
} }
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 isDisabled = ['id', 'parentId'].includes(key); const isDisabled = ['id', 'parentId'].includes(key);
const input = isDynamicInput const input = isDynamicInput
? <input ? <input
className='text-base font-medium transition-all text-gray-800 mt-1 block w-full px-3 py-2 key={key}
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
'
type={type}
id={key} id={key}
className={className}
type={type}
value={value} value={value}
checked={checked} checked={checked}
onChange={(event) => { onChange={(event) => {
@ -89,22 +94,23 @@ const handleProperties = (
disabled={isDisabled} disabled={isDisabled}
/> />
: <input : <input
className='text-base font-medium transition-all text-gray-800 mt-1 block w-full px-3 py-2 key={key}
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
'
type={type}
id={key} id={key}
className={className}
type={type}
defaultValue={value} defaultValue={value}
defaultChecked={checked} defaultChecked={checked}
disabled={isDisabled} disabled={isDisabled}
/>; />;
groupInput.push( groupInput.push(
<div key={id} className='mt-4'> <label
<label className='text-sm font-medium text-gray-800' htmlFor={key}>{key}</label> key={id}
{input} className='mt-4 text-xs font-medium text-gray-800'
</div> htmlFor={key}
>
{key}
</label>
); );
groupInput.push(input);
}; };

View file

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { XPositionReference } from '../../../Enums/XPositionReference';
import { IContainerModel } from '../../../Interfaces/ContainerModel'; import { IContainerModel } from '../../../Interfaces/ContainerModel';
import { getDepth } from '../../../utils/itertools'; import { getDepth } from '../../../utils/itertools';
import { Dimension } from './Dimension'; import { Dimension } from './Dimension';
@ -17,7 +18,14 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
const containersElements = props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />); const containersElements = props.model.children.map(child => <Container key={`container-${child.properties.id}`} model={child} />);
const xText = Number(props.model.properties.width) / 2; const xText = Number(props.model.properties.width) / 2;
const yText = Number(props.model.properties.height) / 2; const yText = Number(props.model.properties.height) / 2;
const transform = `translate(${Number(props.model.properties.x)}, ${Number(props.model.properties.y)})`;
const [transformedX, transformedY] = transformPosition(
Number(props.model.properties.x),
Number(props.model.properties.y),
Number(props.model.properties.width),
props.model.properties.XPositionReference
);
const transform = `translate(${transformedX}, ${transformedY})`;
// g style // g style
const defaultStyle: React.CSSProperties = { const defaultStyle: React.CSSProperties = {
@ -54,7 +62,8 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
id={id} id={id}
xStart={xStart} xStart={xStart}
xEnd={xEnd} xEnd={xEnd}
y={y} yStart={y}
yEnd={y}
strokeWidth={strokeWidth} strokeWidth={strokeWidth}
text={text} text={text}
/> />
@ -74,3 +83,13 @@ export const Container: React.FC<IContainerProps> = (props: IContainerProps) =>
</g> </g>
); );
}; };
function transformPosition(x: number, y: number, width: number, xPositionReference = XPositionReference.Left): [number, number] {
let transformedX = x;
if (xPositionReference === XPositionReference.Center) {
transformedX -= width / 2;
} else if (xPositionReference === XPositionReference.Right) {
transformedX -= width;
}
return [transformedX, y];
}

View file

@ -3,45 +3,80 @@ import * as React from 'react';
interface IDimensionProps { interface IDimensionProps {
id: string id: string
xStart: number xStart: number
yStart: number
xEnd: number xEnd: number
y: number yEnd: number
text: string text: string
strokeWidth: number strokeWidth: number
} }
/**
* 2D Parametric function. Returns a new coordinate from the origin coordinate
* See for more details https://en.wikipedia.org/wiki/Parametric_equation.
* TL;DR a parametric function is a function with a parameter
* @param x0 Origin coordinate
* @param t The parameter
* @param vx Transform vector
* @returns Returns a new coordinate from the origin coordinate
*/
const applyParametric = (x0: number, t: number, vx: number): number => x0 + t * vx;
export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) => { export const Dimension: React.FC<IDimensionProps> = (props: IDimensionProps) => {
const style: React.CSSProperties = { const style: React.CSSProperties = {
stroke: 'black' stroke: 'black'
}; };
/// We need to find the points of the notches
// Get the vector of the line
const [deltaX, deltaY] = [(props.xEnd - props.xStart), (props.yEnd - props.yStart)];
// Get the unit vector
const norm = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const [unitX, unitY] = [deltaX / norm, deltaY / norm];
// Get the perpandicular vector
const [perpVecX, perpVecY] = [unitY, -unitX];
// Use the parametric function to get the coordinates (x = x0 + t * v.x)
const startTopX = applyParametric(props.xStart, 4, perpVecX);
const startTopY = applyParametric(props.yStart, 4, perpVecY);
const startBottomX = applyParametric(props.xStart, -4, perpVecX);
const startBottomY = applyParametric(props.yStart, -4, perpVecY);
const endTopX = applyParametric(props.xEnd, 4, perpVecX);
const endTopY = applyParametric(props.yEnd, 4, perpVecY);
const endBottomX = applyParametric(props.xEnd, -4, perpVecX);
const endBottomY = applyParametric(props.yEnd, -4, perpVecY);
return ( return (
<g key={props.id}> <g key={props.id}>
<line <line
x1={props.xStart} x1={startTopX}
y1={props.y - 4 * props.strokeWidth} y1={startTopY}
x2={props.xStart} x2={startBottomX}
y2={props.y + 4 * props.strokeWidth} y2={startBottomY}
strokeWidth={props.strokeWidth} strokeWidth={props.strokeWidth}
style={style} style={style}
/> />
<line <line
x1={props.xStart} x1={props.xStart}
y1={props.y} y1={props.yStart}
x2={props.xEnd} x2={props.xEnd}
y2={props.y} y2={props.yEnd}
strokeWidth={props.strokeWidth} strokeWidth={props.strokeWidth}
style={style} style={style}
/> />
<line <line
x1={props.xEnd} x1={endTopX}
y1={props.y - 4 * props.strokeWidth} y1={endTopY}
x2={props.xEnd} x2={endBottomX}
y2={props.y + 4 * props.strokeWidth} y2={endBottomY}
strokeWidth={props.strokeWidth} strokeWidth={props.strokeWidth}
style={style} style={style}
/> />
<text <text
x={(props.xStart + props.xEnd) / 2} x={(props.xStart + props.xEnd) / 2}
y={props.y} y={props.yStart}
> >
{props.text} {props.text}
</text> </text>

View file

@ -1,66 +0,0 @@
import * as React from 'react';
import { ContainerModel } from '../../../Interfaces/ContainerModel';
import { getDepth, MakeIterator } from '../../../utils/itertools';
import { Dimension } from './Dimension';
interface IDimensionLayerProps {
isHidden: boolean
roots: ContainerModel | ContainerModel[] | null
}
const GAP: number = 50;
const getDimensionsNodes = (root: ContainerModel): React.ReactNode[] => {
const it = MakeIterator(root);
const dimensions: React.ReactNode[] = [];
for (const container of it) {
// WARN: this might be dangerous later when using other units/rules
const width = Number(container.properties.width);
const id = `dim-${container.properties.id}`;
const xStart: number = container.properties.x;
const xEnd = xStart + width;
const y = -(GAP * (getDepth(container) + 1));
const strokeWidth = 1;
const text = width.toString();
dimensions.push(
<Dimension
id={id}
xStart={xStart}
xEnd={xEnd}
y={y}
strokeWidth={strokeWidth}
text={text}
/>
);
}
return dimensions;
};
/**
* A layer containing all dimension
*
* @deprecated In order to avoid adding complexity
* with computing the position in a group hierarchy,
* use Dimension directly inside the Container,
* Currently it is glitched as
* it does not take parents into account,
* and will not work correctly
* @param props
* @returns
*/
export const DimensionLayer: React.FC<IDimensionLayerProps> = (props: IDimensionLayerProps) => {
let dimensions: React.ReactNode[] = [];
if (Array.isArray(props.roots)) {
props.roots.forEach(child => {
dimensions.concat(getDimensionsNodes(child));
});
} else if (props.roots !== null) {
dimensions = getDimensionsNodes(props.roots);
}
return (
<g visibility={props.isHidden ? 'hidden' : 'visible'}>
{ dimensions }
</g>
);
};

View file

@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import { XPositionReference } from '../Enums/XPositionReference';
/** Model of available container used in application configuration */ /** Model of available container used in application configuration */
export interface AvailableContainer { export interface AvailableContainer {
Type: string Type: string
Width: number Width: number
Height: number Height: number
XPositionReference?: XPositionReference
Style: React.CSSProperties Style: React.CSSProperties
} }

View file

@ -1,4 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { XPositionReference } from '../Enums/XPositionReference';
export default interface Properties extends React.CSSProperties { export default interface Properties extends React.CSSProperties {
id: string id: string
@ -6,4 +7,5 @@ export default interface Properties extends React.CSSProperties {
x: number x: number
y: number y: number
isRigidBody: boolean isRigidBody: boolean
XPositionReference?: XPositionReference
} }

View file

@ -36,3 +36,5 @@ export const DEFAULT_MAINCONTAINER_PROPS: Properties = {
fillOpacity: 0, fillOpacity: 0,
stroke: 'black' stroke: 'black'
}; };
export const NOTCHES_LENGTH = 4;

View file

@ -76,9 +76,6 @@ const GetSVGLayoutConfiguration = () => {
fillOpacity: 0, fillOpacity: 0,
borderWidth: 2, borderWidth: 2,
stroke: 'blue', stroke: 'blue',
transform: 'translateX(-50%)',
transformOrigin: 'center',
transformBox: 'fill-box'
} }
} }
], ],