diff --git a/src/utils/graph/common.js b/src/utils/graph/common.js index 2927049a02..2a01c6dc63 100644 --- a/src/utils/graph/common.js +++ b/src/utils/graph/common.js @@ -29,30 +29,6 @@ export const snap = (value, unit) => Math.round(value / unit) * unit; */ export const distance1d = (a, b) => Math.abs(a - b); -/** - * Returns the value `a - b` - * @param {number} a The first number - * @param {number} b The second number - * @returns {number} The result - */ -export const subtract = (a, b) => a - b; - -/** - * Returns `true` if `a === b` otherwise `false` - * @param {number} a The first value - * @param {number} b The second value - * @returns {boolean} The result - */ -export const equalTo = (a, b) => a === b; - -/** - * Returns `true` if `a >= b` otherwise `false` - * @param {number} a The first number - * @param {number} b The second number - * @returns {boolean} The result - */ -export const greaterOrEqual = (a, b) => a >= b; - /** * Returns the angle in radians between the points a and b relative to the X-axis about the origin * @param {object} a The first point diff --git a/src/utils/graph/constraints.js b/src/utils/graph/constraints.js index 9c358a8232..545851a380 100644 --- a/src/utils/graph/constraints.js +++ b/src/utils/graph/constraints.js @@ -1,4 +1,4 @@ -import { distance1d, greaterOrEqual, equalTo, subtract } from './common'; +import { Constraint, Operator, Strength } from 'kiwi.js'; /** * Constraint base definitions. @@ -11,14 +11,29 @@ import { distance1d, greaterOrEqual, equalTo, subtract } from './common'; */ export const rowConstraint = { property: 'y', - difference: subtract, - distance: distance1d, - operator: greaterOrEqual, - target: (a, b, co, constants) => constants.spaceY, - strength: () => 1, - weightA: () => 0, - weightB: () => 1, - required: true, + + solve: (constraint, constants) => { + const { a, b } = constraint; + const difference = a.y - b.y; + const target = constants.spaceY; + + if (difference >= target) { + return; + } + + const resolve = difference - target; + a.y -= 0.5 * resolve; + b.y += 0.5 * resolve; + }, + + strict: (constraint, constants, variableA, variableB) => { + return new Constraint( + variableA.minus(variableB), + Operator.Ge, + constants.spaceY, + Strength.required + ); + }, }; /** @@ -26,14 +41,29 @@ export const rowConstraint = { */ export const layerConstraint = { property: 'y', - difference: subtract, - distance: distance1d, - operator: greaterOrEqual, - target: (a, b, co, constants) => constants.layerSpace, - strength: () => 1, - weightA: () => 0, - weightB: () => 1, - required: false, + + solve: (constraint, constants) => { + const { a, b } = constraint; + const difference = a.y - b.y; + const target = constants.layerSpace; + + if (difference >= target) { + return; + } + + const resolve = difference - target; + a.y -= 0.5 * resolve; + b.y += 0.5 * resolve; + }, + + strict: (constraint, constants, variableA, variableB) => { + return new Constraint( + variableA.minus(variableB), + Operator.Ge, + constants.layerSpace, + Strength.required + ); + }, }; /** @@ -41,16 +71,31 @@ export const layerConstraint = { */ export const parallelConstraint = { property: 'x', - difference: subtract, - distance: distance1d, - operator: equalTo, - target: () => 0, - // Lower degree nodes can be moved more freely than higher - strength: (co) => - 1 / Math.max(1, 0.5 * (co.a.targets.length + co.b.sources.length)), - weightA: () => 0.5, - weightB: () => 0.5, - required: false, + + solve: (constraint) => { + const { a, b } = constraint; + const difference = a.x - b.x; + + if (difference === 0) { + return; + } + + const strength = + 1 / Math.max(1, 0.5 * (a.targets.length + b.sources.length)); + + const resolve = strength * difference; + a.x -= 0.5 * resolve; + b.x += 0.5 * resolve; + }, + + strict: (constraint, constants, variableA, variableB) => { + return new Constraint( + variableA.minus(variableB), + Operator.Eq, + 0, + Strength.strong + ); + }, }; /** @@ -58,49 +103,53 @@ export const parallelConstraint = { */ export const crossingConstraint = { property: 'x', - difference: subtract, - distance: distance1d, - operator: (distance, target, difference) => - target >= 0 ? difference >= target : difference <= target, - target: (a, b, co, constants) => { - // Find the minimal target position that separates both nodes - const sourceDelta = co.edgeA.sourceNode.x - co.edgeB.sourceNode.x; - const targetDelta = co.edgeA.targetNode.x - co.edgeB.targetNode.x; - return sourceDelta + targetDelta < 0 ? -constants.basisX : constants.basisX; + + solve: (constraint, constants) => { + const { a, b, edgeA, edgeB } = constraint; + const difference = a.x - b.x; + const sourceDelta = edgeA.sourceNode.x - edgeB.sourceNode.x; + const targetDelta = edgeA.targetNode.x - edgeB.targetNode.x; + const target = + sourceDelta + targetDelta < 0 ? -constants.basisX : constants.basisX; + + if (target >= 0 ? difference >= target : difference <= target) { + return; + } + + const strength = 1 / constants.basisX; + + const resolve = strength * (difference - target); + a.x -= 0.5 * resolve; + b.x += 0.5 * resolve; }, - strength: (co, constants) => 1 / constants.basisX, - weightA: () => 0.5, - weightB: () => 0.5, - required: false, }; /** - * Layout constraint in X for minimum node separation (loose) + * Layout constraint in X for minimum node separation */ export const separationConstraint = { property: 'x', - difference: subtract, - distance: distance1d, - operator: (distance, target, difference) => difference <= target, - target: (ax, bx, co, constants) => - -constants.spaceX - co.a.width * 0.5 - co.b.width * 0.5, - strength: () => 1, - weightA: () => 0.5, - weightB: () => 0.5, - required: false, -}; -/** - * Layout constraint in X for minimum node separation (strict) - */ -export const separationStrictConstraint = { - property: 'x', - difference: subtract, - distance: distance1d, - operator: greaterOrEqual, - target: (ax, bx, co) => co.separation, - strength: () => 1, - weightA: () => 0, - weightB: () => 1, - required: true, + solve: (constraint) => { + const { a, b } = constraint; + const difference = b.x - a.x; + const target = constraint.separation; + + if (difference >= target) { + return; + } + + const resolve = difference - target; + a.x += 0.5 * resolve; + b.x -= 0.5 * resolve; + }, + + strict: (constraint, constants, variableA, variableB) => { + return new Constraint( + variableB.minus(variableA), + Operator.Ge, + constraint.separation, + Strength.required + ); + }, }; diff --git a/src/utils/graph/graph.test.js b/src/utils/graph/graph.test.js index 307ef84e48..7f23a568af 100644 --- a/src/utils/graph/graph.test.js +++ b/src/utils/graph/graph.test.js @@ -2,9 +2,9 @@ import { mockState } from '../state.mock'; import { getVisibleNodes } from '../../selectors/nodes'; import { getVisibleEdges } from '../../selectors/edges'; import { getVisibleLayerIDs } from '../../selectors/disabled'; +import { Constraint, Operator, Strength } from 'kiwi.js'; import { graph } from './graph'; import { solve } from './solver'; -import { greaterOrEqual, equalTo, subtract } from './common'; import { clamp, @@ -291,99 +291,104 @@ describe('commmon', () => { }); describe('solver', () => { - it('equalTo returns true if a === b otherwise false', () => { - expect(equalTo(0, 0)).toEqual(true); - expect(equalTo(1, 0)).toEqual(false); - }); - - it('greaterOrEqual returns true if a >= b otherwise false', () => { - expect(greaterOrEqual(-1, 0)).toEqual(false); - expect(greaterOrEqual(0, 0)).toEqual(true); - expect(greaterOrEqual(1, 0)).toEqual(true); - }); - - it('subtract returns the value a - b', () => { - expect(subtract(1, 2)).toEqual(-1); - expect(subtract(2, 1)).toEqual(1); - }); - it('solve finds a valid solution to given constraints (loose)', () => { const testA = { id: 0, x: 0, y: 0 }; const testB = { id: 1, x: 0, y: 0 }; const testC = { id: 2, x: 0, y: 0 }; - const constraintBase = { - difference: subtract, - distance: distance1d, - strength: () => 1, - weightA: () => 0.5, - weightB: () => 0.5, - required: false, + const solveEqConstraint = (constraint) => { + const { + a, + b, + target, + base: { property }, + } = constraint; + const difference = a[property] - b[property]; + + if (difference === target) { + return; + } + + const resolve = difference - target; + a[property] -= 0.5 * resolve; + b[property] += 0.5 * resolve; + }; + + const solveGeConstraint = (constraint) => { + const { + a, + b, + target, + base: { property }, + } = constraint; + const difference = a[property] - b[property]; + + if (difference >= target) { + return; + } + + const resolve = difference - target; + a[property] -= 0.5 * resolve; + b[property] += 0.5 * resolve; }; const constraintXA = { a: testA, b: testB, + target: 5, base: { - ...constraintBase, + solve: solveEqConstraint, property: 'x', - operator: equalTo, - target: () => 5, }, }; const constraintXB = { a: testB, b: testC, + target: 8, base: { - ...constraintBase, + solve: solveGeConstraint, property: 'x', - operator: greaterOrEqual, - target: () => 8, }, }; const constraintXC = { a: testA, b: testC, + target: 20, base: { - ...constraintBase, + solve: solveGeConstraint, property: 'x', - operator: greaterOrEqual, - target: () => 20, }, }; const constraintYA = { a: testA, b: testC, + target: 5, base: { - ...constraintBase, + solve: solveEqConstraint, property: 'y', - operator: equalTo, - target: () => 5, }, }; const constraintYB = { a: testB, b: testC, + target: 1, base: { - ...constraintBase, + solve: solveGeConstraint, property: 'y', - operator: greaterOrEqual, - target: () => 1, }, }; const constraintYC = { a: testB, b: testA, + target: 100, base: { - ...constraintBase, + solve: solveEqConstraint, property: 'y', - operator: equalTo, - target: () => 100, }, }; @@ -415,78 +420,91 @@ describe('solver', () => { const testB = { id: 1, x: 0, y: 0 }; const testC = { id: 2, x: 0, y: 0 }; - const constraintBase = { - difference: subtract, - distance: distance1d, - strength: () => 1, - weightA: () => 0.5, - weightB: () => 0.5, - required: true, + const strictEqConstraint = ( + constraint, + constants, + variableA, + variableB + ) => { + return new Constraint( + variableA.minus(variableB), + Operator.Eq, + constraint.target, + Strength.required + ); + }; + + const strictGeConstraint = ( + constraint, + constants, + variableA, + variableB + ) => { + return new Constraint( + variableA.minus(variableB), + Operator.Ge, + constraint.target, + Strength.required + ); }; const constraintXA = { a: testA, b: testB, + target: 5, base: { - ...constraintBase, + strict: strictEqConstraint, property: 'x', - operator: equalTo, - target: () => 5, }, }; const constraintXB = { a: testB, b: testC, + target: 8, base: { - ...constraintBase, + strict: strictGeConstraint, property: 'x', - operator: greaterOrEqual, - target: () => 8, }, }; const constraintXC = { a: testA, b: testC, + target: 20, base: { - ...constraintBase, + strict: strictGeConstraint, property: 'x', - operator: greaterOrEqual, - target: () => 20, }, }; const constraintYA = { a: testA, b: testC, + target: 5, base: { - ...constraintBase, + strict: strictEqConstraint, property: 'y', - operator: equalTo, - target: () => 5, }, }; const constraintYB = { a: testB, b: testC, + target: 1, base: { - ...constraintBase, + strict: strictGeConstraint, property: 'y', - operator: greaterOrEqual, - target: () => 1, }, }; const constraintYC = { a: testB, b: testA, + target: 100, base: { - ...constraintBase, + strict: strictEqConstraint, property: 'y', - operator: equalTo, - target: () => 100, }, }; diff --git a/src/utils/graph/layout.js b/src/utils/graph/layout.js index 09660c8c53..204e03a4c8 100644 --- a/src/utils/graph/layout.js +++ b/src/utils/graph/layout.js @@ -6,7 +6,6 @@ import { parallelConstraint, crossingConstraint, separationConstraint, - separationStrictConstraint, } from './constraints'; /** @@ -34,20 +33,13 @@ export const layout = ({ layerSpaceY, iterations, }) => { - const layerConstraints = []; - const crossingConstraints = []; - const parallelConstraints = []; - const parallelSingleConstraints = []; - const parallelDoubleConstraints = []; - const separationConstraints = []; - const separationStrictConstraints = []; - + // Set initial positions for nodes for (const node of nodes) { node.x = 0; node.y = 0; } - // Constraint constants passed to solver + // Constants passed to solver const constants = { spaceX, spaceY, @@ -55,58 +47,124 @@ export const layout = ({ layerSpace: (spaceY + layerSpaceY) * 0.5, }; - // Constraints in Y formed by the edges of the graph - const rowConstraints = edges.map((edge) => ({ + // Constraints to separate nodes into rows + const rowConstraints = createRowConstraints(edges); + + // Constraints to separate nodes into layers + const layerConstraints = createLayerConstraints(nodes, layers); + + // Find the node positions given these constraints + solve([...rowConstraints, ...layerConstraints], constants, 1, true); + + // Find the rows using the node positions after solving + const rows = groupByRow(nodes); + + // Constraints to avoid edges crossing + const crossingConstraints = createCrossingConstraints(edges); + + // Constraints to maintain parallel vertical edges + const { + parallelConstraints, + parallelSingleConstraints, + parallelDoubleConstraints, + } = createParallelConstraints(edges); + + // Constraints to maintain a minimum horizontal node spacing + const separationConstraints = createSeparationConstraints(rows); + + // Solve these constraints using multiple iterations + for (let i = 0; i < iterations; i += 1) { + // Solve main constraints + solve(crossingConstraints, constants, 1); + solve(parallelConstraints, constants, 1); + + // Further improve special cases with more effort + solve(parallelSingleConstraints, constants, iterations * 0.5); + solve(parallelDoubleConstraints, constants, iterations * 0.5); + + // Update and solve separation constraints given the updated positions + updateSeparationConstraints(separationConstraints, rows, spaceX); + solve(separationConstraints, constants, iterations * 0.5); + } + + // Update separation constraints but ensure spacing is exact + updateSeparationConstraints(separationConstraints, rows, spaceX, true); + + // Find the final node positions given these strict constraints + solve([...separationConstraints, ...parallelConstraints], constants, 1, true); + + // Adjust vertical spacing between rows for legibility + expandDenseRows(edges, rows, spaceY); +}; + +/** + * Creates row constraints for the given edges. + * @param {array} edges The input edges + * @returns {array} The constraints + */ +const createRowConstraints = (edges) => + edges.map((edge) => ({ base: rowConstraint, a: edge.targetNode, b: edge.sourceNode, })); - // Constraints in Y separating nodes into layers if specified - if (layers) { - const layerNames = Object.values(layers); - let layerNodes = nodes.filter( - (node) => node.nearestLayer === layerNames[0] - ); - - // For each defined layer - for (let i = 0; i < layerNames.length - 1; i += 1) { - const layer = layerNames[i]; - const nextLayer = layerNames[i + 1]; - const nextLayerNodes = nodes.filter( - (node) => node.nearestLayer === nextLayer - ); - - // Create a temporary intermediary 'node' - const layerNode = { id: layer, x: 0, y: 0 }; - - // Constraints in Y for each node such that node.y <= layerNode.y - spaceY - for (const node of layerNodes) { - layerConstraints.push({ - base: layerConstraint, - a: layerNode, - b: node, - }); - } +/** + * Creates layer constraints for the given nodes and layers. + * @param {array} nodes The input nodes + * @param {array=} layers The input layers if any + * @returns {array} The constraints + */ +const createLayerConstraints = (nodes, layers) => { + const layerConstraints = []; - // Constraints in Y for each node on the next layer such that node.y >= layerNode.y - for (const node of nextLayerNodes) { - layerConstraints.push({ - base: layerConstraint, - a: node, - b: layerNode, - }); - } + // Early out if no layers defined + if (!layers) { + return layerConstraints; + } + + // Group the nodes for each layer + const layerGroups = layers.map((name) => + nodes.filter((node) => node.nearestLayer === name) + ); + + // For each layer of nodes + for (let i = 0; i < layerGroups.length - 1; i += 1) { + const layerNodes = layerGroups[i]; + const nextLayerNodes = layerGroups[i + 1]; - layerNodes = nextLayerNodes; + // Create a temporary intermediary node for the layer + const intermediary = { id: `layer-${i}`, x: 0, y: 0 }; + + // Constrain each node in the layer to above the intermediary + for (const node of layerNodes) { + layerConstraints.push({ + base: layerConstraint, + a: intermediary, + b: node, + }); + } + + // Constrain each node in the next layer to below the intermediary + for (const node of nextLayerNodes) { + layerConstraints.push({ + base: layerConstraint, + a: node, + b: intermediary, + }); } } - // Find the positions of each node in Y given the constraints exactly - solve([...rowConstraints, ...layerConstraints], constants, 1, true); + return layerConstraints; +}; - // Find the rows formed by the nodes - const rows = groupByRow(nodes); +/** + * Creates crossing constraints for the given edges. + * @param {array} edges The input edges + * @returns {array} The constraints + */ +const createCrossingConstraints = (edges) => { + const crossingConstraints = []; // For every pair of edges for (let i = 0; i < edges.length; i += 1) { @@ -139,8 +197,25 @@ export const layout = ({ } } - // Constraints in X to minimise edge length thereby prioritising straight parallel edges in Y + return crossingConstraints; +}; + +/** + * Creates parallel constraints for the given edges. + * Returns object with additional arrays that identify these special cases: + * - edges connected to single-degree nodes at either end + * - edges connected to single-degree nodes at both ends + * @param {array} edges The input edges + * @returns {object} An object containing the constraints + */ +const createParallelConstraints = (edges) => { + const parallelConstraints = []; + const parallelSingleConstraints = []; + const parallelDoubleConstraints = []; + + // For each edge for (const edge of edges) { + // Constraint to keep it vertical and therefore parallel const constraint = { base: parallelConstraint, a: edge.sourceNode, @@ -149,6 +224,7 @@ export const layout = ({ parallelConstraints.push(constraint); + // Identify special cases const sourceHasOneTarget = edge.sourceNode.targets.length === 1; const targetHasOneSource = edge.targetNode.sources.length === 1; @@ -157,89 +233,92 @@ export const layout = ({ parallelSingleConstraints.push(constraint); } - // Collect edges connected to single-degree at both ends + // Collect edges connected to single-degree nodes at both ends if (sourceHasOneTarget && targetHasOneSource) { parallelDoubleConstraints.push(constraint); } } - // Solving loop for constraints in X - const halfIterations = Math.ceil(iterations * 0.5); - - for (let i = 0; i < iterations; i += 1) { - // Minimise crossing - solve(crossingConstraints, constants, 1); - - // Minimise edge length - solve(parallelConstraints, constants, 1); - - // Minimise edge length specifically for low-degree edges more strongly - solve(parallelSingleConstraints, constants, halfIterations); - solve(parallelDoubleConstraints, constants, halfIterations); - - // Clear separation constraints from previous iteration - separationConstraints.length = 0; - - // For each row - for (let l = 0; l < rows.length; l += 1) { - const rowNodes = rows[l]; + return { + parallelConstraints, + parallelSingleConstraints, + parallelDoubleConstraints, + }; +}; - // Sort rows in order of X position. Break ties with ids for stability - rowNodes.sort((a, b) => compare(a.x, b.x, a.id, b.id)); +/** + * Creates horizontal separation constraints for the given rows. + * @param {array} rows The rows containing nodes + * @returns {array} The constraints + */ +const createSeparationConstraints = (rows) => { + const separationConstraints = []; - // Constraints in X to maintain minimum node separation - for (let j = 0; j < rowNodes.length - 1; j += 1) { - separationConstraints.push({ - base: separationConstraint, - a: rowNodes[j], - b: rowNodes[j + 1], - }); - } + // Constraints to maintain horizontal node separation + for (const rowNodes of rows) { + for (let j = 0; j < rowNodes.length - 1; j += 1) { + separationConstraints.push({ + base: separationConstraint, + a: null, + b: null, + }); } - - // Minimise node separation overlap - solve(separationConstraints, constants, halfIterations); } - // For each row already sorted in X + return separationConstraints; +}; + +/** + * Updates horizontal separation constraints for the given rows. + * @param {array} separationConstraints The constraints to update + * @param {array} rows The rows containing nodes + * @param {number} spaceX The desired separation in X + * @returns {array} The constraints + */ +const updateSeparationConstraints = ( + separationConstraints, + rows, + spaceX, + snapped = false +) => { + let k = 0; + + // For each row for (let l = 0; l < rows.length; l += 1) { const rowNodes = rows[l]; - // For each node on the row - for (let i = 0; i < rowNodes.length - 1; i += 1) { - // Find the current node separation - const separation = (rowNodes[i + 1].x - rowNodes[i].x) * 0.8; + // Sort rows horizontally, breaks ties with ids for stability + rowNodes.sort((a, b) => compare(a.x, b.x, a.id, b.id)); + + // Update constraints given updated row order + for (let j = 0; j < rowNodes.length - 1; j += 1) { + const constraint = separationConstraints[k]; - // Find the minimal required separation + // Update the constraint objects in order + constraint.a = rowNodes[j]; + constraint.b = rowNodes[j + 1]; + + // Find the minimal required horizontal separation const minSeparation = - rowNodes[i].width * 0.5 + spaceX + rowNodes[i + 1].width * 0.5; - - // Snap the separation to a unit amount - const targetSeparation = Math.max( - snap(separation, spaceX), - minSeparation - ); - - // Constraints in X to maintain target separation - separationStrictConstraints.push({ - base: separationStrictConstraint, - a: rowNodes[i + 1], - b: rowNodes[i], - separation: targetSeparation, - }); + spaceX + constraint.a.width * 0.5 + constraint.b.width * 0.5; + + if (!snapped) { + // Use the minimal separation + constraint.separation = minSeparation; + } else { + // Find the current node horizontal separation + const separation = constraint.b.x - constraint.a.x; + + // Snap the current horizontal separation to a unit amount + constraint.separation = Math.max( + snap(separation * 0.8, spaceX), + minSeparation + ); + } + + k += 1; } } - - // Find final positions of each node in X under given constraints exactly - solve( - [...separationStrictConstraints, ...parallelConstraints], - constants, - 1, - true - ); - - // Add additional spacing in Y for rows with many crossing edges - expandDenseRows(edges, rows, spaceY); }; /** @@ -253,9 +332,11 @@ const expandDenseRows = (edges, rows, spaceY) => { const densities = rowDensity(edges); let currentOffsetY = 0; - // Add spacing based on density, snapped to a grid to improve vertical rhythm + // Add spacing based on density for (let i = 0; i < densities.length; i += 1) { const density = densities[i]; + + // Snap to improve vertical rhythm const offsetY = snap(density * spaceY, Math.round(spaceY * 0.25)); currentOffsetY += offsetY; diff --git a/src/utils/graph/solver.js b/src/utils/graph/solver.js index d987a6278e..718f9d4af6 100644 --- a/src/utils/graph/solver.js +++ b/src/utils/graph/solver.js @@ -7,31 +7,7 @@ * # The full license is in the file COPYING.txt, distributed with this software. * #------------------------------------------------------------------------------ **/ -import * as kiwi from 'kiwi.js'; - -import { equalTo, greaterOrEqual } from './common'; - -/** - * Combines the given object's id and key to create a new key - * @param {number} obj An object with `id` property - * @param {number} key An identifier string - * @returns {string} The combined key - */ -const key = (obj, key) => { - if (typeof obj.id === 'undefined') - throw new Error(`Object is missing property 'id' required for key.`); - return obj.id + '_' + key; -}; - -/** - * Given an operator function, returns the equivalent kiwi.js operator if defined - * @param {function} operator The operator function - * @returns {object|undefined} The kiwi.js operator - */ -export const toStrictOperator = (operator) => { - if (operator === equalTo) return kiwi.Operator.Eq; - if (operator === greaterOrEqual) return kiwi.Operator.Ge; -}; +import { Solver, Variable } from 'kiwi.js'; /** * Applies the given constraints to the objects in-place. @@ -41,13 +17,9 @@ export const toStrictOperator = (operator) => { * @param {object} constraint.a The first object to constrain * @param {object} constraint.b The second object to constrain * @param {string} constraint.base.property The property name on `a` and `b` to constrain - * @param {boolean} constraint.base.required Whether the constraint must be satisfied during strict solving - * @param {function} constraint.base.difference A signed difference function given `a` and `b` - * @param {function} constraint.base.distance An absolute distance function given `a` and `b` - * @param {function} constraint.base.target A target difference for `a` and `b` - * @param {function} constraint.base.weightA The amount to adjust `a[property]` - * @param {function} constraint.base.weightB The amount to adjust `b[property]` - * @param {object=} constants The constants used by constraints + * @param {?function} constraint.base.solve A function that solves the constraint in-place + * @param {?function} constraint.base.strict A function returns the constraint in strict form + * @param {?object} constants The constants used by constraints * @param {number=1} iterations The number of iterations * @param {boolean=false} strict */ @@ -58,13 +30,12 @@ export const solve = ( strict = false ) => { if (strict) return solveStrict(constraints, constants); - return solveLoose(constraints, constants, iterations); + return solveLoose(constraints, constants, Math.ceil(iterations)); }; /** * Applies the given constraints to the objects in-place. - * Constraint targets and operators can be static or dynamic. - * A solution is approximated iteratively + * A solution is approximated iteratively. * @param {array} constraints The constraints. See docs for `solve` * @param {object} constants The constants used by constraints * @param {number} iterations The number of iterations @@ -72,69 +43,54 @@ export const solve = ( const solveLoose = (constraints, constants, iterations) => { for (let i = 0; i < iterations; i += 1) { for (const co of constraints) { - const base = co.base; - const a = co.a[base.property]; - const b = co.b[base.property]; - const difference = base.difference(a, b, co, constants); - const distance = base.distance(a, b, co, constants); - const target = base.target(a, b, co, constants, difference, distance); - - if (!base.operator(distance, target, difference)) { - const resolve = base.strength(co, constants) * (difference - target); - let weightA = base.weightA(co, constants); - let weightB = base.weightB(co, constants); - - weightA = weightA / (weightA + weightB); - weightB = 1 - weightA; - - co.a[base.property] -= weightA * resolve; - co.b[base.property] += weightB * resolve; - } + co.base.solve(co, constants); } } }; /** * Applies the given constraints to the objects in-place. - * A solution is found exactly if possible, otherwise throws an error - * Limitations: - * - Constraint targets and operators must be static - * - `constraint.difference` is always subtract - * - `constraint.distance` is always subtract (i.e. signed) + * A solution is found exactly if possible, otherwise throws an error. * @param {array} constraints The constraints. See docs for `solve` * @param {object} constants The constants used by constraints */ const solveStrict = (constraints, constants) => { - const solver = new kiwi.Solver(); + const solver = new Solver(); const variables = {}; + const variableId = (obj, property) => `${obj.id}_${property}`; + + const addVariable = (obj, property) => { + const id = variableId(obj, property); + + if (!variables[id]) { + const variable = (variables[id] = new Variable()); + variable.property = property; + variable.obj = obj; + } + }; + for (const co of constraints) { - const base = co.base; - variables[key(co.a, base.property)] = new kiwi.Variable(); - variables[key(co.b, base.property)] = new kiwi.Variable(); + addVariable(co.a, co.base.property); + addVariable(co.b, co.base.property); } for (const co of constraints) { - const base = co.base; - const expression = variables[key(co.a, base.property)].minus( - variables[key(co.b, base.property)] + solver.addConstraint( + co.base.strict( + co, + constants, + variables[variableId(co.a, co.base.property)], + variables[variableId(co.b, co.base.property)] + ) ); - - co.constraint = new kiwi.Constraint( - expression, - toStrictOperator(base.operator), - base.target(null, null, co, constants), - base.required === true ? kiwi.Strength.required : kiwi.Strength.strong - ); - - solver.addConstraint(co.constraint); } solver.updateVariables(); - for (const co of constraints) { - const base = co.base; - co.a[base.property] = variables[key(co.a, base.property)].value(); - co.b[base.property] = variables[key(co.b, base.property)].value(); + const variablesList = Object.values(variables); + + for (const variable of variablesList) { + variable.obj[variable.property] = variable.value(); } };