svg-layout-designer-react/src/Components/Editor/Actions/AddContainer.ts
2023-02-17 09:34:48 +00:00

524 lines
15 KiB
TypeScript

/**
* This file is dedicated to the AddContainer
*/
import { AddMethod } from '../../../Enums/AddMethod';
import { IAvailableContainer } from '../../../Interfaces/IAvailableContainer';
import { IConfiguration } from '../../../Interfaces/IConfiguration';
import { IContainerModel } from '../../../Interfaces/IContainerModel';
import { IHistoryState } from '../../../Interfaces/IHistoryState';
import { IPattern, GetPattern, ContainerOrPattern } from '../../../Interfaces/IPattern';
import { ISymbolModel } from '../../../Interfaces/ISymbolModel';
import { Orientation } from '../../../Enums/Orientation';
import { GetDefaultContainerProps } from '../../../utils/default';
import { FindContainerById } from '../../../utils/itertools';
import { ApplyMargin, RestoreX, RestoreY } from '../../../utils/svg';
import { ApplyBehaviors, ApplyBehaviorsOnSiblingsChildren } from '../Behaviors/Behaviors';
import { GetCurrentHistory, UpdateCounters } from '../Editor';
import { SortChildren } from './ContainerOperations';
/**
* Add a new container to a selected container
* @param type The type of container
* @param configuration Configuration of the App
* @param fullHistory History of the editor
* @param historyCurrentStep Current step
* @returns void
*/
export function AddContainerToSelectedContainer(
type: string,
selected: IContainerModel,
configuration: IConfiguration,
fullHistory: IHistoryState[],
historyCurrentStep: number
): IHistoryState[] {
return AddContainer(
selected.children.length,
type,
selected.properties.id,
configuration,
fullHistory,
historyCurrentStep
);
}
/**
* Create and add a new container at `index` in children of parent of `parentId`
* @param index Index where to insert to the new container
* @param type Type of container
* @param parentId Parent in which to insert the new container
* @param configuration Configuration of the app
* @param fullHistory History of the editor
* @param historyCurrentStep Current step
* @returns void
*/
export function AddContainers(
index: number,
availableContainers: IAvailableContainer[],
parentId: string,
configuration: IConfiguration,
fullHistory: IHistoryState[],
historyCurrentStep: number
): {
history: IHistoryState[]
newContainers: IContainerModel[]
} {
const history = GetCurrentHistory(fullHistory, historyCurrentStep);
const current = history[history.length - 1];
const containers = structuredClone(current.containers);
// Find the parent in the clone
const parentClone: IContainerModel | undefined = FindContainerById(
containers, parentId
);
if (parentClone === null || parentClone === undefined) {
throw new Error('[AddContainer] Container model was not found among children of the main container!');
}
// Deep clone the counters
const newCounters = Object.assign({}, current.typeCounters);
// Iterate over the containers
const newContainers: IContainerModel[] = [];
availableContainers.forEach((availableContainer, typeIndex) => {
// Get the preset properties from the API
const newContainer = AddNewContainerToParent(
availableContainer,
configuration,
containers,
parentClone,
index,
typeIndex,
newCounters,
current.symbols
);
newContainers.push(newContainer);
});
// Update the state
const containersIds = newContainers.map(container => container.properties.id);
history.push({
lastAction: `Add [${containersIds.join(', ')}] in ${parentClone.properties.id}`,
mainContainer: current.mainContainer,
selectedContainerId: parentClone.properties.id,
containers,
typeCounters: newCounters,
symbols: structuredClone(current.symbols),
selectedSymbolId: current.selectedSymbolId
});
return {
history,
newContainers
};
}
function AddNewContainerToParent(
availableContainer: IAvailableContainer,
configuration: IConfiguration,
containers: Map<string, IContainerModel>,
parentClone: IContainerModel,
index: number,
typeIndex: number,
newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel>,
initChilds: boolean = true
): IContainerModel {
const type = availableContainer.Type;
const defaultConfig = configuration.AvailableContainers
.find(option => option.Type === type);
if (defaultConfig === undefined) {
throw new Error(`[AddContainer] Object type not found among default config. Found: ${type}`);
}
const containerConfig = Object.assign(structuredClone(defaultConfig), availableContainer);
// Default margin
const left: number = containerConfig.Margin?.left ?? 0;
const bottom: number = containerConfig.Margin?.bottom ?? 0;
const top: number = containerConfig.Margin?.top ?? 0;
const right: number = containerConfig.Margin?.right ?? 0;
// Default coordinates
let width = containerConfig.Width ?? containerConfig.MaxWidth ?? containerConfig.MinWidth ?? parentClone.properties.width;
let height = containerConfig.Height ?? containerConfig.MaxHeight ?? containerConfig.MinHeight ?? parentClone.properties.height;
let x = RestoreX(containerConfig.X ?? 0, width, containerConfig.PositionReference);
let y = RestoreY(containerConfig.Y ?? 0, height, containerConfig.PositionReference);
({ x, y, width, height } = ApplyMargin(x, y, width, height, left, bottom, top, right));
// Apply an add method (append or insert/replace)
({ 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);
const count = newCounters[type];
const defaultProperties = GetDefaultContainerProps(
type,
count,
parentClone,
x,
y,
width,
height,
containerConfig
);
// Create the container
const newContainer: IContainerModel = {
properties: defaultProperties,
children: [],
userData: {}
};
// 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.properties.id);
} else {
parentClone.children.splice(index, 0, newContainer.properties.id);
}
// Sort the parent children by x
SortChildren(containers, parentClone);
/// Handle behaviors here ///
// Apply the behaviors (flex, rigid, anchor)
ApplyBehaviors(containers, newContainer, symbols);
// Then, apply the behaviors on its siblings (mostly for flex)
ApplyBehaviorsOnSiblingsChildren(containers, newContainer, symbols);
// Initialize default children of the container
if (initChilds) {
if (containerConfig.DefaultChildType !== undefined) {
InitializeDefaultChild(
newContainer,
configuration,
containerConfig,
containers,
newCounters,
symbols
);
} else {
InitializeChildrenWithPattern(
newContainer,
configuration,
containers,
containerConfig,
newCounters,
symbols
);
}
}
return newContainer;
}
/**
* Create and add a new container at `index` in children of parent of `parentId`
* @param index Index where to insert to the new container
* @param type Type of container
* @param parentId Parent in which to insert the new container
* @param configuration Configuration of the app
* @param fullHistory History of the editor
* @param historyCurrentStep Current step
* @param setHistory State setter of History
* @param setHistoryCurrentStep State setter of the current step
* @returns new history
*/
export function AddContainer(
index: number,
type: string,
parentId: string,
configuration: IConfiguration,
fullHistory: IHistoryState[],
historyCurrentStep: number
): IHistoryState[] {
// just call AddContainers with an array on a single element
const { history } = AddContainers(
index,
// eslint-disable-next-line @typescript-eslint/naming-convention
[{ Type: type }],
parentId,
configuration,
fullHistory,
historyCurrentStep
);
return history;
}
/**
* Initialize the children of the container
* @param configuration Current configuration of the app
* @param containerConfig The new container config used to read the default child type
* @param newContainer The new container to initialize its default children
* @param newCounters Type counter used for unique ids
* @returns
*/
function InitializeDefaultChild(
newContainer: IContainerModel,
configuration: IConfiguration,
containerConfig: IAvailableContainer,
containers: Map<string, IContainerModel>,
newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel>
): void {
if (containerConfig.DefaultChildType === undefined) {
return;
}
const currentConfig = configuration.AvailableContainers
.find(option => option.Type === containerConfig.DefaultChildType);
const parent = newContainer;
if (currentConfig === undefined) {
return;
}
AddNewContainerToParent(
currentConfig,
configuration,
containers,
parent,
0, 0,
newCounters,
symbols
);
}
function InitializeChildrenWithPattern(
newContainer: IContainerModel,
configuration: IConfiguration,
containers: Map<string, IContainerModel>,
containerConfig: IAvailableContainer,
newCounters: Record<string, number>,
symbols: Map<string, ISymbolModel>
): void {
const patternId = containerConfig.Pattern;
if (patternId === undefined || patternId === null) {
return;
}
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 containerOrPattern = GetPattern(patternId, configs, patterns);
if (containerOrPattern === undefined) {
console.warn(`[InitializeChildrenWithPattern] PatternId ${patternId} was neither found as Pattern nor as IAvailableContainer`);
return;
}
// 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: IContainerModel,
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 = {
containerOrPattern: rootPattern,
parent: newContainer
};
const queue: Node[] = [rootNode];
while (queue.length > 0) {
let levelSize = queue.length;
const maxLevelSize = levelSize - 1;
while (levelSize-- !== 0) {
const node = queue.shift() as Node;
const newParent = AddContainerInLevel(node, maxLevelSize, levelSize, configuration, containers, newCounters, symbols, configs);
if (newParent === undefined) {
// node.pattern is not a IPattern, there is no children to iterate
continue;
}
for (let i = 0; i <= newParent.pattern.children.length - 1; i++) {
const nextNode = GetNextNode(newParent.parent, newParent.pattern, i, configs, patterns);
if (nextNode === undefined) {
continue;
}
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,
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 {
containerOrPattern: ContainerOrPattern
parent: IContainerModel
}
/**
* Returns a new offset by applying an Add method (append, insert etc.)
* See AddMethod
* @param index Index of the container
* @param containerConfig Configuration of a container
* @param parent Parent container
* @param x Additionnal offset
* @returns New offset
*/
function ApplyAddMethod(
containers: Map<string, IContainerModel>,
index: number,
containerConfig: IAvailableContainer,
parent: IContainerModel,
x: number,
y: number
): { x: number, y: number } {
if (index > 0 && (
containerConfig.AddMethod === undefined ||
containerConfig.AddMethod === null ||
containerConfig.AddMethod === AddMethod.Append
)) {
// Append method (default)
const lastChildId: string = parent.children[index - 1];
const lastChild = FindContainerById(containers, lastChildId);
if (lastChild !== undefined) {
const isHorizontal = parent.properties.orientation === Orientation.Horizontal;
if (isHorizontal) {
x += lastChild.properties.x + lastChild.properties.width;
} else {
y += lastChild.properties.y + lastChild.properties.height;
}
}
}
return { x, y };
}