/** * 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, parentClone: IContainerModel, index: number, typeIndex: number, newCounters: Record, symbols: Map, 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, newCounters: Record, symbols: Map ): 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, containerConfig: IAvailableContainer, newCounters: Record, symbols: Map ): void { const patternId = containerConfig.Pattern; if (patternId === undefined || patternId === null) { return; } const configs: Map = new Map(configuration.AvailableContainers.map(config => [config.Type, config])); const patterns: Map = 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, newCounters: Record, symbols: Map, configs: Map, patterns: Map ): 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, newCounters: Record, symbols: Map, configs: Map ): { 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, patterns: Map ): 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, 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 }; }