Merged PR 212: Optimize FindChildrenById from O(n) to O(1)

Optimize FindChildrenById from O(n) to O(1):
- Deprecate FindContainerByIdDFS
- Container: Replace Children to string[]
- Add HashMap to IHistoryState that contains all containers

To access a container by id now cost O(1) without any additional cost

+ Implement CICD for SVGLibs
This commit is contained in:
Eric Nguyen 2022-10-12 09:39:54 +00:00
parent 466ef2b08b
commit c256a76e01
45 changed files with 775 additions and 450 deletions

View file

@ -69,12 +69,11 @@ export function AddContainers(
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
// Deep clone the main container for the history
const clone: IContainerModel = structuredClone(current.mainContainer);
const containers = structuredClone(current.containers);
// Find the parent in the clone
const parentClone: IContainerModel | undefined = FindContainerById(
clone, parentId
containers, parentId
);
if (parentClone === null || parentClone === undefined) {
@ -90,14 +89,15 @@ export function AddContainers(
// Iterate over the containers
availableContainers.forEach((availableContainer, typeIndex) => {
// Get the preset properties from the API
AddNewContainerToParent(availableContainer, configuration, parentClone, index, typeIndex, newCounters, current.symbols, containerIds);
AddNewContainerToParent(availableContainer, configuration, containers, parentClone, index, typeIndex, newCounters, current.symbols, containerIds);
});
// Update the state
history.push({
lastAction: `Add [${containerIds.join(', ')}] in ${parentClone.properties.id}`,
mainContainer: clone,
mainContainer: current.mainContainer,
selectedContainerId: parentClone.properties.id,
containers,
typeCounters: newCounters,
symbols: structuredClone(current.symbols),
selectedSymbolId: current.selectedSymbolId
@ -109,6 +109,7 @@ export function AddContainers(
function AddNewContainerToParent(
availableContainer: IAvailableContainer,
configuration: IConfiguration,
containers: Map<string, IContainerModel>,
parentClone: IContainerModel,
index: number,
typeIndex: number,
@ -143,7 +144,7 @@ function AddNewContainerToParent(
({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right));
// Apply an add method (append or insert/replace)
({ x, y } = ApplyAddMethod(index + typeIndex, containerConfig, parentClone, x, y));
({ x, y } = ApplyAddMethod(containers, index + typeIndex, containerConfig, parentClone, x, y));
// Set the counter of the object type in order to assign an unique id
UpdateCounters(newCounters, type);
@ -170,22 +171,25 @@ function AddNewContainerToParent(
}
);
// Register the container in the hashmap
containers.set(newContainer.properties.id, newContainer);
// Add it to the parent
if (index === parentClone.children.length) {
parentClone.children.push(newContainer);
parentClone.children.push(newContainer.properties.id);
} else {
parentClone.children.splice(index, 0, newContainer);
parentClone.children.splice(index, 0, newContainer.properties.id);
}
// Sort the parent children by x
SortChildren(parentClone);
SortChildren(containers, parentClone);
/// Handle behaviors here ///
// Apply the behaviors (flex, rigid, anchor)
ApplyBehaviors(newContainer, symbols);
ApplyBehaviors(containers, newContainer, symbols);
// Then, apply the behaviors on its siblings (mostly for flex)
ApplyBehaviorsOnSiblingsChildren(newContainer, symbols);
ApplyBehaviorsOnSiblingsChildren(containers, newContainer, symbols);
// Initialize default children of the container
if (initChilds) {
@ -194,6 +198,7 @@ function AddNewContainerToParent(
newContainer,
configuration,
containerConfig,
containers,
newCounters,
symbols
);
@ -201,6 +206,7 @@ function AddNewContainerToParent(
InitializeChildrenWithPattern(
newContainer,
configuration,
containers,
containerConfig,
newCounters,
symbols
@ -258,6 +264,7 @@ function InitializeDefaultChild(
newContainer: ContainerModel,
configuration: IConfiguration,
containerConfig: IAvailableContainer,
containers: Map<string, IContainerModel>,
newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel>
): void {
@ -276,6 +283,7 @@ function InitializeDefaultChild(
AddNewContainerToParent(
currentConfig,
configuration,
containers,
parent,
0, 0,
newCounters,
@ -286,6 +294,7 @@ function InitializeDefaultChild(
function InitializeChildrenWithPattern(
newContainer: ContainerModel,
configuration: IConfiguration,
containers: Map<string, IContainerModel>,
containerConfig: IAvailableContainer,
newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel>
@ -307,6 +316,7 @@ function InitializeChildrenWithPattern(
AddNewContainerToParent(
container,
configuration,
containers,
newContainer,
0, 0,
newCounters,
@ -334,6 +344,7 @@ function InitializeChildrenWithPattern(
AddNewContainerToParent(
containerConfig,
configuration,
containers,
node.parent,
0, 0,
newCounters,
@ -350,9 +361,10 @@ function InitializeChildrenWithPattern(
console.warn(`[InitializeChildrenFromPattern] IAvailableContainer from pattern was not found in the configuration: ${pattern.wrapper}.
Process will ignore the container.`);
} else {
parent = AddNewContainerToParent(
const newChildContainer = AddNewContainerToParent(
container,
configuration,
containers,
parent,
0, 0,
newCounters,
@ -360,6 +372,9 @@ function InitializeChildrenWithPattern(
undefined,
false
);
// iterate
parent = newChildContainer;
}
}
@ -398,6 +413,7 @@ interface Node {
* @returns New offset
*/
function ApplyAddMethod(
containers: Map<string, IContainerModel>,
index: number,
containerConfig: IAvailableContainer,
parent: IContainerModel,
@ -410,8 +426,8 @@ function ApplyAddMethod(
containerConfig.AddMethod === AddMethod.Append
)) {
// Append method (default)
const lastChild: IContainerModel | undefined = parent.children
.at(index - 1);
const lastChildId: string = parent.children[index - 1];
const lastChild = FindContainerById(containers, lastChildId);
if (lastChild !== undefined) {
const isHorizontal = parent.properties.orientation === Orientation.Horizontal;

View file

@ -24,7 +24,8 @@ export function SelectContainer(
history.push({
lastAction: `Select ${containerId}`,
mainContainer: structuredClone(current.mainContainer),
mainContainer: current.mainContainer,
containers: structuredClone(current.containers),
selectedContainerId: containerId,
typeCounters: Object.assign({}, current.typeCounters),
symbols: structuredClone(current.symbols),
@ -48,8 +49,9 @@ export function DeleteContainer(
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
const mainContainerClone: IContainerModel = structuredClone(current.mainContainer);
const container = FindContainerById(mainContainerClone, containerId);
const containers = structuredClone(current.containers);
const mainContainerClone: IContainerModel | undefined = FindContainerById(containers, current.mainContainer);
const container = FindContainerById(containers, containerId);
if (container === undefined) {
throw new Error(`[DeleteContainer] Tried to delete a container that is not present in the main container: ${containerId}`);
@ -71,21 +73,22 @@ export function DeleteContainer(
}
const newSymbols = structuredClone(current.symbols);
UnlinkContainerFromSymbols(newSymbols, container);
UnlinkContainerFromSymbols(containers, newSymbols, container);
const index = container.parent.children.indexOf(container);
if (index > -1) {
const index = container.parent.children.indexOf(container.properties.id);
const success = containers.delete(container.properties.id);
if (index > -1 && success) {
container.parent.children.splice(index, 1);
} else {
throw new Error('[DeleteContainer] Could not find container among parent\'s children');
}
ApplyBehaviorsOnSiblings(container, current.symbols);
ApplyBehaviorsOnSiblings(containers, container, current.symbols);
// Select the previous container
// or select the one above
const selectedContainerId = GetSelectedContainerOnDelete(
mainContainerClone,
containers,
current.selectedContainerId,
container.parent,
index
@ -93,7 +96,8 @@ export function DeleteContainer(
history.push({
lastAction: `Delete ${containerId}`,
mainContainer: mainContainerClone,
mainContainer: current.mainContainer,
containers,
selectedContainerId,
typeCounters: Object.assign({}, current.typeCounters),
symbols: newSymbols,
@ -115,16 +119,15 @@ export function DeleteContainer(
* @returns {IContainerModel} Next selected container
*/
function GetSelectedContainerOnDelete(
mainContainerClone: IContainerModel,
containers: Map<string, IContainerModel>,
selectedContainerId: string,
parent: IContainerModel,
index: number
): string {
const newSelectedContainer = FindContainerById(mainContainerClone, selectedContainerId) ??
const newSelectedContainerId = FindContainerById(containers, selectedContainerId)?.properties.id ??
parent.children.at(index) ??
parent.children.at(index - 1) ??
parent;
const newSelectedContainerId = newSelectedContainer.properties.id;
parent.properties.id;
return newSelectedContainerId;
}
@ -134,8 +137,12 @@ function GetSelectedContainerOnDelete(
* @param symbols Symbols to update
* @param container Container to unlink
*/
function UnlinkContainerFromSymbols(symbols: Map<string, ISymbolModel>, container: IContainerModel): void {
const it = MakeDFSIterator(container);
function UnlinkContainerFromSymbols(
containers: Map<string, IContainerModel>,
symbols: Map<string, ISymbolModel>,
container: IContainerModel
): void {
const it = MakeDFSIterator(container, containers);
for (const child of it) {
const symbol = symbols.get(child.properties.linkedSymbolId);
if (symbol === undefined) {
@ -167,18 +174,19 @@ export function OnPropertyChange(
throw new Error('[OnPropertyChange] Property was changed before selecting a Container');
}
const mainContainerClone: IContainerModel = structuredClone(current.mainContainer);
const container: ContainerModel | undefined = FindContainerById(mainContainerClone, selected.properties.id);
const containers = structuredClone(current.containers);
const container: ContainerModel | undefined = FindContainerById(containers, selected.properties.id);
if (container === null || container === undefined) {
throw new Error('[OnPropertyChange] Container model was not found among children of the main container!');
}
SetContainer(container, key, value, type, current.symbols);
SetContainer(containers, container, key, value, type, current.symbols);
history.push({
lastAction: `Change ${key} of ${container.properties.id}`,
mainContainer: mainContainerClone,
mainContainer: current.mainContainer,
containers,
selectedContainerId: container.properties.id,
typeCounters: Object.assign({}, current.typeCounters),
symbols: structuredClone(current.symbols),
@ -189,20 +197,30 @@ export function OnPropertyChange(
/**
* Sort the parent children by x
* @param parentClone The clone used for the sort
* @param parent The clone used for the sort
* @returns void
*/
export function SortChildren(parentClone: IContainerModel | null | undefined): void {
if (parentClone === null || parentClone === undefined) {
export function SortChildren(
containers: Map<string, IContainerModel>,
parent: IContainerModel | null | undefined
): void {
if (parent === null || parent === undefined) {
return;
}
const isHorizontal = parentClone.properties.orientation === Orientation.Horizontal;
const children = parentClone.children;
const isHorizontal = parent.properties.orientation === Orientation.Horizontal;
const children = parent.children;
if (!isHorizontal) {
parentClone.children.sort(
(a, b) => {
parent.children.sort(
(aId, bId) => {
const a = FindContainerById(containers, aId);
const b = FindContainerById(containers, bId);
if (a === undefined || b === undefined) {
return 0;
}
const yA = TransformY(a.properties.y, a.properties.height, a.properties.positionReference);
const yB = TransformY(b.properties.y, b.properties.height, b.properties.positionReference);
if (yA < yB) {
@ -212,16 +230,23 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v
return 1;
}
// xA = xB
const indexA = children.indexOf(a);
const indexB = children.indexOf(b);
const indexA = children.indexOf(aId);
const indexB = children.indexOf(bId);
return indexA - indexB;
}
);
return;
}
parentClone.children.sort(
(a, b) => {
parent.children.sort(
(aId, bId) => {
const a = FindContainerById(containers, aId);
const b = FindContainerById(containers, bId);
if (a === undefined || b === undefined) {
return 0;
}
const xA = TransformX(a.properties.x, a.properties.width, a.properties.positionReference);
const xB = TransformX(b.properties.x, b.properties.width, b.properties.positionReference);
if (xA < xB) {
@ -231,8 +256,8 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v
return 1;
}
// xA = xB
const indexA = children.indexOf(a);
const indexB = children.indexOf(b);
const indexA = children.indexOf(aId);
const indexB = children.indexOf(bId);
return indexA - indexB;
}
);
@ -247,6 +272,7 @@ export function SortChildren(parentClone: IContainerModel | null | undefined): v
* @param symbols Current list of symbols
*/
function SetContainer(
containers: Map<string, IContainerModel>,
container: ContainerModel,
key: string, value: string | number | boolean | number[],
type: PropertyType,
@ -267,13 +293,13 @@ function SetContainer(
);
// sort the children list by their position
SortChildren(container.parent);
SortChildren(containers, container.parent);
// Apply special behaviors: rigid, flex, symbol, anchor
ApplyBehaviors(container, symbols);
ApplyBehaviors(containers, container, symbols);
// Apply special behaviors on siblings
ApplyBehaviorsOnSiblingsChildren(container, symbols);
ApplyBehaviorsOnSiblingsChildren(containers, container, symbols);
}
/**

View file

@ -21,7 +21,7 @@ export function GetAction(
): (target: HTMLElement) => void {
return (target: HTMLElement) => {
const id = target.id;
const container = FindContainerById(currentState.mainContainer, id);
const container = FindContainerById(currentState.containers, id);
if (container === undefined) {
Swal.fire({
@ -33,7 +33,7 @@ export function GetAction(
}
/* eslint-disable @typescript-eslint/naming-convention */
const { prev, next } = GetPreviousAndNextSiblings(container);
const { prev, next } = GetPreviousAndNextSiblings(currentState.containers, container);
const request: ISetContainerListRequest = {
Container: container,
@ -59,18 +59,18 @@ export function GetAction(
};
}
function GetPreviousAndNextSiblings(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 next;
if (container.parent !== undefined &&
container.parent !== null &&
container.parent.children.length > 1) {
const index = container.parent.children.indexOf(container);
const index = container.parent.children.indexOf(container.properties.id);
if (index > 0) {
prev = container.parent.children[index - 1];
prev = FindContainerById(containers, container.parent.children[index - 1]);
}
if (index < container.parent.children.length - 1) {
next = container.parent.children[index + 1];
next = FindContainerById(containers, container.parent.children[index + 1]);
}
}
return { prev, next };
@ -144,7 +144,7 @@ function HandleReplace(
throw new Error('[ReplaceContainer] Cannot replace a container that does not exists');
}
const index = selectedContainer.parent.children.indexOf(selectedContainer);
const index = selectedContainer.parent.children.indexOf(selectedContainer.properties.id);
const newHistoryAfterDelete = DeleteContainer(
selectedContainer.properties.id,

View file

@ -36,6 +36,7 @@ export function AddSymbol(
history.push({
lastAction: `Add ${name}`,
mainContainer: structuredClone(current.mainContainer),
containers: structuredClone(current.containers),
selectedContainerId: current.selectedContainerId,
typeCounters: newCounters,
symbols: newSymbols,
@ -55,6 +56,7 @@ export function SelectSymbol(
history.push({
lastAction: `Select ${symbolId}`,
mainContainer: structuredClone(current.mainContainer),
containers: structuredClone(current.containers),
selectedContainerId: current.selectedContainerId,
typeCounters: structuredClone(current.typeCounters),
symbols: structuredClone(current.symbols),
@ -78,15 +80,15 @@ export function DeleteSymbol(
throw new Error(`[DeleteSymbol] Could not find symbol in the current state!: ${symbolId}`);
}
const newMainContainer = structuredClone(current.mainContainer);
UnlinkSymbolFromContainers(symbol, newMainContainer);
const containers = structuredClone(current.containers);
UnlinkSymbolFromContainers(containers, symbol);
newSymbols.delete(symbolId);
history.push({
lastAction: `Select ${symbolId}`,
mainContainer: newMainContainer,
mainContainer: current.mainContainer,
containers,
selectedContainerId: current.selectedContainerId,
typeCounters: structuredClone(current.typeCounters),
symbols: newSymbols,
@ -100,9 +102,9 @@ export function DeleteSymbol(
* @param symbol Symbol to remove
* @param root Container and its children to remove a symbol from
*/
function UnlinkSymbolFromContainers(symbol: ISymbolModel, root: IContainerModel): void {
function UnlinkSymbolFromContainers(containers: Map<string, IContainerModel>, symbol: ISymbolModel): void {
symbol.linkedContainers.forEach((containerId) => {
const container = FindContainerById(root, containerId);
const container = FindContainerById(containers, containerId);
if (container === undefined) {
return;
@ -140,22 +142,23 @@ export function OnPropertyChange(
(symbol as any)[key] = value;
const newMainContainer = structuredClone(current.mainContainer);
const containers = structuredClone(current.containers);
symbol.linkedContainers.forEach((containerId) => {
const container = FindContainerById(newMainContainer, containerId);
const container = FindContainerById(containers, containerId);
if (container === undefined) {
return;
}
ApplyBehaviors(container, newSymbols);
ApplyBehaviors(containers, container, newSymbols);
ApplyBehaviorsOnSiblingsChildren(container, newSymbols);
ApplyBehaviorsOnSiblingsChildren(containers, container, newSymbols);
});
history.push({
lastAction: `Change ${key} of ${symbol.id}`,
mainContainer: newMainContainer,
mainContainer: current.mainContainer,
containers,
selectedContainerId: current.selectedContainerId,
typeCounters: Object.assign({}, current.typeCounters),
symbols: newSymbols,