Merged PR 167: Add Flex and fix bugs (read desc)

Note: The branch name does not fit the new features.

- Implement Flex with simplex
- Enable rigid body by default (removed IsRigidBody property) <=== possibly a bad idea
- Sort children in add and update properties
- Implement MaxWidth
- Add more docs

Fixes :
- .env.production url
- Symbols: not blocking the linked container when the parent is moving
This commit is contained in:
Eric Nguyen 2022-08-25 13:28:32 +00:00
parent ec3fddec9d
commit 7f3f6a489a
43 changed files with 1127 additions and 453 deletions

View file

@ -68,7 +68,7 @@ export const DEFAULT_CONFIG: IConfiguration = {
AvailableContainers: [
{
Type: 'Container',
Width: 75,
MaxWidth: 200,
Height: 100,
Style: {
fillOpacity: 0,
@ -79,7 +79,7 @@ export const DEFAULT_CONFIG: IConfiguration = {
AvailableSymbols: [],
MainContainer: {
Type: 'Container',
Width: 2000,
Width: 800,
Height: 100,
Style: {
fillOpacity: 0,
@ -93,16 +93,19 @@ export const DEFAULT_CONFIG: IConfiguration = {
*/
export const DEFAULT_MAINCONTAINER_PROPS: IContainerProperties = {
id: 'main',
type: 'container',
parentId: '',
linkedSymbolId: '',
displayedText: 'main',
x: 0,
y: 0,
margin: {},
minWidth: 1,
maxWidth: Number.MAX_SAFE_INTEGER,
width: Number(DEFAULT_CONFIG.MainContainer.Width),
height: Number(DEFAULT_CONFIG.MainContainer.Height),
isRigidBody: false,
isAnchor: false,
isFlex: false,
XPositionReference: XPositionReference.Left,
style: {
stroke: 'black',
@ -126,20 +129,25 @@ export const GetDefaultContainerProps = (
parent: IContainerModel,
x: number,
y: number,
width: number,
height: number,
containerConfig: IAvailableContainer
): IContainerProperties => ({
id: `${type}-${typeCount}`,
type,
parentId: parent.properties.id,
linkedSymbolId: '',
displayedText: `${type}-${typeCount}`,
x,
y,
width: containerConfig.Width ?? containerConfig.MinWidth ?? parent.properties.width,
height: containerConfig.Height ?? parent.properties.height,
isRigidBody: false, // set this to true to replicate Florian's project
margin: containerConfig.Margin ?? {},
width,
height,
isAnchor: false,
isFlex: containerConfig.IsFlex ?? containerConfig.Width === undefined,
XPositionReference: containerConfig.XPositionReference ?? XPositionReference.Left,
minWidth: containerConfig.MinWidth ?? 1,
maxWidth: containerConfig.MaxWidth ?? Number.MAX_SAFE_INTEGER,
customSVG: containerConfig.CustomSVG,
style: structuredClone(containerConfig.Style),
userData: structuredClone(containerConfig.UserData)

View file

@ -14,7 +14,7 @@ export function * MakeIterator(root: IContainerModel): Generator<IContainerModel
for (let i = container.children.length - 1; i >= 0; i--) {
const child = container.children[i];
if (visited.has(child)) {
return;
continue;
}
visited.add(child);
queue.push(child);
@ -71,9 +71,20 @@ export function getDepth(parent: IContainerModel): number {
* @returns The absolute position of the container
*/
export function getAbsolutePosition(container: IContainerModel): [number, number] {
let x = container.properties.x;
let y = container.properties.y;
let current = container.parent;
const x = container.properties.x;
const y = container.properties.y;
return cancelParentTransform(container.parent, x, y);
}
/**
* Cancel the hierarchic transformations to the given x, y
* @param parent Parent of the container to remove its transform
* @param x value to be restored
* @param y value to be restored
* @returns x and y such that the transformations of the parent are cancelled
*/
export function cancelParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] {
let current = parent;
while (current != null) {
x += current.properties.x;
y += current.properties.y;
@ -82,6 +93,23 @@ export function getAbsolutePosition(container: IContainerModel): [number, number
return [x, y];
}
/**
* Cancel the hierarchic transformations to the given x, y
* @param parent Parent of the container to remove its transform
* @param x value to be restored
* @param y value to be restored
* @returns x and y such that the transformations of the parent are cancelled
*/
export function applyParentTransform(parent: IContainerModel | null, x: number, y: number): [number, number] {
let current = parent;
while (current != null) {
x -= current.properties.x;
y -= current.properties.y;
current = current.parent;
}
return [x, y];
}
export function findContainerById(root: IContainerModel, id: string): IContainerModel | undefined {
const it = MakeIterator(root);
for (const container of it) {
@ -91,3 +119,20 @@ export function findContainerById(root: IContainerModel, id: string): IContainer
}
return undefined;
}
export interface IPair<T> {
cur: T
next: T
}
export function * pairwise<T>(arr: T[]): Generator<IPair<T>, void, unknown> {
for (let i = 0; i < arr.length - 1; i++) {
yield { cur: arr[i], next: arr[i + 1] };
}
}
export function * reversePairwise<T>(arr: T[]): Generator<IPair<T>, void, unknown> {
for (let i = arr.length - 1; i > 0; i--) {
yield { cur: arr[i], next: arr[i - 1] };
}
}

View file

@ -1,5 +1,6 @@
import { findContainerById, MakeIterator } from './itertools';
import { IEditorState } from '../Interfaces/IEditorState';
import { IHistoryState } from '../Interfaces/IHistoryState';
/**
* Revive the Editor state
@ -14,31 +15,37 @@ export function Revive(editorState: IEditorState): void {
// restore the parents and the selected container
for (const state of history) {
if (state.MainContainer === null || state.MainContainer === undefined) {
continue;
}
state.Symbols = new Map(state.Symbols);
for (const symbol of state.Symbols.values()) {
symbol.linkedContainers = new Set(symbol.linkedContainers);
}
const it = MakeIterator(state.MainContainer);
for (const container of it) {
const parentId = container.properties.parentId;
if (parentId === null) {
container.parent = null;
continue;
}
const parent = findContainerById(state.MainContainer, parentId);
if (parent === undefined) {
continue;
}
container.parent = parent;
}
ReviveState(state);
}
}
export const ReviveState = (
state: IHistoryState
): void => {
if (state.MainContainer === null || state.MainContainer === undefined) {
return;
}
state.Symbols = new Map(state.Symbols);
for (const symbol of state.Symbols.values()) {
symbol.linkedContainers = new Set(symbol.linkedContainers);
}
const it = MakeIterator(state.MainContainer);
for (const container of it) {
const parentId = container.properties.parentId;
if (parentId === null) {
container.parent = null;
continue;
}
const parent = findContainerById(state.MainContainer, parentId);
if (parent === undefined) {
continue;
}
container.parent = parent;
}
};
export const getCircularReplacer = (): (key: any, value: object | Map<string, any> | null) => object | null | undefined => {
return (key: any, value: object | null) => {
if (key === 'parent') {

246
src/utils/simplex.ts Normal file
View file

@ -0,0 +1,246 @@
/**
* @module {Simplex} Apply the simplex algorithm
* https://www.imse.iastate.edu/files/2015/08/Explanation-of-Simplex-Method.docx
*/
/**
* Apply the simplex algorithms to the minimum widths
*
* Note: Some optimizations were made to improve performance in order to solve
* with max(minWidths). In point of fact most coefficient are equal to 1 or -1.
*
* Let the following format be the linear problem :
* x >= b are the minimum widths constraint
* sum(x) <= b is the maximum width constraint
* s are slack variables
* @param minWidths
* @param requiredMaxWidth
* @returns
*/
export function Simplex(minWidths: number[], requiredMaxWidth: number): number[] {
/// 1) standardized the equations
// add the min widths constraints
const maximumConstraints = minWidths.map(minWidth => minWidth * -1);
// add the max width constraint
maximumConstraints.push(requiredMaxWidth);
/// 2) Create the initial matrix
// get row length (nVariables + nConstraints + 1 (z) + 1 (b))
const nVariables = minWidths.length;
const nConstraints = maximumConstraints.length;
const rowlength = nVariables + nConstraints + 2;
const matrix = GetInitialMatrix(maximumConstraints, rowlength, nVariables);
/// Apply the algorithm
const finalMatrix = ApplyMainLoop(matrix, rowlength);
// 5) read the solutions
const solutions: number[] = GetSolutions(nVariables + nConstraints + 1, finalMatrix);
return solutions;
}
const MAX_TRIES = 10;
/**
* Specific to min widths algorithm
* Get the initial matrix from the maximum constraints
* and the number of variables
* @param maximumConstraints
* @param rowlength
* @param nVariables
* @returns
*/
function GetInitialMatrix(
maximumConstraints: number[],
rowlength: number,
nVariables: number
): number[][] {
const nConstraints = maximumConstraints.length;
const matrix = maximumConstraints.map((maximumConstraint, index) => {
const row: number[] = Array(rowlength).fill(0);
// insert the variable coefficient a of a*x
if (index <= nConstraints - 2) {
// insert the the variable coefficient of the minimum widths constraints (negative identity matrix)
row[index] = -1;
} else {
// insert the the variable coefficient of the maximum width constraint
row.fill(1, 0, nVariables);
}
// insert the slack variable coefficient b of b*s (identity matrix)
row[index + nVariables] = 1;
// insert the constraint coefficient (b)
row[rowlength - 1] = maximumConstraint;
return row;
});
// add objective function in the last row
const row: number[] = Array(rowlength).fill(0);
// insert z coefficient
row[rowlength - 2] = 1;
// insert variable coefficients
row.fill(-1, 0, nVariables);
matrix.push(row);
return matrix;
}
function getAllIndexes(arr: number[], val: number): number[] {
const indexes = []; let i = -1;
while ((i = arr.indexOf(val, i + 1)) !== -1) {
indexes.push(i);
}
return indexes;
}
/**
* Apply the main loop of the simplex algorithm and return the final matrix:
* - While the last row of the matrix has negative values :
* - 1) find the column with the smallest negative coefficient in the last row
* - 2) in that column, find the pivot by selecting the row with the smallest ratio
* such as ratio = constraint of last column / coefficient of the selected row of the selected column
* - 3) create the new matrix such as:
* - 4) the selected column must have 1 in the pivot and zeroes in the other rows
* - 5) in the selected rows other columns (other than the selected column)
* must be divided by that pivot: coef / pivot
* - 6) for the others cells, apply the pivot: new value = (-coefficient in the old col) * (coefficient in the new row) + old value
* - 7) if in the new matrix there are still negative values in the last row,
* redo the algorithm with the new matrix as the base matrix
* - 8) otherwise returns the basic variable such as
* a basic variable is defined by a single 1 and only zeroes in its column
* other variables are equal to zeroes
* @param oldMatrix
* @param rowlength
* @returns
*/
function ApplyMainLoop(oldMatrix: number[][], rowlength: number): number[][] {
let matrix = oldMatrix;
let tries = MAX_TRIES;
const indexesTried: Record<number, number> = {};
while (matrix[matrix.length - 1].some((v: number) => v < 0) && tries > 0) {
// 1) find the index with smallest coefficient (O(n)+)
const lastRow = matrix[matrix.length - 1];
const min = Math.min(...lastRow);
const indexes = getAllIndexes(lastRow, min);
// to avoid infinite loop try to select the least used selected index
const pivotColIndex = getLeastUsedIndex(indexes, indexesTried);
// record the usage of index by incrementing
indexesTried[pivotColIndex] = indexesTried[pivotColIndex] !== undefined ? indexesTried[pivotColIndex] + 1 : 1;
// 2) find the smallest non negative non null ratio bi/xij (O(m))
const ratios = [];
for (let i = 0; i <= matrix.length - 2; i++) {
const coefficient = matrix[i][pivotColIndex];
const constraint = matrix[i][rowlength - 1];
if (coefficient === 0) {
ratios.push(Infinity);
continue;
}
const ratio = constraint / coefficient;
if (ratio < 0) {
ratios.push(Infinity);
continue;
}
ratios.push(ratio);
}
const minRatio = Math.min(...ratios);
const pivotRowIndex = ratios.indexOf(minRatio); // i
/// Init the new matrix
const newMatrix = structuredClone(matrix);
const pivot = matrix[pivotRowIndex][pivotColIndex];
// 3) apply on the pivot row the inverse of the pivot
const newPivotRow = newMatrix[pivotRowIndex];
newPivotRow.forEach((coef, colIndex) => {
newPivotRow[colIndex] = coef / pivot;
});
// 4) update all values
newMatrix.forEach((row, rowIndex) => {
if (rowIndex === pivotRowIndex) {
return;
}
row.forEach((coef, colIndex) => {
if (colIndex === pivotColIndex) {
// set zeroes on pivot col
row[colIndex] = 0;
return;
}
// update value = old value + ((-old coef of pivot column) * (new coef of pivot row))
row[colIndex] = coef + (-matrix[rowIndex][pivotColIndex] * newMatrix[pivotRowIndex][colIndex]);
});
});
matrix = newMatrix;
tries--;
}
if (tries === 0) {
throw new Error('[Flex]Simplexe: Could not find a solution');
}
return matrix;
}
/**
* Get the solutions from the final matrix
*
* @param {number} nCols Number of solutions that you want to obtain
* @param {number[][]} finalMatrix Final matrix after the algorithm is applied
* @return {*} {number[]} A list of solutions of the final matrix
*/
function GetSolutions(nCols: number, finalMatrix: number[][]): number[] {
const solutions: number[] = Array(nCols).fill(0);
for (let i = 0; i < nCols; i++) {
const counts: Record<number, number> = {};
const col: number[] = [];
for (let j = 0; j < finalMatrix.length; j++) {
const row = finalMatrix[j];
counts[row[i]] = counts[row[i]] !== undefined ? counts[row[i]] + 1 : 1;
col.push(row[i]);
}
// a basic variable has a single 1 and only zeroes in the column
const nRows = finalMatrix.length;
const isBasic = counts[1] === 1 && counts[0] === (nRows - 1);
if (isBasic) {
const oneIndex = col.indexOf(1);
const row = finalMatrix[oneIndex];
solutions[i] = row[row.length - 1];
} else {
solutions[i] = 0;
}
}
return solutions;
}
/**
* Returns the least used index from the indexesTried
* @param indexes Indexes of all occurences
* @param indexesTried Record of indexes. Count the number of times the index was used.
* @returns The least used index
*/
function getLeastUsedIndex(indexes: number[], indexesTried: Record<number, number>): number {
let minUsed = Infinity;
let minIndex = -1;
for (const index of indexes) {
const occ = indexesTried[index];
if (occ === undefined) {
minIndex = index;
break;
}
if (occ < minUsed) {
minIndex = index;
minUsed = occ;
}
}
return minIndex;
}

View file

@ -1,5 +1,15 @@
import { XPositionReference } from '../Enums/XPositionReference';
// TODO: Big refactoring
/**
* TODO:
* All of these methods should have been
* inside ContainerModel class
* But because of serialization, the methods are lost.
* Rather than adding more functions to this class,
* it is better to fix serialization with the reviver.
*/
export function transformX(x: number, width: number, xPositionReference = XPositionReference.Left): number {
let transformedX = x;
if (xPositionReference === XPositionReference.Center) {
@ -19,3 +29,69 @@ export function restoreX(x: number, width: number, xPositionReference = XPositio
}
return transformedX;
}
export function ApplyMargin(
x: number,
y: number,
width: number,
height: number,
left?: number,
bottom?: number,
top?: number,
right?: number
): { x: number, y: number, width: number, height: number } {
left = left ?? 0;
right = right ?? 0;
bottom = bottom ?? 0;
top = top ?? 0;
x = ApplyXMargin(x, left);
y += top;
width = ApplyWidthMargin(width, left, right);
height -= (bottom + top);
return { x, y, width, height };
}
export function RemoveMargin(
x: number,
y: number,
width: number,
height: number,
left?: number,
bottom?: number,
top?: number,
right?: number
): { x: number, y: number, width: number, height: number } {
bottom = bottom ?? 0;
top = top ?? 0;
x = RemoveXMargin(x, left);
y -= top;
width = RemoveWidthMargin(width, left, right);
height += (bottom + top);
return { x, y, width, height };
}
export function ApplyXMargin(x: number, left?: number): number {
left = left ?? 0;
x += left;
return x;
}
export function RemoveXMargin(x: number, left?: number): number {
left = left ?? 0;
x -= left;
return x;
}
export function ApplyWidthMargin(width: number, left?: number, right?: number): number {
left = left ?? 0;
right = right ?? 0;
width -= (left + right);
return width;
}
export function RemoveWidthMargin(width: number, left?: number, right?: number): number {
left = left ?? 0;
right = right ?? 0;
width += (left + right);
return width;
}