Skip to content

k4b7/constraint.js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status

constraint.js

Simplifies using cassowary.js constraint solver by wrapping existing object and class such they automatically create appropriate c.Variable and c.Expression instances. Adding constraints between objects is also simplified by passing a function contain code describing the constraints to addConstraints.

Example

var c = require("constraint");

var r1 = { x: 50, y: 50, w: 100, h: 25 };
var r2 = { x: 50, y: 50, w: 75, h: 75 };

var desc = { x: "var", y: "var", w: "fixed", h: "fixed" };

c.wrapObject(r1, desc);
c.wrapObject(r2, desc);

c.addConstraints(function (r1, r2) {
    r2.x + r2.h / 2 == r1.x + r1.h / 2; // align horizontally along the center
    r2.y - 10 == r1.x + r1.h;           // create a 10px vertical gap
}, r1, r2);
    
console.log(r1);    // 
console.log(r2);    //

Motivation

It's much easier to describe a relation between expressions than it is to figure the code to enforce that relationship each time one of the variables in the relation changes. Describe the relationships between properties or expressions also makes the intent clearer.

The problem however, is that constraint solvers (at least cassowary) requires a developer to write a lot of boilerplate code to not only set up constraints but also update them and update variables on existing objects.

Some of this boilerplate can be avoided in client code by embedded library classes with the boilerplate. Unfortunately, if you want to use existing libraries this would require modifying the libraries. Moreover, it doesn't fix the situation where you have POJOs (Plain Old Javascript Objects).

Documentation

Descriptors specify which properties to create variables or expressions for within the constraint solver. Each key should match a property on the object or class being wrapped.

  • "var" creates a variable which is affected by other variables
  • "fixed" creates a variable which is unaffected by other variables

Both of these can only be created for simple properties, not computed properties.

  • "comp" creates an expression which is affected by the variables it depends on

This can only be used with computed properties.

var c = require("constraint");
class Rect {
    constructor(x, y, w, h) {
        Object.assign(self, {x, y, w, h});
    }
    get right() {
        return this.x + this.w;
    }
    get bottom() {
        return this.y + this.h;
    }
}

Rect = c.wrapClass(Rect, { 
    x: "var", y: "var", 
    w: "fixed", h: "fixed", 
    right: "comp", bottom: "comp" 
});

var r1 = new Rect(50, 50, 100, 25);
var r2 = new Rect(50, 50, 75, 75);

c.addConstraints(function (r1, r2) {
    r2.y == r1.bottom;
    r2.x == r1.right;
}, r1, r2);

console.log(r1);    // {x:0, y:0, w:100, h:25}
console.log(r2);    // {x:100, y:25, w:75, h:75}

Note that x and y are initialized to 0. All properties marked as "var" will default to some initial value after the constraints have been setup. In order to "fix" this, simply set the value on the properties you want to update.

r1.x = 200;

console.log(r1);    // {x:200, y:0, w:100, h:25}
console.log(r2);    // {x:300, y:25, w:75, h:75}

When setting properties to values that conflict with each other, the last property to be set wins.

r1.x = 200;
r2.x = 200;
    
console.log(r1);    // {x:100, y:0, w:100, h:25}
console.log(r2);    // {x:200, y:25, w:75, h:75}

Notes about addConstraints: argument order matters. The function being passed in is parsed and the the names of the arguments are and their order is extracted from that function. The last two arguments to addConstraints must be in the same order.

c.addConstraints(function (r1, r2) {
    r2.y == r1.bottom;
    r2.x == r1.right;
}, r1, r2); // GOOD

c.addConstraints(function (r1, r2) {
    r2.y == r1.bottom;
    r2.x == r1.right;
}, r2, r1); // BAD - r1 and r2 have been swapped

The second call to addConstraints is equivalent to:

c.addConstraints(function (r1, r2) {
    r1.y == r2.bottom;
    r1.x == r2.right;
}, r1, r2);

There's also a handy decorator syntax that you can use if you're using a browser or transpiler that supports it.

var desc = { 
    x: "var", y: "var", 
    w: "fixed", h: "fixed", 
    right: "comp", bottom: "comp" 
};

@c.decorateClass(desc)
class Rect {
    constructor(x, y, w, h) {
        Object.assign(self, {x, y, w, h});
    }
    get right() {
        return this.x + this.w;
    }
    get bottom() {
        return this.y + this.h;
    }
}

The system supports all of the same linear constraints as cassowary.js. This includes: >, <, >=, <=, ==. Here's an example using the > operator.

c.addConstraints(function (r1, r2) {
    r2.x > r1.x + r1.w;     // r2 is always to the left of r1
}, r1, r2);

Expressions can include linear operators, constants, and properties on objects that have been wrapped by constraint.js.

Limitations

The system cannot wrap variables that aren't objects. If there variables that you would like to create constraints for, create an object containing the variables as properties, wrap that object, and create constraints.

var x = 5;
var y = 10;

var obj = c.wrapObject({x, y});

c.addConstraints(function(obj) {
    obj.y = 2 * obj.x;
});

console.log(obj);                   // { x:0, y: 0 }
console.log(`x = ${x}, y = ${y}`);  // x = 5, y = 10

obj.x = 50;

console.log(obj);                   // { x:50, y:100 }

Computed properties containing only a single return statement are supported.

Implmentation Details

Symbols

The wrap functions modify existing objects and classes in such a way that they won't interfere with their operation. This is done by adding properties using Symbols as computed property names. Symbols prevent the possibility of collision with existing properties will protect them from be accidentally clobbered by other code.

Code as data

Calling .toString() on a function reference returns the source code for that function. The source code for individual functions is parsed using esprima. Contraints are derived from expressions within the AST. This technique is used for "comp" properties when wrapping objects/classes and by addConstraints.

Future Work

  • moar tests
  • comparison operators listed above
  • decorators for methods (which should replace the need for the descriptor)
  • support more expressions within addConstraint
    • array indexing
    • looping constructs
  • improve error handling
  • get current constraints
    • all
    • for a particular property (on an object)
  • ability to remove constraints
  • allow different objects to be in separate solvers
    • disallow constraints between objects in separate solvers

About

Define constraints with JavaScript syntax. Uses cassowary.js as a solver.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors