Merged PR 17: Implement rigid body Fix multiple bugs
Implement rigid body Fix saveload bug: having null elements Fix events being duplicated and not being removed
This commit is contained in:
parent
d2e1d9f0a4
commit
616fe3e9ac
22 changed files with 804 additions and 95 deletions
|
@ -23,6 +23,7 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
|
|||
const [editorState, setEditorState] = useState<IEditorState>({
|
||||
configuration: DEFAULT_CONFIG,
|
||||
history: [{
|
||||
LastAction: '',
|
||||
MainContainer: defaultMainContainer,
|
||||
SelectedContainer: defaultMainContainer,
|
||||
SelectedContainerId: defaultMainContainer.properties.id,
|
||||
|
@ -40,14 +41,16 @@ export const App: React.FunctionComponent<IAppProps> = (props) => {
|
|||
return;
|
||||
}
|
||||
|
||||
fetch(state)
|
||||
.then(
|
||||
async(response) => await response.json(),
|
||||
(error) => { throw new Error(error); }
|
||||
)
|
||||
.then((data: IEditorState) => {
|
||||
LoadState(data, setEditorState, setLoaded);
|
||||
}, (error) => { throw new Error(error); });
|
||||
if (!isLoaded) {
|
||||
fetch(state)
|
||||
.then(
|
||||
async(response) => await response.json(),
|
||||
(error) => { throw new Error(error); }
|
||||
)
|
||||
.then((data: IEditorState) => {
|
||||
LoadState(data, setEditorState, setLoaded);
|
||||
}, (error) => { throw new Error(error); });
|
||||
}
|
||||
});
|
||||
|
||||
if (isLoaded) {
|
||||
|
|
|
@ -22,6 +22,7 @@ export function NewEditor(
|
|||
y: 0,
|
||||
width: configuration.MainContainer.Width,
|
||||
height: configuration.MainContainer.Height,
|
||||
isRigidBody: false,
|
||||
fillOpacity: 0,
|
||||
stroke: 'black'
|
||||
}
|
||||
|
@ -34,6 +35,7 @@ export function NewEditor(
|
|||
history:
|
||||
[
|
||||
{
|
||||
LastAction: '',
|
||||
MainContainer,
|
||||
SelectedContainer: MainContainer,
|
||||
SelectedContainerId: MainContainer.properties.id,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { HistoryState } from "../../Interfaces/HistoryState";
|
||||
import { HistoryState } from '../../Interfaces/HistoryState';
|
||||
import { Configuration } from '../../Interfaces/Configuration';
|
||||
import { ContainerModel, IContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { findContainerById } from '../../utils/itertools';
|
||||
import { getCurrentHistory } from './Editor';
|
||||
import { SizePointer } from '../../Interfaces/SizePointer';
|
||||
|
||||
/**
|
||||
* Select a container
|
||||
|
@ -19,22 +20,19 @@ export function SelectContainer(
|
|||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||
const current = history[history.length - 1];
|
||||
|
||||
if (current.MainContainer === null) {
|
||||
throw new Error('[SelectContainer] Tried to select a container while there is no main container!');
|
||||
}
|
||||
|
||||
const mainContainerClone = structuredClone(current.MainContainer);
|
||||
const SelectedContainer = findContainerById(mainContainerClone, container.properties.id);
|
||||
const selectedContainer = findContainerById(mainContainerClone, container.properties.id);
|
||||
|
||||
if (SelectedContainer === undefined) {
|
||||
if (selectedContainer === undefined) {
|
||||
throw new Error('[SelectContainer] Cannot find container among children of main container!');
|
||||
}
|
||||
|
||||
setHistory(history.concat([{
|
||||
LastAction: `Select container ${selectedContainer.properties.id}`,
|
||||
MainContainer: mainContainerClone,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters),
|
||||
SelectedContainer,
|
||||
SelectedContainerId: SelectedContainer.properties.id
|
||||
SelectedContainer: selectedContainer,
|
||||
SelectedContainerId: selectedContainer.properties.id,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]));
|
||||
setHistoryCurrentStep(history.length);
|
||||
}
|
||||
|
@ -49,10 +47,6 @@ export function DeleteContainer(
|
|||
const history = getCurrentHistory(fullHistory, historyCurrentStep);
|
||||
const current = history[historyCurrentStep];
|
||||
|
||||
if (current.MainContainer === null) {
|
||||
throw new Error('[DeleteContainer] Error: Tried to delete a container without a main container');
|
||||
}
|
||||
|
||||
const mainContainerClone: IContainerModel = structuredClone(current.MainContainer);
|
||||
const container = findContainerById(mainContainerClone, containerId);
|
||||
|
||||
|
@ -62,7 +56,7 @@ export function DeleteContainer(
|
|||
|
||||
if (container === mainContainerClone) {
|
||||
// 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!');
|
||||
}
|
||||
|
||||
if (container === null || container === undefined) {
|
||||
|
@ -77,9 +71,10 @@ export function DeleteContainer(
|
|||
}
|
||||
|
||||
setHistory(history.concat([{
|
||||
LastAction: `Delete container ${containerId}`,
|
||||
MainContainer: mainContainerClone,
|
||||
SelectedContainer: null,
|
||||
SelectedContainerId: '',
|
||||
MainContainer: mainContainerClone,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]));
|
||||
setHistoryCurrentStep(history.length);
|
||||
|
@ -168,7 +163,7 @@ export function AddContainer(
|
|||
}
|
||||
|
||||
let x = 0;
|
||||
if (index !== 0) {
|
||||
if (index > 0) {
|
||||
const lastChild: IContainerModel | undefined = parentClone.children.at(index - 1);
|
||||
if (lastChild !== undefined) {
|
||||
x = lastChild.properties.x + Number(lastChild.properties.width);
|
||||
|
@ -185,6 +180,7 @@ export function AddContainer(
|
|||
y: 0,
|
||||
width: properties?.Width,
|
||||
height: parentClone.properties.height,
|
||||
isRigidBody: false,
|
||||
...properties.Style
|
||||
},
|
||||
[],
|
||||
|
@ -202,10 +198,11 @@ export function AddContainer(
|
|||
|
||||
// Update the state
|
||||
setHistory(history.concat([{
|
||||
LastAction: 'Add container',
|
||||
MainContainer: clone,
|
||||
TypeCounters: newCounters,
|
||||
SelectedContainer: parentClone,
|
||||
SelectedContainerId: parentClone.properties.id
|
||||
SelectedContainerId: parentClone.properties.id,
|
||||
TypeCounters: newCounters
|
||||
}]));
|
||||
setHistoryCurrentStep(history.length);
|
||||
}
|
||||
|
@ -218,7 +215,7 @@ export function AddContainer(
|
|||
*/
|
||||
export function OnPropertyChange(
|
||||
key: string,
|
||||
value: string | number,
|
||||
value: string | number | boolean,
|
||||
fullHistory: HistoryState[],
|
||||
historyCurrentStep: number,
|
||||
setHistory: Dispatch<SetStateAction<HistoryState[]>>,
|
||||
|
@ -232,18 +229,14 @@ export function OnPropertyChange(
|
|||
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
|
||||
}
|
||||
|
||||
if (current.MainContainer === null ||
|
||||
current.MainContainer === undefined) {
|
||||
throw new Error('[OnPropertyChange] Property was changed before the main container was added');
|
||||
}
|
||||
|
||||
if (parent === null) {
|
||||
const selectedContainerClone: IContainerModel = structuredClone(current.SelectedContainer);
|
||||
(selectedContainerClone.properties as any)[key] = value;
|
||||
setHistory(history.concat([{
|
||||
LastAction: 'Change property of main',
|
||||
MainContainer: selectedContainerClone,
|
||||
SelectedContainer: selectedContainerClone,
|
||||
SelectedContainerId: selectedContainerClone.properties.id,
|
||||
MainContainer: selectedContainerClone,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]));
|
||||
setHistoryCurrentStep(history.length);
|
||||
|
@ -259,11 +252,223 @@ export function OnPropertyChange(
|
|||
|
||||
(container.properties as any)[key] = value;
|
||||
|
||||
if (container.properties.isRigidBody) {
|
||||
RecalculatePhysics(container);
|
||||
}
|
||||
|
||||
setHistory(history.concat([{
|
||||
LastAction: `Change property of container ${container.properties.id}`,
|
||||
MainContainer: mainContainerClone,
|
||||
SelectedContainer: container,
|
||||
SelectedContainerId: container.properties.id,
|
||||
MainContainer: mainContainerClone,
|
||||
TypeCounters: Object.assign({}, current.TypeCounters)
|
||||
}]));
|
||||
setHistoryCurrentStep(history.length);
|
||||
}
|
||||
|
||||
// TODO put this in a different file
|
||||
|
||||
export function RecalculatePhysics(container: IContainerModel): IContainerModel {
|
||||
container = constraintBodyInsideParent(container);
|
||||
container = constraintBodyInsideUnallocatedWidth(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Limit a rect inside a parent rect by applying the following rules :
|
||||
* it cannot be bigger than the parent
|
||||
* it cannot go out of bound
|
||||
* @param container
|
||||
* @returns
|
||||
*/
|
||||
function constraintBodyInsideParent(container: IContainerModel): IContainerModel {
|
||||
if (container.parent === null || container.parent === undefined) {
|
||||
return container;
|
||||
}
|
||||
|
||||
const parentProperties = container.parent.properties;
|
||||
const parentWidth = Number(parentProperties.width);
|
||||
const parentHeight = Number(parentProperties.height);
|
||||
|
||||
return constraintBodyInsideSpace(container, 0, 0, parentWidth, parentHeight);
|
||||
}
|
||||
|
||||
function constraintBodyInsideSpace(
|
||||
container: IContainerModel,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
): IContainerModel {
|
||||
const containerProperties = container.properties;
|
||||
const containerX = Number(containerProperties.x);
|
||||
const containerY = Number(containerProperties.y);
|
||||
const containerWidth = Number(containerProperties.width);
|
||||
const containerHeight = Number(containerProperties.height);
|
||||
|
||||
// Check size bigger than parent
|
||||
const isBodyLargerThanParent = containerWidth > width;
|
||||
const isBodyTallerThanParentHeight = containerHeight > height;
|
||||
if (isBodyLargerThanParent || isBodyTallerThanParentHeight) {
|
||||
if (isBodyLargerThanParent) {
|
||||
containerProperties.x = x;
|
||||
containerProperties.width = width;
|
||||
}
|
||||
if (isBodyTallerThanParentHeight) {
|
||||
containerProperties.y = y;
|
||||
containerProperties.height = height;
|
||||
}
|
||||
return container;
|
||||
}
|
||||
|
||||
// Check horizontal out of bound
|
||||
if (containerX < x) {
|
||||
containerProperties.x = x;
|
||||
}
|
||||
if (containerX + containerWidth > width) {
|
||||
containerProperties.x = x + width - containerWidth;
|
||||
}
|
||||
|
||||
// Check vertical out of bound
|
||||
if (containerY < y) {
|
||||
containerProperties.y = y;
|
||||
}
|
||||
if (containerY + containerHeight > height) {
|
||||
containerProperties.y = y + height - containerHeight;
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unallocated widths inside a container
|
||||
* An allocated width is defined by its the widths of the children that are rigid bodies.
|
||||
* An example of this allocation system is the disk space
|
||||
* (except the fact that disk space is divided by block).
|
||||
* @param container
|
||||
* @returns {SizePointer[]} Array of unallocated widths (x=position of the unallocated space, width=size of the allocated space)
|
||||
*/
|
||||
function getAvailableWidths(container: IContainerModel, exception: IContainerModel): SizePointer[] {
|
||||
const x = 0;
|
||||
const width = Number(container.properties.width);
|
||||
let unallocatedSpaces: SizePointer[] = [{ x, width }];
|
||||
|
||||
const rigidBodies = container.children.filter(child => child.properties.isRigidBody);
|
||||
for (const child of rigidBodies) {
|
||||
if (child === exception) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// get the space of the child that is inside the parent
|
||||
let newUnallocatedSpace: SizePointer[] = [];
|
||||
for (const unallocatedSpace of unallocatedSpaces) {
|
||||
const newUnallocatedWidths = getAvailableWidthsTwoLines(
|
||||
unallocatedSpace.x,
|
||||
unallocatedSpace.x + unallocatedSpace.width,
|
||||
child.properties.x,
|
||||
child.properties.x + Number(child.properties.width));
|
||||
newUnallocatedSpace = newUnallocatedSpace.concat(newUnallocatedWidths);
|
||||
}
|
||||
unallocatedSpaces = newUnallocatedSpace;
|
||||
}
|
||||
|
||||
return unallocatedSpaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the unallocated widths between two lines in 1D
|
||||
* @param min1 left of the first line
|
||||
* @param max1 rigth of the first line
|
||||
* @param min2 left of the second line
|
||||
* @param max2 right of the second line
|
||||
* @returns Available widths
|
||||
*/
|
||||
function getAvailableWidthsTwoLines(min1: number, max1: number, min2: number, max2: number): SizePointer[] {
|
||||
if (min2 < min1 && max2 > max1) {
|
||||
// object 2 is overlapping full width
|
||||
return [];
|
||||
}
|
||||
|
||||
if (min1 >= min2) {
|
||||
// object 2 is partially overlapping on the left
|
||||
return [{
|
||||
x: max2,
|
||||
width: max1 - max2
|
||||
}];
|
||||
}
|
||||
|
||||
if (max2 >= max1) {
|
||||
// object 2 is partially overlapping on the right
|
||||
return [{
|
||||
x: min2,
|
||||
width: max2 - min1
|
||||
}];
|
||||
}
|
||||
|
||||
// object 2 is overlapping in the middle
|
||||
return [
|
||||
{
|
||||
x: min1,
|
||||
width: min2 - min1
|
||||
},
|
||||
{
|
||||
x: min2,
|
||||
width: max1 - max2
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param container
|
||||
* @returns
|
||||
*/
|
||||
function constraintBodyInsideUnallocatedWidth(container: IContainerModel): IContainerModel {
|
||||
if (container.parent === null) {
|
||||
return container;
|
||||
}
|
||||
|
||||
const availableWidths = getAvailableWidths(container.parent, container);
|
||||
const containerX = Number(container.properties.x);
|
||||
|
||||
// Sort the available width
|
||||
availableWidths
|
||||
.sort((width1, width2) => Math.abs(width1.x - containerX) - Math.abs(width2.x - containerX));
|
||||
|
||||
if (availableWidths.length === 0) {
|
||||
throw new Error('No available space found on the parent container. Try to free the parent a little before placing it inside.');
|
||||
}
|
||||
|
||||
const availableWidthFound = availableWidths.find(
|
||||
width => isFitting(container, width)
|
||||
);
|
||||
|
||||
if (availableWidthFound === undefined) {
|
||||
// There is two way to reach this part of the code
|
||||
// 1) toggle the isRigidBody such as width > availableWidth.width
|
||||
// 2) resize a container such as width > availableWidth.width
|
||||
// We want the container to fit automatically inside the available space
|
||||
// even if it means to resize the container
|
||||
// The end goal is that the code never show the error message no matter what action is done
|
||||
// TODO: Actually give an option to not fit and show the error message shown below
|
||||
const availableWidth = availableWidths[0];
|
||||
container.properties.x = availableWidth.x;
|
||||
container.properties.width = availableWidth.width;
|
||||
// throw new Error('[constraintBodyInsideUnallocatedWidth] BIGERR: No available space found on the parent container, even though there is some.');
|
||||
return container;
|
||||
}
|
||||
|
||||
return constraintBodyInsideSpace(
|
||||
container,
|
||||
availableWidthFound.x,
|
||||
0,
|
||||
availableWidthFound.width,
|
||||
Number(container.parent.properties.height)
|
||||
);
|
||||
}
|
||||
|
||||
function isFitting(container: IContainerModel, sizePointer: SizePointer): boolean {
|
||||
const containerWidth = Number(container.properties.width);
|
||||
|
||||
return containerWidth <= sizePointer.width;
|
||||
}
|
||||
|
|
|
@ -24,24 +24,21 @@ export const getCurrentHistory = (history: HistoryState[], historyCurrentStep: n
|
|||
export const getCurrentHistoryState = (history: HistoryState[], historyCurrentStep: number): HistoryState => history[historyCurrentStep];
|
||||
|
||||
const Editor: React.FunctionComponent<IEditorProps> = (props) => {
|
||||
const [history, setHistory] = React.useState<HistoryState[]>([...props.history]);
|
||||
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(0);
|
||||
const [history, setHistory] = React.useState<HistoryState[]>(structuredClone(props.history));
|
||||
const [historyCurrentStep, setHistoryCurrentStep] = React.useState<number>(props.historyCurrentStep);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('keyup', (event) => onKeyDown(
|
||||
const onKeyUp = (event: KeyboardEvent): void => onKeyDown(
|
||||
event,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistoryCurrentStep
|
||||
));
|
||||
);
|
||||
|
||||
window.addEventListener('keyup', onKeyUp);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keyup', (event) => onKeyDown(
|
||||
event,
|
||||
history,
|
||||
historyCurrentStep,
|
||||
setHistoryCurrentStep
|
||||
));
|
||||
window.removeEventListener('keyup', onKeyUp);
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { HistoryState } from "../../Interfaces/HistoryState";
|
||||
import { HistoryState } from '../../Interfaces/HistoryState';
|
||||
|
||||
export function onKeyDown(
|
||||
event: KeyboardEvent,
|
||||
|
|
|
@ -16,14 +16,15 @@ describe.concurrent('Elements sidebar', () => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
width: 2000,
|
||||
height: 100
|
||||
height: 100,
|
||||
isRigidBody: false
|
||||
},
|
||||
userData: {}
|
||||
}}
|
||||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
SelectedContainer={null}
|
||||
onPropertyChange={() => {}}
|
||||
OnPropertyChange={() => {}}
|
||||
SelectContainer={() => {}}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
|
@ -44,7 +45,8 @@ describe.concurrent('Elements sidebar', () => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
width: 2000,
|
||||
height: 100
|
||||
height: 100,
|
||||
isRigidBody: false
|
||||
},
|
||||
userData: {}
|
||||
};
|
||||
|
@ -54,7 +56,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
SelectedContainer={MainContainer}
|
||||
onPropertyChange={() => {}}
|
||||
OnPropertyChange={() => {}}
|
||||
SelectContainer={() => {}}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
|
@ -98,7 +100,8 @@ describe.concurrent('Elements sidebar', () => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
width: 2000,
|
||||
height: 100
|
||||
height: 100,
|
||||
isRigidBody: false
|
||||
},
|
||||
userData: {}
|
||||
};
|
||||
|
@ -113,7 +116,8 @@ describe.concurrent('Elements sidebar', () => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
height: 0,
|
||||
isRigidBody: false
|
||||
},
|
||||
userData: {}
|
||||
}
|
||||
|
@ -129,7 +133,8 @@ describe.concurrent('Elements sidebar', () => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
height: 0,
|
||||
isRigidBody: false
|
||||
},
|
||||
userData: {}
|
||||
}
|
||||
|
@ -140,7 +145,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
SelectedContainer={MainContainer}
|
||||
onPropertyChange={() => {}}
|
||||
OnPropertyChange={() => {}}
|
||||
SelectContainer={() => {}}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
|
@ -164,7 +169,8 @@ describe.concurrent('Elements sidebar', () => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
width: 2000,
|
||||
height: 100
|
||||
height: 100,
|
||||
isRigidBody: false
|
||||
},
|
||||
userData: {}
|
||||
};
|
||||
|
@ -178,7 +184,8 @@ describe.concurrent('Elements sidebar', () => {
|
|||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0
|
||||
height: 0,
|
||||
isRigidBody: false
|
||||
},
|
||||
userData: {}
|
||||
};
|
||||
|
@ -194,7 +201,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
SelectedContainer={SelectedContainer}
|
||||
onPropertyChange={() => {}}
|
||||
OnPropertyChange={() => {}}
|
||||
SelectContainer={selectContainer}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
|
@ -217,7 +224,7 @@ describe.concurrent('Elements sidebar', () => {
|
|||
isOpen={true}
|
||||
isHistoryOpen={false}
|
||||
SelectedContainer={SelectedContainer}
|
||||
onPropertyChange={() => {}}
|
||||
OnPropertyChange={() => {}}
|
||||
SelectContainer={selectContainer}
|
||||
DeleteContainer={() => {}}
|
||||
AddContainer={() => {}}
|
||||
|
|
|
@ -13,7 +13,7 @@ interface IElementsSidebarProps {
|
|||
isOpen: boolean
|
||||
isHistoryOpen: boolean
|
||||
SelectedContainer: IContainerModel | null
|
||||
onPropertyChange: (key: string, value: string) => void
|
||||
OnPropertyChange: (key: string, value: string | number | boolean) => void
|
||||
SelectContainer: (container: IContainerModel) => void
|
||||
DeleteContainer: (containerid: string) => void
|
||||
AddContainer: (index: number, type: string, parent: string) => void
|
||||
|
@ -71,40 +71,39 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
|||
|
||||
// Event listeners
|
||||
React.useEffect(() => {
|
||||
const onContextMenu = (event: MouseEvent): void => handleRightClick(
|
||||
event,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId,
|
||||
setContextMenuPosition
|
||||
);
|
||||
|
||||
const onLeftClick = (): void => handleLeftClick(
|
||||
isContextMenuOpen,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId
|
||||
);
|
||||
|
||||
elementRef.current?.addEventListener(
|
||||
'contextmenu',
|
||||
(event) => handleRightClick(
|
||||
event,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId,
|
||||
setContextMenuPosition
|
||||
));
|
||||
onContextMenu
|
||||
);
|
||||
|
||||
window.addEventListener(
|
||||
'click',
|
||||
(event) => handleLeftClick(
|
||||
isContextMenuOpen,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId
|
||||
));
|
||||
onLeftClick
|
||||
);
|
||||
|
||||
return () => {
|
||||
elementRef.current?.addEventListener(
|
||||
elementRef.current?.removeEventListener(
|
||||
'contextmenu',
|
||||
(event) => handleRightClick(
|
||||
event,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId,
|
||||
setContextMenuPosition
|
||||
));
|
||||
onContextMenu
|
||||
);
|
||||
|
||||
window.removeEventListener(
|
||||
'click',
|
||||
(event) => handleLeftClick(
|
||||
isContextMenuOpen,
|
||||
setIsContextMenuOpen,
|
||||
setOnClickContainerId
|
||||
));
|
||||
onLeftClick
|
||||
);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -141,9 +140,12 @@ export const ElementsSidebar: React.FC<IElementsSidebarProps> = (props: IElement
|
|||
y={contextMenuPosition.y}
|
||||
isOpen={isContextMenuOpen}
|
||||
>
|
||||
<MenuItem className='contextmenu-item' text='Delete' onClick={() => props.DeleteContainer(onClickContainerId)} />
|
||||
<MenuItem className='contextmenu-item' text='Delete' onClick={() => {
|
||||
setIsContextMenuOpen(false);
|
||||
props.DeleteContainer(onClickContainerId);
|
||||
}} />
|
||||
</Menu>
|
||||
<Properties properties={props.SelectedContainer?.properties} onChange={props.onPropertyChange}></Properties>
|
||||
<Properties properties={props.SelectedContainer?.properties} onChange={props.OnPropertyChange}></Properties>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,7 +21,8 @@ describe.concurrent('Properties', () => {
|
|||
id: 'stuff',
|
||||
parentId: 'parentId',
|
||||
x: 1,
|
||||
y: 1
|
||||
y: 1,
|
||||
isRigidBody: false
|
||||
};
|
||||
|
||||
const handleChange = vi.fn((key, value) => {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import * as React from 'react';
|
||||
import ContainerProperties from '../../Interfaces/Properties';
|
||||
import { INPUT_TYPES } from './PropertiesInputTypes';
|
||||
|
||||
interface IPropertiesProps {
|
||||
properties?: ContainerProperties
|
||||
onChange: (key: string, value: string) => void
|
||||
onChange: (key: string, value: string | number | boolean) => void
|
||||
}
|
||||
|
||||
export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps) => {
|
||||
|
@ -26,11 +27,24 @@ export const Properties: React.FC<IPropertiesProps> = (props: IPropertiesProps)
|
|||
const handleProperties = (
|
||||
[key, value]: [string, string | number],
|
||||
groupInput: React.ReactNode[],
|
||||
onChange: (key: string, value: string) => void
|
||||
onChange: (key: string, value: string | number | boolean) => void
|
||||
): void => {
|
||||
const id = `property-${key}`;
|
||||
const type = 'text';
|
||||
const isDisabled = key === 'id' || key === 'parentId'; // hardcoded
|
||||
let type = 'text';
|
||||
let checked;
|
||||
|
||||
/// hardcoded stuff for ergonomy ///
|
||||
if (typeof value === 'boolean') {
|
||||
checked = value;
|
||||
}
|
||||
|
||||
if (key in INPUT_TYPES) {
|
||||
type = INPUT_TYPES[key];
|
||||
}
|
||||
|
||||
const isDisabled = ['id', 'parentId'].includes(key);
|
||||
///
|
||||
|
||||
groupInput.push(
|
||||
<div key={id} className='mt-4'>
|
||||
<label className='text-sm font-medium text-gray-800' htmlFor={id}>{key}</label>
|
||||
|
@ -43,7 +57,14 @@ const handleProperties = (
|
|||
type={type}
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(event) => onChange(key, event.target.value)}
|
||||
checked={checked}
|
||||
onChange={(event) => {
|
||||
if (type === 'checkbox') {
|
||||
onChange(key, event.target.checked);
|
||||
return;
|
||||
}
|
||||
onChange(key, event.target.value);
|
||||
}}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
|
7
src/Components/Properties/PropertiesInputTypes.tsx
Normal file
7
src/Components/Properties/PropertiesInputTypes.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
export const INPUT_TYPES: Record<string, string> = {
|
||||
x: 'number',
|
||||
y: 'number',
|
||||
width: 'number',
|
||||
height: 'number',
|
||||
isRigidBody: 'checkbox'
|
||||
};
|
|
@ -35,10 +35,11 @@ export const SVG: React.FC<ISVGProps> = (props: ISVGProps) => {
|
|||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('resize', () => resizeViewBox(setViewer));
|
||||
const onResize = (): void => resizeViewBox(setViewer);
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
return () => {
|
||||
window.addEventListener('resize', () => resizeViewBox(setViewer));
|
||||
window.removeEventListener('resize', onResize);
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Sidebar } from '../Sidebar/Sidebar';
|
|||
import { History } from '../History/History';
|
||||
import { AvailableContainer } from '../../Interfaces/AvailableContainer';
|
||||
import { ContainerModel } from '../../Interfaces/ContainerModel';
|
||||
import { HistoryState } from "../../Interfaces/HistoryState";
|
||||
import { HistoryState } from '../../Interfaces/HistoryState';
|
||||
import { PhotographIcon, UploadIcon } from '@heroicons/react/outline';
|
||||
import { FloatingButton } from '../FloatingButton/FloatingButton';
|
||||
import { Bar } from '../Bar/Bar';
|
||||
|
@ -16,7 +16,7 @@ interface IUIProps {
|
|||
AvailableContainers: AvailableContainer[]
|
||||
SelectContainer: (container: ContainerModel) => void
|
||||
DeleteContainer: (containerId: string) => void
|
||||
OnPropertyChange: (key: string, value: string) => void
|
||||
OnPropertyChange: (key: string, value: string | number | boolean) => void
|
||||
AddContainerToSelectedContainer: (type: string) => void
|
||||
AddContainer: (index: number, type: string, parentId: string) => void
|
||||
SaveEditorAsJSON: () => void
|
||||
|
@ -58,7 +58,7 @@ export const UI: React.FunctionComponent<IUIProps> = (props: IUIProps) => {
|
|||
SelectedContainer={props.current.SelectedContainer}
|
||||
isOpen={isElementsSidebarOpen}
|
||||
isHistoryOpen={isHistoryOpen}
|
||||
onPropertyChange={props.OnPropertyChange}
|
||||
OnPropertyChange={props.OnPropertyChange}
|
||||
SelectContainer={props.SelectContainer}
|
||||
DeleteContainer={props.DeleteContainer}
|
||||
AddContainer={props.AddContainer}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue