Merge branch 'dev'

This commit is contained in:
Eric NGUYEN 2022-11-03 10:16:03 +01:00
commit d48d45f5ca
35 changed files with 565 additions and 486 deletions

1
.gitattributes vendored
View file

@ -2,3 +2,4 @@
*.pdf filter=lfs diff=lfs merge=lfs -text *.pdf filter=lfs diff=lfs merge=lfs -text
*.dwg filter=lfs diff=lfs merge=lfs -text *.dwg filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text
*.pptx filter=lfs diff=lfs merge=lfs -text

View file

@ -1,4 +1,5 @@
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Collections.Generic;
namespace SVGLDLibs.Models namespace SVGLDLibs.Models
{ {
@ -149,6 +150,6 @@ namespace SVGLDLibs.Models
* User data that can be used for data storage or custom SVG * User data that can be used for data storage or custom SVG
*/ */
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public object userData; public Dictionary<string, string> userData;
} }
} }

View file

@ -6,8 +6,8 @@ namespace SVGLDLibs.Models
public class PointModel public class PointModel
{ {
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string X { get; set; } public double x { get; set; }
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string Y { get; set; } public double y { get; set; }
} }
} }

View file

@ -1,17 +1,24 @@
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Collections.Generic;
namespace SVGLDLibs.Models namespace SVGLDLibs.Models
{ {
[DataContract] [DataContract]
public class SymbolModel : AvailableSymbolModel public class SymbolModel
{ {
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string Id { get; set; } public string id { get; set; }
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public PointModel Point { get; set; } public string type { get; set; }
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public bool IsLinkedToContainer { get; set; } public AvailableSymbolModel config { get; set; }
[DataMember(EmitDefaultValue = false)] [DataMember(EmitDefaultValue = false)]
public string LinkedContainerId { get; set; } public double x { get; set; }
[DataMember(EmitDefaultValue = false)]
public double width { get; set; }
[DataMember(EmitDefaultValue = false)]
public double height { get; set; }
[DataMember(EmitDefaultValue = false)]
public List<string> linkedContainers { get; set; }
} }
} }

View file

@ -45,7 +45,7 @@ public class SVGLDController : ControllerBase
} }
[HttpPost(Name = nameof(SVGLDLibs.Models.Configuration))] [HttpPost(Name = nameof(SVGLDLibs.Models.Configuration))]
public bool ConfigurationResponseModel(Configuration model) public bool Configuration(Configuration model)
{ {
return true; return true;
} }

BIN
docs/#Project/Pages/SVGLD_Cotes.pdf (Stored with Git LFS) Normal file

Binary file not shown.

BIN
docs/#Project/Pages/SVGLD_Cotes.pptx (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -5,10 +5,6 @@ onmessage = (e) => {
const getCircularReplacer = () => { const getCircularReplacer = () => {
return (key, value) => { return (key, value) => {
if (key === 'parent') {
return;
}
if (key === 'containers') { if (key === 'containers') {
return [...value.entries()] return [...value.entries()]
.map(([Key, Value]) => ({ Key, Value })); .map(([Key, Value]) => ({ Key, Value }));

View file

@ -98,7 +98,6 @@ describe.concurrent('Models test suite', () => {
}); });
const mainContainer = new ContainerModel( const mainContainer = new ContainerModel(
null,
DEFAULT_MAINCONTAINER_PROPS DEFAULT_MAINCONTAINER_PROPS
); );
@ -191,12 +190,7 @@ describe.concurrent('Models test suite', () => {
} }
], ],
CustomSVG: 'string', CustomSVG: 'string',
Style: {}, Style: {}
UserData: {
additionalProp1: 'string',
additionalProp2: 'string',
additionalProp3: 'string'
}
}; };
it('AvailableContainerModel', async() => { it('AvailableContainerModel', async() => {
@ -239,7 +233,7 @@ describe.concurrent('Models test suite', () => {
wrapper: 'string' wrapper: 'string'
}; };
it('ConfigurationResponseModel', async() => { it('Configuration', async() => {
const model: IConfiguration = { const model: IConfiguration = {
AvailableContainers: [ AvailableContainers: [
availableContainerModel availableContainerModel
@ -251,10 +245,15 @@ describe.concurrent('Models test suite', () => {
category category
], ],
MainContainer: availableContainerModel, MainContainer: availableContainerModel,
Patterns: [pattern] Patterns: [pattern],
APIConfiguration: {
apiFetchUrl: '',
apiGetFeedbackUrl: '',
apiSetContainerListUrl: ''
}
}; };
const res = await Post2API('ConfigurationResponseModel', JSON.stringify(model)); const res = await Post2API('Configuration', JSON.stringify(model));
expect(res).toBe(true); expect(res).toBe(true);
}); });
@ -267,7 +266,6 @@ describe.concurrent('Models test suite', () => {
it('ContainerModel', async() => { it('ContainerModel', async() => {
const model: IContainerModel = { const model: IContainerModel = {
children: [], children: [],
parent: null,
properties: containerProperties, properties: containerProperties,
userData: {} userData: {}
}; };

View file

@ -1,5 +1,5 @@
import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react'; import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from 'react';
import { events as EVENTS } from '../../Events/AppEvents'; import { UseCustomEvents } from '../../Events/AppEvents';
import { MainMenu } from '../MainMenu/MainMenu'; import { MainMenu } from '../MainMenu/MainMenu';
import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel'; import { ContainerModel, IContainerModel } from '../../Interfaces/IContainerModel';
import { Editor } from '../Editor/Editor'; import { Editor } from '../Editor/Editor';
@ -41,44 +41,11 @@ function UseHTTPGETStatePreloading(
}); });
}; };
function UseCustomEvents(
root: Element | Document,
appRef: React.RefObject<HTMLDivElement>,
setEditor: (newState: IEditorState) => void,
setLoaded: (loaded: boolean) => void
): void {
useEffect(() => {
const funcs = new Map<string, () => void>();
for (const event of EVENTS) {
function Func(eventInitDict?: CustomEventInit): void {
return event.func(
root,
setEditor,
setLoaded,
eventInitDict
);
}
appRef.current?.addEventListener(event.name, Func);
funcs.set(event.name, Func);
}
return () => {
for (const event of EVENTS) {
const func = funcs.get(event.name);
if (func === undefined) {
continue;
}
appRef.current?.removeEventListener(event.name, func);
}
};
});
}
export function App(props: IAppProps): JSX.Element { export function App(props: IAppProps): JSX.Element {
const [isLoaded, setLoaded] = useState<boolean>(false); const [isLoaded, setLoaded] = useState<boolean>(false);
const appRef = useRef<HTMLDivElement>(null); const appRef = useRef<HTMLDivElement>(null);
const defaultMainContainer = new ContainerModel( const defaultMainContainer = new ContainerModel(
null,
DEFAULT_MAINCONTAINER_PROPS DEFAULT_MAINCONTAINER_PROPS
); );
const containers = new Map<string, IContainerModel>(); const containers = new Map<string, IContainerModel>();

View file

@ -4,6 +4,7 @@ import { GetAbsolutePosition } from '../../utils/itertools';
import { RemoveMargin } from '../../utils/svg'; import { RemoveMargin } from '../../utils/svg';
interface ISelectorProps { interface ISelectorProps {
containers: Map<string, IContainerModel>
selected?: IContainerModel selected?: IContainerModel
scale?: number scale?: number
} }
@ -14,7 +15,7 @@ export function RenderSelector(ctx: CanvasRenderingContext2D, frameCount: number
} }
const scale = (props.scale ?? 1); const scale = (props.scale ?? 1);
let [x, y] = GetAbsolutePosition(props.selected); let [x, y] = GetAbsolutePosition(props.containers, props.selected);
let [width, height] = [ let [width, height] = [
props.selected.properties.width, props.selected.properties.width,
props.selected.properties.height props.selected.properties.height

View file

@ -10,7 +10,7 @@ import { IHistoryState } from '../../../Interfaces/IHistoryState';
import { IPattern, GetPattern, ContainerOrPattern } from '../../../Interfaces/IPattern'; import { IPattern, GetPattern, ContainerOrPattern } from '../../../Interfaces/IPattern';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { GetDefaultContainerProps, DEFAULTCHILDTYPE_MAX_DEPTH, DEFAULTCHILDTYPE_ALLOW_CYCLIC } from '../../../utils/default'; import { GetDefaultContainerProps } from '../../../utils/default';
import { FindContainerById } from '../../../utils/itertools'; import { FindContainerById } from '../../../utils/itertools';
import { ApplyMargin } from '../../../utils/svg'; import { ApplyMargin } from '../../../utils/svg';
import { ApplyBehaviors, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors'; import { ApplyBehaviors, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors';
@ -27,21 +27,15 @@ import { SortChildren } from './ContainerOperations';
*/ */
export function AddContainerToSelectedContainer( export function AddContainerToSelectedContainer(
type: string, type: string,
selected: IContainerModel | undefined, selected: IContainerModel,
configuration: IConfiguration, configuration: IConfiguration,
fullHistory: IHistoryState[], fullHistory: IHistoryState[],
historyCurrentStep: number historyCurrentStep: number
): IHistoryState[] | null { ): IHistoryState[] {
if (selected === null ||
selected === undefined) {
return null;
}
const parent = selected;
return AddContainer( return AddContainer(
parent.children.length, selected.children.length,
type, type,
parent.properties.id, selected.properties.id,
configuration, configuration,
fullHistory, fullHistory,
historyCurrentStep historyCurrentStep
@ -163,7 +157,6 @@ function AddNewContainerToParent(
// Create the container // Create the container
const newContainer = new ContainerModel( const newContainer = new ContainerModel(
parentClone,
defaultProperties, defaultProperties,
[], [],
{ {
@ -308,96 +301,176 @@ function InitializeChildrenWithPattern(
const configs: Map<string, IAvailableContainer> = new Map(configuration.AvailableContainers.map(config => [config.Type, config])); const configs: Map<string, IAvailableContainer> = new Map(configuration.AvailableContainers.map(config => [config.Type, config]));
const patterns: Map<string, IPattern> = new Map(configuration.Patterns.map(pattern => [pattern.id, pattern])); const patterns: Map<string, IPattern> = new Map(configuration.Patterns.map(pattern => [pattern.id, pattern]));
const containerOrPattern = GetPattern(patternId, configs, patterns); const containerOrPattern = GetPattern(patternId, configs, patterns);
const pattern = containerOrPattern as IPattern;
if (pattern.children === undefined) { if (containerOrPattern === undefined) {
const container = containerOrPattern as IAvailableContainer; console.warn(`[InitializeChildrenWithPattern] PatternId ${patternId} was neither found as Pattern nor as IAvailableContainer`);
// Add Container return;
AddNewContainerToParent(
container,
configuration,
containers,
newContainer,
0, 0,
newCounters,
symbols
);
} }
// BFS over patterns // BFS over patterns
BuildPatterns(containerOrPattern, newContainer, configuration, containers, newCounters, symbols, configs, patterns);
}
/**
* Apply the BFS algorithm to build containers from given patterns
* from the top to the bottom
*
* @param pattern
* @param newContainer
* @param configuration
* @param containers
* @param newCounters
* @param symbols
* @param configs
* @param patterns
*/
function BuildPatterns(
rootPattern: ContainerOrPattern,
newContainer: ContainerModel,
configuration: IConfiguration,
containers: Map<string, IContainerModel>,
newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel>,
configs: Map<string, IAvailableContainer>,
patterns: Map<string, IPattern>
): void {
const rootNode: Node = { const rootNode: Node = {
containerOrPattern: pattern, containerOrPattern: rootPattern,
parent: newContainer parent: newContainer
}; };
const queue: Node[] = [rootNode]; const queue: Node[] = [rootNode];
while (queue.length > 0) { while (queue.length > 0) {
let levelSize = queue.length; let levelSize = queue.length;
const maxLevelSize = levelSize - 1;
while (levelSize-- !== 0) { while (levelSize-- !== 0) {
const node = queue.shift() as Node; const node = queue.shift() as Node;
const pattern = node.containerOrPattern as IPattern; const newParent = AddContainerInLevel(node, maxLevelSize, levelSize, configuration, containers, newCounters, symbols, configs);
if (pattern.children === undefined) { if (newParent === undefined) {
// Add Container // node.pattern is not a IPattern, there is no children to iterate
const containerConfig = node.containerOrPattern as IAvailableContainer;
AddNewContainerToParent(
containerConfig,
configuration,
containers,
node.parent,
0, 0,
newCounters,
symbols
);
continue; continue;
} }
let parent = node.parent; for (let i = 0; i <= newParent.pattern.children.length - 1; i++) {
if (pattern.wrapper !== undefined) { const nextNode = GetNextNode(newParent.parent, newParent.pattern, i, configs, patterns);
// Add Container
const container = configs.get(pattern.wrapper);
if (container === undefined) {
console.warn(`[InitializeChildrenFromPattern] IAvailableContainer from pattern was not found in the configuration: ${pattern.wrapper}.
Process will ignore the container.`);
} else {
const newChildContainer = AddNewContainerToParent(
container,
configuration,
containers,
parent,
0, 0,
newCounters,
symbols,
undefined,
false
);
// iterate if (nextNode === undefined) {
parent = newChildContainer;
}
}
for (let i = pattern.children.length - 1; i >= 0; i--) {
const childId: string = pattern.children[i];
const child = GetPattern(childId, configs, patterns);
if (child === undefined) {
continue; continue;
} }
const nextNode: Node = {
containerOrPattern: child,
parent
};
queue.push(nextNode); queue.push(nextNode);
} }
} }
} }
} }
/**
* Add a new container in the parent if node.pattern is a Pattern.
* Then, return the next parent to iterate with a pattern/container.
* Otherwise, if node.pattern is a IAvailableContainer,
* create the container from node.pattern and return undefined.
*
* @param node
* @param maxLevelSize
* @param levelSize
* @param configuration
* @param containers
* @param newCounters
* @param symbols
* @param configs
* @returns
*/
function AddContainerInLevel(
node: Node,
maxLevelSize: number,
levelSize: number,
configuration: IConfiguration,
containers: Map<string, IContainerModel>,
newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel>,
configs: Map<string, IAvailableContainer>
): { parent: IContainerModel, pattern: IPattern } | undefined {
if (!('children' in node.containerOrPattern)) {
// Add Container from pattern
const containerConfig: IAvailableContainer = node.containerOrPattern;
const index = maxLevelSize - levelSize;
AddNewContainerToParent(
containerConfig,
configuration,
containers,
node.parent,
index, 0,
newCounters,
symbols
);
return;
}
const pattern: IPattern = node.containerOrPattern;
const parent = node.parent;
if (pattern.wrapper === undefined) {
return { parent, pattern };
}
// Add Container from wrapper
// and set the new parent as the child of this parent
const container = configs.get(pattern.wrapper);
if (container === undefined) {
console.warn(`[InitializeChildrenFromPattern] IAvailableContainer from pattern was not found in the configuration: ${pattern.wrapper}.
Process will ignore the container.`);
return { parent, pattern };
}
const newChildContainer = AddNewContainerToParent(
container,
configuration,
containers,
parent,
0, 0,
newCounters,
symbols,
undefined,
false
);
// change the parent to be the child of the wrapper
return { parent: newChildContainer, pattern };
}
/**
* Return the next node from the given pattern from the configs
*
* @param parent
* @param pattern
* @param i
* @param configs
* @param patterns
* @returns {Node} The next node
*/
function GetNextNode(
parent: IContainerModel,
pattern: IPattern,
i: number,
configs: Map<string, IAvailableContainer>,
patterns: Map<string, IPattern>
): Node | undefined {
const childId: string = pattern.children[i];
const child = GetPattern(childId, configs, patterns);
if (child === undefined) {
return undefined;
}
return {
containerOrPattern: child,
parent
};
}
interface Node { interface Node {
containerOrPattern: ContainerOrPattern containerOrPattern: ContainerOrPattern
parent: IContainerModel parent: IContainerModel

View file

@ -57,9 +57,10 @@ 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}`);
} }
const parent = FindContainerById(containers, container.properties.parentId);
if (container === mainContainerClone || if (container === mainContainerClone ||
container.parent === undefined || parent === undefined ||
container.parent === null) { parent === null) {
Swal.fire({ Swal.fire({
title: 'Oops...', title: 'Oops...',
text: 'Deleting the main container is not allowed!', text: 'Deleting the main container is not allowed!',
@ -75,10 +76,10 @@ export function DeleteContainer(
const newSymbols = structuredClone(current.symbols); const newSymbols = structuredClone(current.symbols);
UnlinkContainerFromSymbols(containers, newSymbols, container); UnlinkContainerFromSymbols(containers, newSymbols, container);
const index = container.parent.children.indexOf(container.properties.id); const index = parent.children.indexOf(container.properties.id);
const success = containers.delete(container.properties.id); const success = containers.delete(container.properties.id);
if (index > -1 && success) { if (index > -1 && success) {
container.parent.children.splice(index, 1); parent.children.splice(index, 1);
} else { } else {
throw new Error('[DeleteContainer] Could not find container among parent\'s children'); throw new Error('[DeleteContainer] Could not find container among parent\'s children');
} }
@ -90,7 +91,7 @@ export function DeleteContainer(
const selectedContainerId = GetSelectedContainerOnDelete( const selectedContainerId = GetSelectedContainerOnDelete(
containers, containers,
current.selectedContainerId, current.selectedContainerId,
container.parent, parent,
index index
); );
@ -202,12 +203,8 @@ export function OnPropertyChange(
*/ */
export function SortChildren( export function SortChildren(
containers: Map<string, IContainerModel>, containers: Map<string, IContainerModel>,
parent: IContainerModel | null | undefined parent: IContainerModel
): void { ): void {
if (parent === null || parent === undefined) {
return;
}
const isHorizontal = parent.properties.orientation === Orientation.Horizontal; const isHorizontal = parent.properties.orientation === Orientation.Horizontal;
const children = parent.children; const children = parent.children;
@ -292,14 +289,17 @@ function SetContainer(
symbols symbols
); );
// sort the children list by their position
SortChildren(containers, container.parent);
// Apply special behaviors: rigid, flex, symbol, anchor // Apply special behaviors: rigid, flex, symbol, anchor
ApplyBehaviors(containers, container, symbols); ApplyBehaviors(containers, container, symbols);
// Apply special behaviors on siblings // Apply special behaviors on siblings
ApplyBehaviorsOnSiblingsChildren(containers, container, symbols); ApplyBehaviorsOnSiblingsChildren(containers, container, symbols);
// sort the children list by their position
const parent = FindContainerById(containers, container.properties.parentId);
if (parent !== null && parent !== undefined) {
SortChildren(containers, parent);
}
} }
/** /**
@ -327,7 +327,7 @@ function AssignProperty(container: ContainerModel, key: string, value: string |
function SetMargin(): void { function SetMargin(): void {
// We need to detect change in order to apply transformation to the width and height // We need to detect change in order to apply transformation to the width and height
// Knowing the current margin is not enough as we dont keep the original width and height // Knowing the current margin is not enough as we dont keep the original width and height
const oldMarginValue: number = (container.properties.margin as any)[key]; const oldMarginValue: number = (container.properties.margin as any)[key] ?? 0;
const diff = Number(value) - oldMarginValue; const diff = Number(value) - oldMarginValue;
switch (key) { switch (key) {
case 'left': case 'left':

View file

@ -1,4 +1,5 @@
import Swal from 'sweetalert2'; import Swal from 'sweetalert2';
import { Dispatch, SetStateAction } from 'react';
import { AddMethod } from '../../../Enums/AddMethod'; import { AddMethod } from '../../../Enums/AddMethod';
import { IAction } from '../../../Interfaces/IAction'; import { IAction } from '../../../Interfaces/IAction';
import { IConfiguration } from '../../../Interfaces/IConfiguration'; import { IConfiguration } from '../../../Interfaces/IConfiguration';
@ -6,12 +7,122 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { IHistoryState } from '../../../Interfaces/IHistoryState'; 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 { DISABLE_API } from '../../../utils/default';
import { FindContainerById } from '../../../utils/itertools'; import { FindContainerById } from '../../../utils/itertools';
import { SetContainerList } from '../../API/api'; import { SetContainerList } from '../../API/api';
import { IMenuAction } from '../../Menu/Menu';
import { GetCurrentHistoryState } from '../Editor';
import { AddContainers } from './AddContainer'; import { AddContainers } from './AddContainer';
import { DeleteContainer } from './ContainerOperations'; import { DeleteContainer } from './ContainerOperations';
import { DeleteSymbol } from './SymbolOperations';
export function GetAction( export function InitActions(
menuActions: Map<string, IMenuAction[]>,
configuration: IConfiguration,
history: IHistoryState[],
historyCurrentStep: number,
setNewHistory: (newHistory: IHistoryState[]) => void,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
menuActions.set(
'',
[
{
text: 'Undo',
title: 'Undo last action',
shortcut: '<kbd>Ctrl</kbd>+<kbd>Z</kbd>',
action: () => {
if (historyCurrentStep <= 0) {
return;
}
setHistoryCurrentStep(historyCurrentStep - 1);
}
},
{
text: 'Redo',
title: 'Redo last action',
shortcut: '<kbd>Ctrl</kbd>+<kbd>Y</kbd>',
action: () => {
if (historyCurrentStep >= history.length - 1) {
return;
}
setHistoryCurrentStep(historyCurrentStep + 1);
}
}
]
);
menuActions.set(
'elements-sidebar-row',
[{
text: 'Delete',
title: 'Delete the container',
shortcut: '<kbd>Suppr</kbd>',
action: (target: HTMLElement) => {
const id = target.id;
const newHistory = DeleteContainer(
id,
history,
historyCurrentStep
);
setNewHistory(newHistory);
}
}]
);
menuActions.set(
'symbols-sidebar-row',
[{
text: 'Delete',
title: 'Delete the container',
shortcut: '<kbd>Suppr</kbd>',
action: (target: HTMLElement) => {
const id = target.id;
const newHistory = DeleteSymbol(
id,
history,
historyCurrentStep
);
setNewHistory(newHistory);
}
}]
);
// API Actions
if (DISABLE_API) {
return;
}
for (const availableContainer of configuration.AvailableContainers) {
if (availableContainer.Actions === undefined || availableContainer.Actions === null) {
continue;
}
for (const action of availableContainer.Actions) {
if (menuActions.get(availableContainer.Type) === undefined) {
menuActions.set(availableContainer.Type, []);
}
const currentState = GetCurrentHistoryState(history, historyCurrentStep);
const newAction: IMenuAction = {
text: action.Label,
title: action.Description,
action: GetAction(
action,
currentState,
configuration,
history,
historyCurrentStep,
setNewHistory
)
};
menuActions.get(availableContainer.Type)?.push(newAction);
}
}
}
function GetAction(
action: IAction, action: IAction,
currentState: IHistoryState, currentState: IHistoryState,
configuration: IConfiguration, configuration: IConfiguration,
@ -62,15 +173,16 @@ export function GetAction(
function GetPreviousAndNextSiblings(containers: Map<string, IContainerModel>, container: IContainerModel): { prev: IContainerModel | undefined, next: IContainerModel | undefined } { function GetPreviousAndNextSiblings(containers: Map<string, IContainerModel>, container: IContainerModel): { prev: IContainerModel | undefined, next: IContainerModel | undefined } {
let prev; let prev;
let next; let next;
if (container.parent !== undefined && const parent = FindContainerById(containers, container.properties.parentId);
container.parent !== null && if (parent !== undefined &&
container.parent.children.length > 1) { parent !== null &&
const index = container.parent.children.indexOf(container.properties.id); parent.children.length > 1) {
const index = parent.children.indexOf(container.properties.id);
if (index > 0) { if (index > 0) {
prev = FindContainerById(containers, container.parent.children[index - 1]); prev = FindContainerById(containers, parent.children[index - 1]);
} }
if (index < container.parent.children.length - 1) { if (index < parent.children.length - 1) {
next = FindContainerById(containers, container.parent.children[index + 1]); next = FindContainerById(containers, parent.children[index + 1]);
} }
} }
return { prev, next }; return { prev, next };
@ -86,6 +198,9 @@ function HandleSetContainerList(
setNewHistory: (newHistory: IHistoryState[]) => void setNewHistory: (newHistory: IHistoryState[]) => void
): void { ): void {
const addingBehavior = response.AddingBehavior ?? action.AddingBehavior; const addingBehavior = response.AddingBehavior ?? action.AddingBehavior;
const current = GetCurrentHistoryState(history, historyCurrentStep);
const containers = current.containers;
const parent = FindContainerById(containers, selectedContainer.properties.parentId);
switch (addingBehavior) { switch (addingBehavior) {
case AddMethod.Append: case AddMethod.Append:
setNewHistory( setNewHistory(
@ -103,6 +218,7 @@ function HandleSetContainerList(
case AddMethod.Replace: case AddMethod.Replace:
setNewHistory( setNewHistory(
HandleReplace( HandleReplace(
containers,
selectedContainer, selectedContainer,
response, response,
configuration, configuration,
@ -112,7 +228,7 @@ function HandleSetContainerList(
); );
break; break;
case AddMethod.ReplaceParent: case AddMethod.ReplaceParent:
if (selectedContainer.parent === undefined || selectedContainer.parent === null) { if (parent === undefined || parent === null) {
Swal.fire({ Swal.fire({
title: 'Error', title: 'Error',
text: 'The selected container has not parent to replace', text: 'The selected container has not parent to replace',
@ -122,7 +238,8 @@ function HandleSetContainerList(
} }
setNewHistory( setNewHistory(
HandleReplace( HandleReplace(
selectedContainer.parent, containers,
parent,
response, response,
configuration, configuration,
history, history,
@ -134,17 +251,19 @@ function HandleSetContainerList(
} }
function HandleReplace( function HandleReplace(
containers: Map<string, IContainerModel>,
selectedContainer: IContainerModel, selectedContainer: IContainerModel,
response: ISetContainerListResponse, response: ISetContainerListResponse,
configuration: IConfiguration, configuration: IConfiguration,
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number historyCurrentStep: number
): IHistoryState[] { ): IHistoryState[] {
if (selectedContainer.parent === undefined || selectedContainer.parent === null) { const parent = FindContainerById(containers, selectedContainer.properties.parentId);
if (parent === undefined || parent === null) {
throw new Error('[ReplaceContainer] Cannot replace a container that does not exists'); throw new Error('[ReplaceContainer] Cannot replace a container that does not exists');
} }
const index = selectedContainer.parent.children.indexOf(selectedContainer.properties.id); const index = parent.children.indexOf(selectedContainer.properties.id);
const newHistoryAfterDelete = DeleteContainer( const newHistoryAfterDelete = DeleteContainer(
selectedContainer.properties.id, selectedContainer.properties.id,

View file

@ -18,12 +18,11 @@ export function ApplyBehaviors(containers: Map<string, IContainerModel>, contain
try { try {
const symbol = symbols.get(container.properties.linkedSymbolId); const symbol = symbols.get(container.properties.linkedSymbolId);
if (container.properties.linkedSymbolId !== '' && symbol !== undefined) { if (container.properties.linkedSymbolId !== '' && symbol !== undefined) {
ApplySymbol(container, symbol); ApplySymbol(containers, container, symbol);
} }
if (container.parent !== undefined && container.parent !== null) { const parent = FindContainerById(containers, container.properties.parentId);
const parent = container.parent; if (parent !== undefined && parent !== null) {
if (container.properties.isAnchor) { if (container.properties.isAnchor) {
ApplyAnchor(containers, container, parent); ApplyAnchor(containers, container, parent);
} }
@ -69,11 +68,12 @@ export function ApplyBehaviorsOnSiblingsChildren(
containers: Map<string, IContainerModel>, containers: Map<string, IContainerModel>,
newContainer: IContainerModel, newContainer: IContainerModel,
symbols: Map<string, ISymbolModel>): void { symbols: Map<string, ISymbolModel>): void {
if (newContainer.parent === null || newContainer.parent === undefined) { const parent = FindContainerById(containers, newContainer.properties.parentId);
if (parent === null || parent === undefined) {
return; return;
} }
newContainer.parent.children parent.children
.forEach((containerId: string) => { .forEach((containerId: string) => {
const container = FindContainerById(containers, containerId); const container = FindContainerById(containers, containerId);
@ -81,8 +81,9 @@ export function ApplyBehaviorsOnSiblingsChildren(
return; return;
} }
if (container.parent !== null) { const containerParent = FindContainerById(containers, container.properties.parentId);
UpdateWarning(containers, container, container.parent); if (containerParent !== null && containerParent !== undefined) {
UpdateWarning(containers, container, containerParent);
} }
if (container === newContainer) { if (container === newContainer) {
@ -102,11 +103,12 @@ export function ApplyBehaviorsOnSiblingsChildren(
* @returns * @returns
*/ */
export function ApplyBehaviorsOnSiblings(containers: Map<string, IContainerModel>, newContainer: IContainerModel, symbols: Map<string, ISymbolModel>): void { export function ApplyBehaviorsOnSiblings(containers: Map<string, IContainerModel>, newContainer: IContainerModel, symbols: Map<string, ISymbolModel>): void {
if (newContainer.parent === null || newContainer.parent === undefined) { const parent = FindContainerById(containers, newContainer.properties.parentId);
if (parent === null || parent === undefined) {
return; return;
} }
newContainer.parent.children parent.children
.forEach((containerId: string) => { .forEach((containerId: string) => {
const container = FindContainerById(containers, containerId); const container = FindContainerById(containers, containerId);
@ -116,8 +118,9 @@ export function ApplyBehaviorsOnSiblings(containers: Map<string, IContainerModel
ApplyBehaviors(containers, container, symbols); ApplyBehaviors(containers, container, symbols);
if (container.parent != null) { const containerParent = FindContainerById(containers, container.properties.parentId);
UpdateWarning(containers, container, container.parent); if (containerParent !== null && containerParent !== undefined) {
UpdateWarning(containers, container, containerParent);
} }
if (container === newContainer) { if (container === newContainer) {

View file

@ -10,7 +10,7 @@ import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISizePointer } from '../../../Interfaces/ISizePointer'; import { ISizePointer } from '../../../Interfaces/ISizePointer';
import { Orientation } from '../../../Enums/Orientation'; import { Orientation } from '../../../Enums/Orientation';
import { ENABLE_HARD_RIGID } from '../../../utils/default'; import { ENABLE_HARD_RIGID } from '../../../utils/default';
import { MakeChildrenIterator } from '../../../utils/itertools'; import { FindContainerById, MakeChildrenIterator } from '../../../utils/itertools';
/** /**
* "Transform the container into a rigid body" * "Transform the container into a rigid body"
@ -122,17 +122,18 @@ export function ConstraintBodyInsideUnallocatedWidth(
containers: Map<string, IContainerModel>, containers: Map<string, IContainerModel>,
container: IContainerModel container: IContainerModel
): IContainerModel { ): IContainerModel {
if (container.parent === null || container.parent === undefined) { const parent = FindContainerById(containers, container.properties.parentId);
if (parent === null || parent === undefined) {
return container; return container;
} }
// Get the available spaces of the parent // Get the available spaces of the parent
const isHorizontal = const isHorizontal =
container.parent.properties.orientation === Orientation.Horizontal; parent.properties.orientation === Orientation.Horizontal;
const children: IContainerModel[] = [...MakeChildrenIterator(containers, container.parent.children)]; const children: IContainerModel[] = [...MakeChildrenIterator(containers, parent.children)];
const availableWidths = GetAvailableWidths( const availableWidths = GetAvailableWidths(
0, 0,
container.parent.properties.width, parent.properties.width,
children, children,
container, container,
isHorizontal isHorizontal
@ -172,7 +173,7 @@ export function ConstraintBodyInsideUnallocatedWidth(
container, container,
0, 0,
availableWidthFound.x, availableWidthFound.x,
container.parent.properties.width, parent.properties.width,
availableWidthFound.width availableWidthFound.width
); );
@ -203,7 +204,7 @@ export function ConstraintBodyInsideUnallocatedWidth(
availableWidthFound.x, availableWidthFound.x,
0, 0,
availableWidthFound.width, availableWidthFound.width,
container.parent.properties.height parent.properties.height
); );
return container; return container;

View file

@ -1,12 +1,16 @@
import { IContainerModel } from '../../../Interfaces/IContainerModel'; import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel'; import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { ApplyParentTransform } from '../../../utils/itertools'; import { ApplyParentTransform, FindContainerById } from '../../../utils/itertools';
import { RestoreX, TransformX } from '../../../utils/svg'; import { RestoreX, TransformX } from '../../../utils/svg';
export function ApplySymbol(container: IContainerModel, symbol: ISymbolModel): IContainerModel { export function ApplySymbol(containers: Map<string, IContainerModel>, container: IContainerModel, symbol: ISymbolModel): IContainerModel {
container.properties.x = TransformX(symbol.x, symbol.width, symbol.config.PositionReference); container.properties.x = TransformX(symbol.x, symbol.width, symbol.config.PositionReference);
container.properties.x = RestoreX(container.properties.x, container.properties.width, container.properties.positionReference); container.properties.x = RestoreX(container.properties.x, container.properties.width, container.properties.positionReference);
const [x] = ApplyParentTransform(container.parent, container.properties.x, 0); const parent = FindContainerById(containers, container.properties.parentId);
let x = 0;
if (parent !== undefined && parent !== null) {
([x] = ApplyParentTransform(containers, parent, container.properties.x, 0));
}
container.properties.x = x; container.properties.x = x;
return container; return container;
} }

View file

@ -6,13 +6,12 @@ import { UI } from '../UI/UI';
import { SelectContainer, DeleteContainer, OnPropertyChange } from './Actions/ContainerOperations'; import { SelectContainer, DeleteContainer, OnPropertyChange } from './Actions/ContainerOperations';
import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save'; import { SaveEditorAsJSON, SaveEditorAsSVG } from './Actions/Save';
import { OnKey } from './Actions/Shortcuts'; import { OnKey } from './Actions/Shortcuts';
import { events as EVENTS } from '../../Events/EditorEvents'; import { UseCustomEvents, UseEditorListener } from '../../Events/EditorEvents';
import { IEditorState } from '../../Interfaces/IEditorState'; import { MAX_HISTORY } from '../../utils/default';
import { DISABLE_API, MAX_HISTORY } from '../../utils/default';
import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations'; import { AddSymbol, OnPropertyChange as OnSymbolPropertyChange, DeleteSymbol, SelectSymbol } from './Actions/SymbolOperations';
import { FindContainerById } from '../../utils/itertools'; import { FindContainerById } from '../../utils/itertools';
import { IMenuAction, Menu } from '../Menu/Menu'; import { Menu } from '../Menu/Menu';
import { GetAction } from './Actions/ContextMenuActions'; import { InitActions } from './Actions/ContextMenuActions';
import { AddContainerToSelectedContainer, AddContainer } from './Actions/AddContainer'; import { AddContainerToSelectedContainer, AddContainer } from './Actions/AddContainer';
interface IEditorProps { interface IEditorProps {
@ -22,112 +21,6 @@ interface IEditorProps {
historyCurrentStep: number historyCurrentStep: number
} }
function InitActions(
menuActions: Map<string, IMenuAction[]>,
configuration: IConfiguration,
history: IHistoryState[],
historyCurrentStep: number,
setNewHistory: (newHistory: IHistoryState[]) => void,
setHistoryCurrentStep: Dispatch<SetStateAction<number>>
): void {
menuActions.set(
'',
[
{
text: 'Undo',
title: 'Undo last action',
shortcut: '<kbd>Ctrl</kbd>+<kbd>Z</kbd>',
action: () => {
if (historyCurrentStep <= 0) {
return;
}
setHistoryCurrentStep(historyCurrentStep - 1);
}
},
{
text: 'Redo',
title: 'Redo last action',
shortcut: '<kbd>Ctrl</kbd>+<kbd>Y</kbd>',
action: () => {
if (historyCurrentStep >= history.length - 1) {
return;
}
setHistoryCurrentStep(historyCurrentStep + 1);
}
}
]
);
menuActions.set(
'elements-sidebar-row',
[{
text: 'Delete',
title: 'Delete the container',
shortcut: '<kbd>Suppr</kbd>',
action: (target: HTMLElement) => {
const id = target.id;
const newHistory = DeleteContainer(
id,
history,
historyCurrentStep
);
setNewHistory(newHistory);
}
}]
);
menuActions.set(
'symbols-sidebar-row',
[{
text: 'Delete',
title: 'Delete the container',
shortcut: '<kbd>Suppr</kbd>',
action: (target: HTMLElement) => {
const id = target.id;
const newHistory = DeleteSymbol(
id,
history,
historyCurrentStep
);
setNewHistory(newHistory);
}
}]
);
// API Actions
if (DISABLE_API) {
return;
}
for (const availableContainer of configuration.AvailableContainers) {
if (availableContainer.Actions === undefined || availableContainer.Actions === null) {
continue;
}
for (const action of availableContainer.Actions) {
if (menuActions.get(availableContainer.Type) === undefined) {
menuActions.set(availableContainer.Type, []);
}
const currentState = GetCurrentHistoryState(history, historyCurrentStep);
const newAction: IMenuAction = {
text: action.Label,
title: action.Description,
action: GetAction(
action,
currentState,
configuration,
history,
historyCurrentStep,
setNewHistory
)
};
menuActions.get(availableContainer.Type)?.push(newAction);
}
}
}
function UseShortcuts( function UseShortcuts(
history: IHistoryState[], history: IHistoryState[],
historyCurrentStep: number, historyCurrentStep: number,
@ -152,63 +45,6 @@ function UseShortcuts(
}); });
} }
function UseCustomEvents(
root: Element | Document,
history: IHistoryState[],
historyCurrentStep: number,
configuration: IConfiguration,
editorRef: React.RefObject<HTMLDivElement>,
setNewHistory: (newHistory: IHistoryState[], historyCurrentStep?: number) => void
): void {
useEffect(() => {
const editorState: IEditorState = {
history,
historyCurrentStep,
configuration
};
const funcs = new Map<string, () => void>();
for (const event of EVENTS) {
function Func(eventInitDict?: CustomEventInit): void {
return event.func(
root,
editorState,
setNewHistory,
eventInitDict
);
}
editorRef.current?.addEventListener(event.name, Func);
funcs.set(event.name, Func);
}
return () => {
for (const event of EVENTS) {
const func = funcs.get(event.name);
if (func === undefined) {
continue;
}
editorRef.current?.removeEventListener(event.name, func);
}
};
});
}
function UseEditorListener(
root: Element | Document,
history: IHistoryState[],
historyCurrentStep: number,
configuration: IConfiguration
): void {
useEffect(() => {
const editorState: IEditorState = {
history,
historyCurrentStep,
configuration
};
const event = new CustomEvent('editorListener', { detail: editorState });
root.dispatchEvent(event);
});
}
/** /**
* Return a macro function to use both setHistory * Return a macro function to use both setHistory
* and setHistoryCurrentStep at the same time * and setHistoryCurrentStep at the same time
@ -279,14 +115,11 @@ export function Editor(props: IEditorProps): JSX.Element {
return ( return (
<div ref={editorRef} className="Editor font-sans h-full"> <div ref={editorRef} className="Editor font-sans h-full">
<UI <UI
selectedContainer={selected} editorState={{
current={current} configuration: props.configuration,
history={history} history,
historyCurrentStep={historyCurrentStep} historyCurrentStep
availableContainers={configuration.AvailableContainers} }}
availableSymbols={configuration.AvailableSymbols}
categories={configuration.Categories}
apiConfiguration={configuration.APIConfiguration}
selectContainer={(container) => setNewHistory( selectContainer={(container) => setNewHistory(
SelectContainer( SelectContainer(
container, container,
@ -307,16 +140,17 @@ export function Editor(props: IEditorProps): JSX.Element {
historyCurrentStep historyCurrentStep
))} ))}
addContainer={(type) => { addContainer={(type) => {
const newHistory = AddContainerToSelectedContainer( if (selected === null || selected === undefined) {
return;
}
setNewHistory(AddContainerToSelectedContainer(
type, type,
selected, selected,
configuration, configuration,
history, history,
historyCurrentStep historyCurrentStep
); ));
if (newHistory !== null) {
setNewHistory(newHistory);
}
}} }}
addContainerAt={(index, type, parent) => setNewHistory( addContainerAt={(index, type, parent) => setNewHistory(
AddContainer( AddContainer(

View file

@ -87,8 +87,9 @@ function HandleOnDrop(
return; return;
} }
if (targetContainer.parent === null || const parent = FindContainerById(containers, targetContainer.properties.parentId);
targetContainer.parent === undefined) { if (parent === null ||
parent === undefined) {
throw new Error('[handleDrop] Tried to drop into a child container without a parent!'); throw new Error('[handleDrop] Tried to drop into a child container without a parent!');
} }
@ -97,11 +98,11 @@ function HandleOnDrop(
// locate the hitboxes // locate the hitboxes
if (y < 12) { if (y < 12) {
const index = targetContainer.parent.children.indexOf(targetContainer.properties.id); const index = parent.children.indexOf(targetContainer.properties.id);
addContainer( addContainer(
index, index,
type, type,
targetContainer.parent.properties.id parent.properties.id
); );
} else if (y < 24) { } else if (y < 24) {
addContainer( addContainer(
@ -109,11 +110,11 @@ function HandleOnDrop(
type, type,
targetContainer.properties.id); targetContainer.properties.id);
} else { } else {
const index = targetContainer.parent.children.indexOf(targetContainer.properties.id); const index = parent.children.indexOf(targetContainer.properties.id);
addContainer( addContainer(
index + 1, index + 1,
type, type,
targetContainer.parent.properties.id parent.properties.id
); );
} }
} }

View file

@ -138,7 +138,7 @@ function AddClassSpecificActions(
shortcut={action.shortcut} shortcut={action.shortcut}
onClick={() => action.action(target)} />); onClick={() => action.action(target)} />);
}); });
children.push(<hr className='border-slate-400' />); children.push(<hr key={`contextmenu-hr-${count}`} className='border-slate-400' />);
}; };
return count; return count;
} }

View file

@ -31,7 +31,7 @@ function GetDimensionsNodes(
max = -Infinity; max = -Infinity;
} }
const absoluteX = GetAbsolutePosition(container)[0]; const absoluteX = GetAbsolutePosition(containers, container)[0];
const x = TransformX(absoluteX, container.properties.width, container.properties.positionReference); const x = TransformX(absoluteX, container.properties.width, container.properties.positionReference);
lastY = container.properties.y + container.properties.height; lastY = container.properties.y + container.properties.height;
if (x < min) { if (x < min) {
@ -74,7 +74,7 @@ function AddNewDimension(currentDepth: number, min: number, max: number, lastY:
const id = `dim-depth-${currentDepth}`; const id = `dim-depth-${currentDepth}`;
const xStart = min; const xStart = min;
const xEnd = max; const xEnd = max;
const y = (lastY + (DIMENSION_MARGIN * (currentDepth + 1))) / scale; const y = lastY + (DIMENSION_MARGIN * (currentDepth + 1)) / scale;
const strokeWidth = 1; const strokeWidth = 1;
const width = xEnd - xStart; const width = xEnd - xStart;
const text = width const text = width

View file

@ -6,6 +6,7 @@ import { GetAbsolutePosition } from '../../../../utils/itertools';
import { RemoveMargin } from '../../../../utils/svg'; import { RemoveMargin } from '../../../../utils/svg';
interface ISelectorProps { interface ISelectorProps {
containers: Map<string, IContainerModel>
selected?: IContainerModel selected?: IContainerModel
scale?: number scale?: number
} }
@ -19,7 +20,7 @@ export function Selector(props: ISelectorProps): JSX.Element {
} }
const scale = (props.scale ?? 1); const scale = (props.scale ?? 1);
let [x, y] = GetAbsolutePosition(props.selected); let [x, y] = GetAbsolutePosition(props.containers, props.selected);
let [width, height] = [ let [width, height] = [
props.selected.properties.width, props.selected.properties.width,
props.selected.properties.height props.selected.properties.height

View file

@ -98,7 +98,7 @@ export function SVG(props: ISVGProps): JSX.Element {
: null} : null}
<DimensionLayer containers={props.containers} scale={scale} root={props.children} /> <DimensionLayer containers={props.containers} scale={scale} root={props.children} />
<SymbolLayer scale={scale} symbols={props.symbols} /> <SymbolLayer scale={scale} symbols={props.symbols} />
<Selector scale={scale} selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */} <Selector containers={props.containers} scale={scale} selected={props.selected} /> {/* leave this at the end so it can be removed during the svg export */}
</svg> </svg>
</ReactSVGPanZoom> </ReactSVGPanZoom>
</div> </div>

View file

@ -1,16 +1,11 @@
import * as React from 'react'; import * as React from 'react';
import { ElementsList } from '../ElementsList/ElementsList'; import { ElementsList } from '../ElementsList/ElementsList';
import { History } from '../History/History'; import { History } from '../History/History';
import { IAvailableContainer } from '../../Interfaces/IAvailableContainer';
import { IContainerModel } from '../../Interfaces/IContainerModel';
import { IHistoryState } from '../../Interfaces/IHistoryState';
import { Bar } from '../Bar/Bar'; import { Bar } from '../Bar/Bar';
import { IAvailableSymbol } from '../../Interfaces/IAvailableSymbol';
import { Symbols } from '../Symbols/Symbols'; import { Symbols } from '../Symbols/Symbols';
import { SymbolsSidebar } from '../SymbolsList/SymbolsList'; import { SymbolsSidebar } from '../SymbolsList/SymbolsList';
import { PropertyType } from '../../Enums/PropertyType'; import { PropertyType } from '../../Enums/PropertyType';
import { Messages } from '../Messages/Messages'; import { Messages } from '../Messages/Messages';
import { ICategory } from '../../Interfaces/ICategory';
import { Sidebar } from '../Sidebar/Sidebar'; import { Sidebar } from '../Sidebar/Sidebar';
import { Components } from '../Components/Components'; import { Components } from '../Components/Components';
import { Viewer } from '../Viewer/Viewer'; import { Viewer } from '../Viewer/Viewer';
@ -19,17 +14,11 @@ import { IMessage } from '../../Interfaces/IMessage';
import { DISABLE_API } from '../../utils/default'; import { DISABLE_API } from '../../utils/default';
import { UseWorker, UseAsync } from './UseWorker'; import { UseWorker, UseAsync } from './UseWorker';
import { FindContainerById } from '../../utils/itertools'; import { FindContainerById } from '../../utils/itertools';
import { IAPIConfiguration } from '../../Interfaces/IAPIConfiguration'; import { IEditorState } from '../../Interfaces/IEditorState';
import { GetCurrentHistoryState } from '../Editor/Editor';
export interface IUIProps { export interface IUIProps {
selectedContainer: IContainerModel | undefined editorState: IEditorState
current: IHistoryState
history: IHistoryState[]
historyCurrentStep: number
availableContainers: IAvailableContainer[]
availableSymbols: IAvailableSymbol[]
categories: ICategory[]
apiConfiguration: IAPIConfiguration | undefined
selectContainer: (containerId: string) => void selectContainer: (containerId: string) => void
deleteContainer: (containerId: string) => void deleteContainer: (containerId: string) => void
onPropertyChange: (key: string, value: string | number | boolean | number[], type?: PropertyType) => void onPropertyChange: (key: string, value: string | number | boolean | number[], type?: PropertyType) => void
@ -67,21 +56,23 @@ function UseSetOrToggleSidebar(
}; };
} }
export function UI(props: IUIProps): JSX.Element { export function UI({ editorState, ...methods }: IUIProps): JSX.Element {
const [selectedSidebar, setSelectedSidebar] = React.useState<SidebarType>(SidebarType.Components); const [selectedSidebar, setSelectedSidebar] = React.useState<SidebarType>(SidebarType.Components);
const [messages, setMessages] = React.useState<IMessage[]>([]); const [messages, setMessages] = React.useState<IMessage[]>([]);
const current = GetCurrentHistoryState(editorState.history, editorState.historyCurrentStep);
const configuration = editorState.configuration;
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (window.Worker && !DISABLE_API) { if (window.Worker && !DISABLE_API) {
UseWorker( UseWorker(
props.current, current,
props.apiConfiguration?.apiGetFeedbackUrl, configuration.APIConfiguration?.apiGetFeedbackUrl,
setMessages setMessages
); );
} else if (!DISABLE_API) { } else if (!DISABLE_API) {
UseAsync( UseAsync(
props.current, current,
props.apiConfiguration?.apiGetFeedbackUrl, configuration.APIConfiguration?.apiGetFeedbackUrl,
setMessages setMessages
); );
} }
@ -94,61 +85,63 @@ export function UI(props: IUIProps): JSX.Element {
let leftChildren: JSX.Element = (<></>); let leftChildren: JSX.Element = (<></>);
let rightChildren: JSX.Element = (<></>); let rightChildren: JSX.Element = (<></>);
const mainContainer = FindContainerById(props.current.containers, props.current.mainContainer) const mainContainer = FindContainerById(current.containers, current.mainContainer);
if (mainContainer === undefined) { if (mainContainer === undefined) {
throw new Error('Tried to initialized UI but there is no main container!'); throw new Error('Tried to initialized UI but there is no main container!');
} }
const selectedContainer = FindContainerById(current.containers, current.selectedContainerId);
switch (selectedSidebar) { switch (selectedSidebar) {
case SidebarType.Components: case SidebarType.Components:
leftSidebarTitle = 'Components'; leftSidebarTitle = 'Components';
leftChildren = <Components leftChildren = <Components
selectedContainer={props.selectedContainer} selectedContainer={selectedContainer}
componentOptions={props.availableContainers} componentOptions={configuration.AvailableContainers}
categories={props.categories} categories={configuration.Categories}
buttonOnClick={props.addContainer} buttonOnClick={methods.addContainer}
/>; />;
rightSidebarTitle = 'Elements'; rightSidebarTitle = 'Elements';
rightChildren = <ElementsList rightChildren = <ElementsList
containers={props.current.containers} containers={current.containers}
mainContainer={mainContainer} mainContainer={mainContainer}
symbols={props.current.symbols} symbols={current.symbols}
selectedContainer={props.selectedContainer} selectedContainer={selectedContainer}
onPropertyChange={props.onPropertyChange} onPropertyChange={methods.onPropertyChange}
selectContainer={props.selectContainer} selectContainer={methods.selectContainer}
addContainer={props.addContainerAt} addContainer={methods.addContainerAt}
/>; />;
break; break;
case SidebarType.Symbols: case SidebarType.Symbols:
leftSidebarTitle = 'Symbols'; leftSidebarTitle = 'Symbols';
leftChildren = <Symbols leftChildren = <Symbols
componentOptions={props.availableSymbols} componentOptions={configuration.AvailableSymbols}
buttonOnClick={props.addSymbol} buttonOnClick={methods.addSymbol}
/>; />;
rightSidebarTitle = 'Symbols'; rightSidebarTitle = 'Symbols';
rightChildren = <SymbolsSidebar rightChildren = <SymbolsSidebar
selectedSymbolId={props.current.selectedSymbolId} selectedSymbolId={current.selectedSymbolId}
symbols={props.current.symbols} symbols={current.symbols}
onPropertyChange={props.onSymbolPropertyChange} onPropertyChange={methods.onSymbolPropertyChange}
selectSymbol={props.selectSymbol} selectSymbol={methods.selectSymbol}
/>; />;
break; break;
case SidebarType.History: case SidebarType.History:
leftSidebarTitle = 'Timeline'; leftSidebarTitle = 'Timeline';
leftChildren = <History leftChildren = <History
history={props.history} history={editorState.history}
historyCurrentStep={props.historyCurrentStep} historyCurrentStep={editorState.historyCurrentStep}
jumpTo={props.loadState} jumpTo={methods.loadState}
/>; />;
break; break;
case SidebarType.Messages: case SidebarType.Messages:
leftSidebarTitle = 'Messages'; leftSidebarTitle = 'Messages';
leftChildren = <Messages leftChildren = <Messages
historyState={props.current} historyState={current}
messages={messages} messages={messages}
clearMessage={() => setMessages([])} clearMessage={() => setMessages([])}
/>; />;
@ -157,8 +150,8 @@ export function UI(props: IUIProps): JSX.Element {
case SidebarType.Settings: case SidebarType.Settings:
leftSidebarTitle = 'Settings'; leftSidebarTitle = 'Settings';
leftChildren = <Settings leftChildren = <Settings
saveEditorAsJSON={props.saveEditorAsJSON} saveEditorAsJSON={methods.saveEditorAsJSON}
saveEditorAsSVG={props.saveEditorAsSVG} saveEditorAsSVG={methods.saveEditorAsSVG}
/>; />;
break; break;
} }
@ -166,7 +159,7 @@ export function UI(props: IUIProps): JSX.Element {
const isLeftSidebarOpen = selectedSidebar !== SidebarType.None; const isLeftSidebarOpen = selectedSidebar !== SidebarType.None;
const isRightSidebarOpen = selectedSidebar === SidebarType.Components || selectedSidebar === SidebarType.Symbols; const isRightSidebarOpen = selectedSidebar === SidebarType.Components || selectedSidebar === SidebarType.Symbols;
let isLeftSidebarOpenClasses = new Set<string>([ const isLeftSidebarOpenClasses = new Set<string>([
'left-sidebar', 'left-sidebar',
'left-16', 'left-16',
'-bottom-full', '-bottom-full',
@ -222,9 +215,9 @@ export function UI(props: IUIProps): JSX.Element {
<Viewer <Viewer
isLeftSidebarOpen={isLeftSidebarOpen} isLeftSidebarOpen={isLeftSidebarOpen}
isRightSidebarOpen={isRightSidebarOpen} isRightSidebarOpen={isRightSidebarOpen}
current={props.current} current={current}
selectedContainer={props.selectedContainer} selectedContainer={selectedContainer}
selectContainer={props.selectContainer} selectContainer={methods.selectContainer}
/> />
<Sidebar <Sidebar
className={`right-sidebar ${isRightSidebarOpenClasses}`} className={`right-sidebar ${isRightSidebarOpenClasses}`}

View file

@ -147,6 +147,7 @@ export function Viewer({
// Draw selector // Draw selector
RenderSelector(ctx, frameCount, { RenderSelector(ctx, frameCount, {
containers: current.containers,
scale, scale,
selected: selectedContainer selected: selectedContainer
}); });

View file

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { IConfiguration } from '../Interfaces/IConfiguration'; import { IConfiguration } from '../Interfaces/IConfiguration';
import { IEditorState } from '../Interfaces/IEditorState'; import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState'; import { IHistoryState } from '../Interfaces/IHistoryState';
@ -22,6 +23,38 @@ export const events: IAppEvent[] = [
{ name: 'getDefaultEditorState', func: GetDefaultEditorState } { name: 'getDefaultEditorState', func: GetDefaultEditorState }
]; ];
export function UseCustomEvents(
root: Element | Document,
appRef: React.RefObject<HTMLDivElement>,
setEditor: (newState: IEditorState) => void,
setLoaded: (loaded: boolean) => void
): void {
useEffect(() => {
const funcs = new Map<string, () => void>();
for (const event of events) {
function Func(eventInitDict?: CustomEventInit): void {
return event.func(
root,
setEditor,
setLoaded,
eventInitDict
);
}
appRef.current?.addEventListener(event.name, Func);
funcs.set(event.name, Func);
}
return () => {
for (const event of events) {
const func = funcs.get(event.name);
if (func === undefined) {
continue;
}
appRef.current?.removeEventListener(event.name, func);
}
};
});
}
function SetEditor( function SetEditor(
root: Element | Document, root: Element | Document,
setEditor: (newState: IEditorState) => void, setEditor: (newState: IEditorState) => void,

View file

@ -1,11 +1,13 @@
import { useEffect } from 'react';
import { AddContainer as AddContainerAction, AddContainerToSelectedContainer as AddContainerToSelectedContainerAction } from '../Components/Editor/Actions/AddContainer'; import { AddContainer as AddContainerAction, AddContainerToSelectedContainer as AddContainerToSelectedContainerAction } from '../Components/Editor/Actions/AddContainer';
import { DeleteContainer as DeleteContainerAction, SelectContainer as SelectContainerAction } from '../Components/Editor/Actions/ContainerOperations'; import { DeleteContainer as DeleteContainerAction, SelectContainer as SelectContainerAction } from '../Components/Editor/Actions/ContainerOperations';
import { AddSymbol as AddSymbolAction, DeleteSymbol as DeleteSymbolAction, SelectSymbol as SelectSymbolAction } from '../Components/Editor/Actions/SymbolOperations'; import { AddSymbol as AddSymbolAction, DeleteSymbol as DeleteSymbolAction, SelectSymbol as SelectSymbolAction } from '../Components/Editor/Actions/SymbolOperations';
import { GetCurrentHistory } from '../Components/Editor/Editor'; import { GetCurrentHistory } from '../Components/Editor/Editor';
import { IConfiguration } from '../Interfaces/IConfiguration';
import { IEditorState } from '../Interfaces/IEditorState'; import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState'; import { IHistoryState } from '../Interfaces/IHistoryState';
import { FindContainerById } from '../utils/itertools'; import { FindContainerById } from '../utils/itertools';
import { GetCircularReplacer, ReviveHistory as ReviveHistoryAction } from '../utils/saveload'; import { GetCircularReplacer } from '../utils/saveload';
export interface IEditorEvent { export interface IEditorEvent {
name: string name: string
@ -34,6 +36,63 @@ export const events: IEditorEvent[] = [
{ name: 'deleteSymbol', func: DeleteSymbol } { name: 'deleteSymbol', func: DeleteSymbol }
]; ];
export function UseCustomEvents(
root: Element | Document,
history: IHistoryState[],
historyCurrentStep: number,
configuration: IConfiguration,
editorRef: React.RefObject<HTMLDivElement>,
setNewHistory: (newHistory: IHistoryState[], historyCurrentStep?: number) => void
): void {
useEffect(() => {
const editorState: IEditorState = {
history,
historyCurrentStep,
configuration
};
const funcs = new Map<string, () => void>();
for (const event of events) {
function Func(eventInitDict?: CustomEventInit): void {
return event.func(
root,
editorState,
setNewHistory,
eventInitDict
);
}
editorRef.current?.addEventListener(event.name, Func);
funcs.set(event.name, Func);
}
return () => {
for (const event of events) {
const func = funcs.get(event.name);
if (func === undefined) {
continue;
}
editorRef.current?.removeEventListener(event.name, func);
}
};
});
}
export function UseEditorListener(
root: Element | Document,
history: IHistoryState[],
historyCurrentStep: number,
configuration: IConfiguration
): void {
useEffect(() => {
const editorState: IEditorState = {
history,
historyCurrentStep,
configuration
};
const event = new CustomEvent('editorListener', { detail: editorState });
root.dispatchEvent(event);
});
}
function GetEditorState(root: Element | Document, function GetEditorState(root: Element | Document,
editorState: IEditorState): void { editorState: IEditorState): void {
const customEvent = new CustomEvent<IEditorState>('getEditorState', { detail: structuredClone(editorState) }); const customEvent = new CustomEvent<IEditorState>('getEditorState', { detail: structuredClone(editorState) });
@ -181,20 +240,16 @@ function AppendContainerToSelectedContainer(root: Element | Document,
const selected = FindContainerById(currentState.containers, currentState.selectedContainerId); const selected = FindContainerById(currentState.containers, currentState.selectedContainerId);
const newHistory = AddContainerToSelectedContainerAction( if (selected !== null && selected !== undefined) {
type, setNewHistory(AddContainerToSelectedContainerAction(
selected, type,
editorState.configuration, selected,
history, editorState.configuration,
editorState.historyCurrentStep history,
); editorState.historyCurrentStep
));
if (newHistory === null) {
return;
} }
setNewHistory(newHistory);
const customEvent = new CustomEvent<IHistoryState>( const customEvent = new CustomEvent<IHistoryState>(
'appendContainerToSelectedContainer', 'appendContainerToSelectedContainer',
{ detail: structuredClone(editorState.history[editorState.historyCurrentStep]) }); { detail: structuredClone(editorState.history[editorState.historyCurrentStep]) });

View file

@ -6,6 +6,7 @@ import { IAction } from './IAction';
import { IMargin } from './IMargin'; import { IMargin } from './IMargin';
import { Orientation } from '../Enums/Orientation'; import { Orientation } from '../Enums/Orientation';
import { Position } from '../Enums/Position'; import { Position } from '../Enums/Position';
import { IKeyValue } from './IKeyValue';
/** Model of available container used in application configuration */ /** Model of available container used in application configuration */
export interface IAvailableContainer { export interface IAvailableContainer {
@ -161,5 +162,5 @@ export interface IAvailableContainer {
* (optional) * (optional)
* User data that can be used for data storage or custom SVG * User data that can be used for data storage or custom SVG
*/ */
UserData?: object UserData?: IKeyValue[]
} }

View file

@ -2,9 +2,6 @@ import { IContainerProperties } from './IContainerProperties';
export interface IContainerModel { export interface IContainerModel {
children: string[] children: string[]
// TODO: Remove parent now that accessing the parent by id is faster.
// TODO: Use GetContainerById(container.properties.parentId) as the better alternative.
parent: IContainerModel | null
properties: IContainerProperties properties: IContainerProperties
userData: Record<string, string | number> userData: Record<string, string | number>
} }
@ -15,18 +12,13 @@ export interface IContainerModel {
*/ */
export class ContainerModel implements IContainerModel { export class ContainerModel implements IContainerModel {
public children: string[]; public children: string[];
// TODO: Remove parent now that accessing the parent by id is faster.
// TODO: Use GetContainerById(container.properties.parentId) as the better alternative.
public parent: IContainerModel | null;
public properties: IContainerProperties; public properties: IContainerProperties;
public userData: Record<string, string | number>; public userData: Record<string, string | number>;
constructor( constructor(
parent: IContainerModel | null,
properties: IContainerProperties, properties: IContainerProperties,
children: string[] = [], children: string[] = [],
userData = {}) { userData = {}) {
this.parent = parent;
this.properties = properties; this.properties = properties;
this.children = children; this.children = children;
this.userData = userData; this.userData = userData;

View file

@ -3,6 +3,7 @@ import { PositionReference } from '../Enums/PositionReference';
import { IMargin } from './IMargin'; import { IMargin } from './IMargin';
import { Orientation } from '../Enums/Orientation'; import { Orientation } from '../Enums/Orientation';
import { Position } from '../Enums/Position'; import { Position } from '../Enums/Position';
import { IKeyValue } from './IKeyValue';
/** /**
* Properties of a container * Properties of a container
@ -122,5 +123,5 @@ export interface IContainerProperties {
* (optional) * (optional)
* User data that can be used for data storage or custom SVG * User data that can be used for data storage or custom SVG
*/ */
userData?: object userData?: IKeyValue[]
} }

View file

@ -0,0 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
export interface IKeyValue {
Key: string
Value: string
}

View file

@ -121,7 +121,6 @@ export function GetDefaultEditorState(configuration: IConfiguration): IEditorSta
); );
} }
const mainContainer = new ContainerModel( const mainContainer = new ContainerModel(
null,
mainContainerConfig mainContainerConfig
); );
const containers = new Map<string, IContainerModel>(); const containers = new Map<string, IContainerModel>();
@ -235,8 +234,6 @@ export function GetDefaultContainerProps(type: string,
height: number, height: number,
containerConfig: IAvailableContainer): IContainerProperties { containerConfig: IAvailableContainer): IContainerProperties {
const orientation = containerConfig.Orientation ?? Orientation.Horizontal; const orientation = containerConfig.Orientation ?? Orientation.Horizontal;
const defaultIsFlex = (orientation === Orientation.Vertical && containerConfig.Height === undefined) ||
(orientation === Orientation.Horizontal && containerConfig.Width === undefined);
return ({ return ({
id: `${type}-${typeCount}`, id: `${type}-${typeCount}`,
type, type,
@ -250,7 +247,7 @@ export function GetDefaultContainerProps(type: string,
width, width,
height, height,
isAnchor: containerConfig.IsAnchor ?? false, isAnchor: containerConfig.IsAnchor ?? false,
isFlex: containerConfig.IsFlex ?? defaultIsFlex, isFlex: containerConfig.IsFlex ?? false,
positionReference: containerConfig.PositionReference ?? PositionReference.TopLeft, positionReference: containerConfig.PositionReference ?? PositionReference.TopLeft,
minWidth: containerConfig.MinWidth ?? 1, minWidth: containerConfig.MinWidth ?? 1,
maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER, maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER,

View file

@ -123,16 +123,16 @@ export function * MakeRecursionDFSIterator(
/** /**
* Returns the depth of the container * Returns the depth of the container
* @returns The depth of the container * @returns The depth of the container
* @deprecated Please avoid using this function inside an iterationl, * @deprecated Please avoid using this function inside an iteration,
* use recursive DFS or iterative BFS to get the depth * use recursive DFS or iterative BFS to get the depth
*/ */
export function GetDepth(parent: IContainerModel): number { export function GetDepth(containers: Map<string, IContainerModel>, parent: IContainerModel): number {
let depth = 0; let depth = 0;
let current: IContainerModel | null = parent; let current: IContainerModel | null = parent;
while (current != null) { while (current != null) {
depth++; depth++;
current = current.parent; current = FindContainerById(containers, current.properties.parentId) ?? null;
} }
return depth; return depth;
@ -142,22 +142,31 @@ export function GetDepth(parent: IContainerModel): number {
* Returns the absolute position by iterating to the parent * Returns the absolute position by iterating to the parent
* @returns The absolute position of the container * @returns The absolute position of the container
*/ */
export function GetAbsolutePosition(container: IContainerModel): [number, number] { export function GetAbsolutePosition(containers: Map<string, IContainerModel>, container: IContainerModel): [number, number] {
const x = container.properties.x; const x = container.properties.x;
const y = container.properties.y; const y = container.properties.y;
return CancelParentTransform(container.parent, x, y); const parent = FindContainerById(containers, container.properties.parentId) ?? null;
return CancelParentTransform(containers, parent, x, y);
} }
export function GetContainerLinkedList(container: IContainerModel, stop?: IContainerModel): IContainerModel[] { export function GetContainerLinkedList(
const it = MakeContainerLinkedListIterator(container, stop); containers: Map<string, IContainerModel>,
container: IContainerModel,
stop?: IContainerModel
): IContainerModel[] {
const it = MakeContainerLinkedListIterator(containers, container, stop);
return [...it]; return [...it];
} }
export function * MakeContainerLinkedListIterator(container: IContainerModel, stop?: IContainerModel): Generator<IContainerModel, void, unknown> { export function * MakeContainerLinkedListIterator(
containers: Map<string, IContainerModel>,
container: IContainerModel,
stop?: IContainerModel
): Generator<IContainerModel, void, unknown> {
let current: IContainerModel | null = container; let current: IContainerModel | null = container;
while (current !== stop && current != null) { while (current !== stop && current != null) {
yield current; yield current;
current = current.parent; current = FindContainerById(containers, current.properties.parentId) ?? null;
} }
} }
@ -169,6 +178,7 @@ export function * MakeContainerLinkedListIterator(container: IContainerModel, st
* @returns x and y such that the transformations of the parent are cancelled * @returns x and y such that the transformations of the parent are cancelled
*/ */
export function CancelParentTransform( export function CancelParentTransform(
containers: Map<string, IContainerModel>,
parent: IContainerModel | null, parent: IContainerModel | null,
x: number, x: number,
y: number, y: number,
@ -178,7 +188,7 @@ export function CancelParentTransform(
return [x, y]; return [x, y];
} }
const it = MakeContainerLinkedListIterator(parent, stop); const it = MakeContainerLinkedListIterator(containers, parent, stop);
for (const current of it) { for (const current of it) {
x += current.properties.x; x += current.properties.x;
y += current.properties.y; y += current.properties.y;
@ -195,6 +205,7 @@ export function CancelParentTransform(
* @returns x and y such that the transformations of the parent are applied * @returns x and y such that the transformations of the parent are applied
*/ */
export function ApplyParentTransform( export function ApplyParentTransform(
containers: Map<string, IContainerModel>,
parent: IContainerModel | null, parent: IContainerModel | null,
x: number, x: number,
y: number, y: number,
@ -204,7 +215,7 @@ export function ApplyParentTransform(
return [x, y]; return [x, y];
} }
const it = MakeContainerLinkedListIterator(parent, stop); const it = MakeContainerLinkedListIterator(containers, parent, stop);
for (const current of it) { for (const current of it) {
x -= current.properties.x; x -= current.properties.x;
y -= current.properties.y; y -= current.properties.y;

View file

@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { FindContainerById, MakeDFSIterator } from './itertools';
import { IEditorState } from '../Interfaces/IEditorState'; import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState'; import { IHistoryState } from '../Interfaces/IHistoryState';
import { IContainerModel } from '../Interfaces/IContainerModel'; import { IContainerModel } from '../Interfaces/IContainerModel';
import { ISymbolModel } from '../Interfaces/ISymbolModel';
/** /**
* Revive the Editor state * Revive the Editor state
@ -30,43 +30,18 @@ export function ReviveState(state: IHistoryState): void {
return; return;
} }
state.symbols = new Map(state.symbols); const symbols: Array<{ Key: string, Value: ISymbolModel }> = (state.symbols) as any;
state.symbols = new Map(symbols.map(({ Key, Value }) => [Key, Value]));
for (const symbol of state.symbols.values()) { for (const symbol of state.symbols.values()) {
symbol.linkedContainers = new Set(symbol.linkedContainers); symbol.linkedContainers = new Set(symbol.linkedContainers);
} }
const containers: Array<{ Key: string, Value: IContainerModel }> = (state.containers) as any; const containers: Array<{ Key: string, Value: IContainerModel }> = (state.containers) as any;
state.containers = new Map(containers.map(({ Key, Value }: {Key: string, Value: IContainerModel}) => [Key, Value])); state.containers = new Map(containers.map(({ Key, Value }) => [Key, Value]));
const root = FindContainerById(state.containers, state.mainContainer);
if (root === undefined) {
return;
}
// TODO: remove parent and remove this bloc of code
// TODO: See IContainerModel.ts for more detail
const it = MakeDFSIterator(root, state.containers);
for (const container of it) {
const parentId = container.properties.parentId;
if (parentId === null) {
container.parent = null;
continue;
}
const parent = FindContainerById(state.containers, parentId);
if (parent === undefined) {
continue;
}
container.parent = parent;
}
} }
export function GetCircularReplacer(): (key: any, value: object | Map<string, any> | null) => object | null | undefined { export function GetCircularReplacer(): (key: any, value: object | Map<string, any> | null) => object | null | undefined {
return (key: any, value: object | null) => { return (key: any, value: object | null) => {
if (key === 'parent') {
return;
}
if (key === 'containers') { if (key === 'containers') {
return [...(value as Map<string, any>).entries()] return [...(value as Map<string, any>).entries()]
.map(([Key, Value]: [string, any]) => ({ Key, Value })); .map(([Key, Value]: [string, any]) => ({ Key, Value }));

View file

@ -113,6 +113,8 @@ export function RemoveMargin(
top?: number, top?: number,
right?: number right?: number
): { x: number, y: number, width: number, height: number } { ): { x: number, y: number, width: number, height: number } {
left = left ?? 0;
right = right ?? 0;
bottom = bottom ?? 0; bottom = bottom ?? 0;
top = top ?? 0; top = top ?? 0;
x = RemoveXMargin(x, left); x = RemoveXMargin(x, left);