Implement associated container and default X, Y position for Symbol Related work items: #7537, #7540
526 lines
15 KiB
TypeScript
526 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, ContainerModel } 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
|
|
): ContainerModel {
|
|
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 = new ContainerModel(
|
|
defaultProperties,
|
|
[],
|
|
{
|
|
type
|
|
}
|
|
);
|
|
|
|
// 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: ContainerModel,
|
|
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: ContainerModel,
|
|
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: 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 = {
|
|
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 };
|
|
}
|