Lume Kiwi

repository·main·Indexed 42 days ago

https://github.com/lume/kiwi

A high-performance TypeScript implementation of the Cassowary constraint solving algorithm. Designed for layout engines and constraint-based systems, it provides a lightweight solver to manage linear constraints using Variable, Expression, and Constraint classes. Version 0.4.4.

Tokens
6.6K
Snippets
20
Records
27
Agent score
52%

What's inside Kiwi

  1. Quickstart: Using the Lume Kiwi Solver

    main

    Lume Kiwi is a lightweight and fast implementation of the Cassowary constraint solving algorithm. To use it, you create a Solver, define Variable objects, add them to the solver as editable variables, define Constraint objects using Expression math, and then call updateVariables() to solve the system.

    Note: You can adjust solver.maxIterations (default is 10,000) if the solver requires more iterations to converge.

    import * as kiwi from '@lume/kiwi'
    
    // Create a solver
    const solver = new kiwi.Solver()
    
    // Adjust the max number of solver iterations before an error is thrown if
    // more is needed. Default is 10,000.
    solver.maxIterations = 20000
    
    // Create and add some editable variables
    const left = new kiwi.Variable()
    const width = new kiwi.Variable()
    solver.addEditVariable(left, kiwi.Strength.strong)
    solver.addEditVariable(width, kiwi.Strength.strong)
    
    // Create a variable calculated through a constraint
    const centerX = new kiwi.Variable()
    const expr = new kiwi.Expression([-1, centerX], left, [0.5, width])
    solver.addConstraint(new kiwi.Constraint(expr, kiwi.Operator.Eq, kiwi.Strength.required))
    
    // Suggest some values to the solver
    solver.suggestValue(left, 0)
    solver.suggestValue(width, 500)
    
    // Lets solve the problem!
    solver.updateVariables()
    
    console.assert(centerX.value() === 250)
  2. Install @lume/kiwi via CDN and Import Maps

    main

    For plain web applications without a build step, or for runtimes like Deno that support import maps, you can load Kiwi directly from a CDN. Add an import map to your HTML to map the @lume/kiwi module specifier to the UNPKG URL:

    <script type="importmap">
      {
        "imports": {
          "@lume/kiwi": "https://unpkg.com/@lume/kiwi@0.4.2/dist/kiwi.js"
        }
      }
    </script>
  3. Basic Usage of Kiwi Constraint Solver

    main

    After installation, import the module and use the Solver, Variable, Constraint, and Expression classes to define and solve linear constraints.

    Key workflow:

    1. Instantiate a new kiwi.Solver().
    2. Create variables using new kiwi.Variable().
    3. Add edit variables to the solver using solver.addEditVariable(variable, strength) to allow them to be suggested.
    4. Suggest values for edit variables using solver.suggestValue(variable, value).
    5. Define constraints using new kiwi.Constraint(expression, operator) and add them via solver.addConstraint(constraint).
    6. Execute the solver using solver.updateVariables() to calculate the resulting values.
    import * as kiwi from '@lume/kiwi'
    
    // Create a solver
    var solver = new kiwi.Solver()
    
    // Create edit variables
    var left = new kiwi.Variable()
    var width = new kiwi.Variable()
    solver.addEditVariable(left, kiwi.Strength.strong)
    solver.addEditVariable(width, kiwi.Strength.strong)
    solver.suggestValue(left, 100)
    solver.suggestValue(width, 400)
    
    // Create and add a constraint
    var right = new kiwi.Variable()
    // Constraint: -1 * right + left + width = 0  => right = left + width
    solver.addConstraint(new kiwi.Constraint(new kiwi.Expression([-1, right], left, width), kiwi.Operator.Eq))
    
    // Solve the constraints
    solver.updateVariables()
    
    console.assert(right.value() === 500)
    
    // later, update the constraints and re-calculate
    setTimeout(() => {
      solver.suggestValue(left, 200)
      solver.suggestValue(width, 600)
    
      solver.updateVariables() // update
    
      console.assert(right.value() === 800)
    }, 2000)
  4. Define linear constraints with the Constraint class

    main

    A Constraint represents a linear equation of the form LHS [operator] RHS.

    Constructor Parameters:

    • expression (Expression): The Left Hand Side (LHS) of the equation.
    • operator (Operator): The relational operator (e.g., <=, >=, ==).
    • rhs (Expression, optional): The Right Hand Side (RHS) of the equation. If omitted, the RHS is implicitly zero.
    • strength (Number, optional): The strength of the constraint. Defaults to Strength.required.

    Instance Methods:

    • expression(): Returns the Expression used for the LHS.
    • op(): Returns the Operator used in the constraint.
    • strength(): Returns the numerical strength of the constraint.
    // Example: x + 5 <= 10 with strong strength
    const constraint = new Constraint(x.plus(5), Operator.LE, 10, Strength.strong);
    
    // Example: x - 2 == 0 (implicit RHS is 0)
    const implicitConstraint = new Constraint(x.minus(2), Operator.EQ);
  5. Use the Solver class to manage constraints

    main

    The Solver class is the core engine for managing and resolving linear constraints between variables. You can create constraints, add/remove them, and manage 'edit variables' which allow you to suggest values that the solver will try to satisfy while respecting required constraints.

    Key Capabilities

    • Constraint Management: Use createConstraint to define relationships between expressions or variables, and addConstraint/removeConstraint to manage the solver's state.
    • Edit Variables: Use addEditVariable to designate specific variables that can be adjusted. These should typically be added with a strength less than Strength.required.
    • Value Suggestion: Use suggestValue(variable, value) to provide a target value for an edit variable, which the solver will then attempt to reconcile with existing constraints.
    • Execution: Call updateVariables() to trigger the solver to recalculate and update the values of all variables based on the current constraints and suggestions.
    // Example conceptual usage
    const solver = new Solver();
    const v1 = new Variable('v1');
    const v2 = new Variable('v2');
    
    // Create a required constraint: v1 + v2 == 10
    solver.createConstraint(v1.plus(v2), Operator.Eq, 10, Strength.required);
    
    // Add v1 as an edit variable so we can suggest values for it
    solver.addEditVariable(v1, Strength.weak);
    solver.suggestValue(v1, 7);
    
    // Solve
    solver.updateVariables();
    console.log(v1.value()); // Should be close to 7
    console.log(v2.value()); // Should be close to 3
  6. Manage edit variables in the Solver

    main

    Edit variables are special variables that the solver can adjust to satisfy constraints. Unlike required constraints, edit variables are used to find a solution that is 'closest' to a suggested value.

    Methods:

    • addEditVariable(variable, strength): Adds a variable as an editable one. The strength should be less than Strength.required.
    • removeEditVariable(variable): Removes the variable from the edit list.
    • hasEditVariable(variable): Returns true if the variable is currently an edit variable.
    • suggestValue(variable, value): Sets a target value for the specified variable to be used during the next updateVariables() call.
  7. Use the Expression class to build mathematical terms

    main

    The Expression class represents a mathematical expression composed of variable terms and constants. You can construct an expression by passing multiple arguments to the constructor, which are then summed. Additionally, you can use 2-tuples of [number, Variable|Expression] to represent terms that are multiplied by a coefficient.

    Supported constructor arguments:

    • number
    • Variable
    • Expression
    • Array (specifically a 2-tuple of [number, Variable|Expression])

    Once an expression is created, you can derive new expressions using arithmetic methods. All these methods return a new Expression instance, allowing for chaining.

    // Example of creating and manipulating expressions
    const expr = new Expression(10, myVariable).plus(5).multiply(2);
  8. Use the Variable class

    main

    The Variable class is the primary user constraint variable in Kiwi. Variables can hold a name, a numeric value, and can be subscribed to for changes.

    Methods

    • new Variable([name]): Creates a new variable. name is an optional String (defaults to "").
    • name(): Returns the variable's name as a String.
    • setName(name): Sets the variable's name.
    • value(): Returns the current calculated Number value.
    • subscribe(callback): Registers a callback function to be executed whenever the variable's value changes.
    • unsubscribe(): Stops the variable from calling the registered callback.

    Creating Expressions

    You can create Expression objects from a Variable using arithmetic methods:

    • plus(value): Adds a Number, Variable, or Expression to the variable.
    • minus(value): Subtracts a Number, Variable, or Expression from the variable.
    • multiply(coefficient): Multiplies the variable by a fixed Number.
    • divide(coefficient): Divides the variable by a fixed Number.
    const v = new kiwi.Variable('myVar');
    v.subscribe(() => console.log('Value changed!'));
    
    const expr = v.plus(10).multiply(2);
  9. Configure symbolic strength with the Strength class

    main

    The Strength class allows you to define the importance or 'weight' of constraints in a solver. You can use predefined symbolic strengths or create custom ones.

    Predefined symbolic strengths:

    • Strength.required: The highest priority strength.
    • Strength.strong: High priority.
    • Strength.medium: Medium priority.
    • Strength.weak: Low priority.

    Custom Strengths: You can create a custom strength using the static Strength.create(a, b, c, [w]) method, where a, b, and c represent the weights for strong, medium, and weak respectively, and w is an optional overall weight (defaults to 1).

    // Using predefined strengths
    const myStrength = Strength.strong;
    
    // Creating a custom strength
    const customStrength = Strength.create(10, 5, 1, 2);
  10. Create constraints with createConstraint()

    main

    The createConstraint method defines a relationship between two sides of an expression using a specified operator and strength. This method automatically adds the created constraint to the solver.

    Parameters:

    ParamTypeDefaultDescription
    lhsExpressionLeft hand side of the expression
    operatorOperatorThe comparison operator (Le, Ge, or Eq)
    rhsExpressionRight hand side (Expression, Variable, or Number)
    strengthNumberStrength.requiredThe importance of the constraint
  11. Manipulate Expressions with arithmetic methods

    main

    The Expression class provides several instance methods to create new expressions based on an existing one:

    • plus(value): Adds a Number, Variable, or Expression to the current expression.
    • minus(value): Subtracts a Number, Variable, or Expression from the current expression.
    • multiply(coefficient): Multiplies the current expression by a fixed Number.
    • divide(coefficient): Divides the current expression by a fixed Number.

    All these methods return a new Expression object.

    const expr = new Expression(x);
    const newExpr = expr.plus(10).multiply(0.5);