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:
parent
ec3fddec9d
commit
7f3f6a489a
43 changed files with 1127 additions and 453 deletions
|
@ -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)
|
||||
|
|
|
@ -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] };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
246
src/utils/simplex.ts
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue