├── .babelrc ├── web.js ├── node.js ├── lib ├── amoebalib.bc ├── amoebalib.cpp └── index.js ├── flow-typed └── amoebalib.js ├── .gitmodules ├── .flowconfig ├── test └── simple_test.js ├── Makefile ├── package.json ├── .gitignore ├── amoeba-LICENSE ├── webpack.config.js └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "flow"] 3 | } 4 | -------------------------------------------------------------------------------- /web.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist/amoebajs_web.js"); 2 | -------------------------------------------------------------------------------- /node.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./dist/amoebajs_node.js"); 2 | -------------------------------------------------------------------------------- /lib/amoebalib.bc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zacharyvoase/amoebajs/master/lib/amoebalib.bc -------------------------------------------------------------------------------- /flow-typed/amoebalib.js: -------------------------------------------------------------------------------- 1 | declare module "./amoebalib.bc" { 2 | declare module.exports: any 3 | } 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "amoeba"] 2 | path = amoeba 3 | url = https://github.com/zacharyvoase/amoeba.git 4 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | unsafe.enable_getters_and_setters=true 11 | -------------------------------------------------------------------------------- /lib/amoebalib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "amoeba.h" 4 | 5 | extern "C" { 6 | __attribute__((used)) int am_getautoupdate(am_Solver *solver) { 7 | return solver->auto_update; 8 | } 9 | __attribute__((used)) double am_getstrength(am_Constraint *constraint) { 10 | return constraint->strength; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/simple_test.js: -------------------------------------------------------------------------------- 1 | amoeba = require('../dist/amoebajs_node.js') 2 | 3 | amoeba.ready.then(() => { 4 | let solver = new amoeba.Solver().setAutoUpdate(true), 5 | v1 = solver.newVariable(), 6 | v2 = solver.newVariable() 7 | 8 | v1.mustBeGreaterThanEqual(0).add() 9 | v1.mustBeLessThanEqual(v2.divide(2).subtract(4)).add() 10 | 11 | console.log("v1 = " + v1.value) 12 | console.log("v2 = " + v2.value) 13 | 14 | v1.suggest(5.5) 15 | 16 | console.log("v1 = " + v1.value) 17 | console.log("v2 = " + v2.value) 18 | }) 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CXX=em++ 2 | CXXFLAGS=-std=c++11 --bind -Iamoeba -DAM_API='extern __attribute__((used))' -DAM_IMPLEMENTATION -s RESERVED_FUNCTION_POINTERS=1 3 | 4 | all: dist/amoebajs_node.js 5 | 6 | dist/amoebajs_node.js: dist/amoebajs_web.js 7 | 8 | dist/amoebajs_web.js: lib/amoebalib.bc lib/index.js 9 | npm run flow 10 | webpack 11 | 12 | lib/amoebalib.bc: lib/amoebalib.cpp amoeba/amoeba.h 13 | ${CXX} ${CXXFLAGS} lib/amoebalib.cpp -o lib/amoebalib.bc 14 | 15 | clean: 16 | rm lib/amoebalib.bc dist/amoebajs_node.js dist/amoebajs_web.js 17 | 18 | .PHONY: all clean 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amoebajs", 3 | "version": "1.1.0", 4 | "author": "Zack Voase ", 5 | "description": "A JS/WebAssembly binding for the Amoeba C++ linear constraint solver", 6 | "homepage": "https://github.com/zacharyvoase/amoebajs", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/zacharyvoase/amoebajs.git" 10 | }, 11 | "license": "Unlicense", 12 | "main": "lib/index.js", 13 | "devDependencies": { 14 | "babel-cli": "^6.26.0", 15 | "babel-core": "^6.26.0", 16 | "babel-loader": "^7.1.2", 17 | "babel-preset-env": "^1.6.0", 18 | "babel-preset-flow": "^6.23.0", 19 | "flow-bin": "^0.53.1", 20 | "generate-export-aliases": "^1.0.0", 21 | "llvmbc-wasm-loader": "^1.0.0", 22 | "webpack": "^3.5.5" 23 | }, 24 | "scripts": { 25 | "prepare": "./node_modules/.bin/generate-export-aliases", 26 | "flow": "./node_modules/.bin/flow" 27 | }, 28 | "config": { 29 | "exportAliases": { 30 | "web": "./dist/amoebajs_web.js", 31 | "node": "./dist/amoebajs_node.js" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /amoeba-LICENSE: -------------------------------------------------------------------------------- 1 | This library contains Amoeba (https://github.com/starwing/amoeba), which is 2 | released under the following license: 3 | 4 | Copyright © 2016 Xavier Wang 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | this software and associated documentation files (the "Software"), to deal in 8 | the Software without restriction, including without limitation the rights to 9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 10 | of the Software, and to permit persons to whom the Software is furnished to do 11 | so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | 5 | const rules = [ 6 | { 7 | test: /\.bc$/, 8 | use: { 9 | loader: 'llvmbc-wasm-loader', 10 | options: { 11 | command: [ 12 | 'em++', 13 | '-s', 'NO_EXIT_RUNTIME=1', 14 | '-s', 'RESERVED_FUNCTION_POINTERS=1', 15 | '--bind' 16 | ] 17 | } 18 | } 19 | }, 20 | { test: /\.js$/, use: 'babel-loader' } 21 | ] 22 | 23 | const plugins = [ 24 | new webpack.BannerPlugin( 25 | fs.readFileSync('./amoeba-LICENSE', {encoding: 'utf-8'}) 26 | ) 27 | ] 28 | 29 | const nodeTarget = { 30 | target: 'node', 31 | entry: './lib/index.js', 32 | output: { 33 | library: 'amoeba', 34 | libraryTarget: 'commonjs2', 35 | path: path.resolve(__dirname, 'dist'), 36 | filename: 'amoebajs_node.js' 37 | }, 38 | module: { 39 | rules: rules 40 | }, 41 | plugins: plugins 42 | } 43 | 44 | const webTarget = { 45 | target: 'web', 46 | entry: './lib/index.js', 47 | output: { 48 | library: 'amoeba', 49 | libraryTarget: 'umd', 50 | path: path.resolve(__dirname, 'dist'), 51 | filename: 'amoebajs_web.js' 52 | }, 53 | module: { 54 | rules: rules 55 | }, 56 | node: { 57 | fs: 'empty', 58 | path: 'empty' 59 | }, 60 | plugins: plugins 61 | } 62 | 63 | module.exports = [nodeTarget, webTarget] 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AmoebaJS 2 | 3 | AmoebaJS is a linear constraint solver for the web. It's based on a WebAssembly 4 | compilation of Xavier Wang's [Amoeba][] C library, bound with an idiomatic 5 | JavaScript interface. 6 | 7 | AmoebaJS is provided in its original source as ES6 + LLVM bytecode, a web 8 | distribution which attaches the module to `window.amoeba`, and a NodeJS 9 | distribution which can be `require()`d. Additional targets may be built for by 10 | users; consult the [webpack config][] for some starting points. 11 | 12 | [Amoeba]: https://github.com/starwing/amoeba 13 | [webpack config]: https://github.com/zacharyvoase/amoebajs/blob/master/webpack.config.js 14 | 15 | 16 | ## Installation 17 | 18 | You can use NPM or Yarn to install: 19 | 20 | $ npm install amoebajs 21 | 22 | 23 | ## Usage 24 | 25 | Import and create a solver: 26 | 27 | let amoeba = require('amoebajs/node') 28 | 29 | let solver = new amoeba.Solver() 30 | 31 | Set up variables: 32 | 33 | let v1 = solver.newVariable(), v2 = solver.newVariable() 34 | 35 | Set up constraints, and call `.add()` to enable them: 36 | 37 | v1.mustEqual(v2.divide(2).subtract(4)).add() 38 | 39 | Suggest a value for one variable: 40 | 41 | v1.suggest(10) 42 | 43 | Read variable values: 44 | 45 | console.log("v1 = " + v1.value) // v1 = 10 46 | console.log("v2 = " + v2.value) // v2 = 28 47 | 48 | 49 | ## Licensing 50 | 51 | The original JavaScript in this repository is released under the Unlicense: 52 | 53 | > This is free and unencumbered software released into the public domain. 54 | > 55 | > Anyone is free to copy, modify, publish, use, compile, sell, or 56 | > distribute this software, either in source code form or as a compiled 57 | > binary, for any purpose, commercial or non-commercial, and by any 58 | > means. 59 | > 60 | > In jurisdictions that recognize copyright laws, the author or authors 61 | > of this software dedicate any and all copyright interest in the 62 | > software to the public domain. We make this dedication for the benefit 63 | > of the public at large and to the detriment of our heirs and 64 | > successors. We intend this dedication to be an overt act of 65 | > relinquishment in perpetuity of all present and future rights to this 66 | > software under copyright law. 67 | > 68 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 69 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 70 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 71 | > IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 72 | > OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 73 | > ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 74 | > OTHER DEALINGS IN THE SOFTWARE. 75 | > 76 | > For more information, please refer to 77 | 78 | Amoeba will be implicitly bundled with all builds, and is licensed under 'the 79 | same license as Lua', which can be inferred as the MIT license: 80 | 81 | > Copyright © 2016 Xavier Wang 82 | > 83 | > Permission is hereby granted, free of charge, to any person obtaining a copy of 84 | > this software and associated documentation files (the "Software"), to deal in 85 | > the Software without restriction, including without limitation the rights to 86 | > use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 87 | > of the Software, and to permit persons to whom the Software is furnished to do 88 | > so, subject to the following conditions: 89 | > 90 | > The above copyright notice and this permission notice shall be included in all 91 | > copies or substantial portions of the Software. 92 | > 93 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 94 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 95 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 96 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 97 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 98 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 99 | > SOFTWARE. 100 | 101 | If you use a built version of this library on your website, you don't need to 102 | include a copy of the Unlicense but you should include a reproduction of Mr. 103 | Wang's license as reproduced above. 104 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import * as lib from './amoebalib.bc' 4 | 5 | export {lib as lib} 6 | export let ready = lib.ready 7 | 8 | export const ErrorCodes = { 9 | AM_OK: 0, 10 | AM_FAILED: -1, 11 | AM_UNSATISFIED: -2, 12 | AM_UNBOUND: -3 13 | } 14 | 15 | type ErrorCode = $Keys 16 | 17 | export const Strengths = { 18 | required: 1000000000, 19 | strong: 1000000, 20 | medium: 1000, 21 | weak: 1 22 | } 23 | 24 | type Strength = $Keys 25 | 26 | export const Relations = { 27 | lessThanEqual: 1, 28 | equal: 2, 29 | greaterThanEqual: 3 30 | } 31 | 32 | type Relation = $Keys 33 | 34 | export class SolverError extends Error { 35 | message: string 36 | code: number 37 | name: string 38 | 39 | constructor(code: number) { 40 | let message 41 | if (code == ErrorCodes.AM_FAILED) { 42 | message = 'Failed' 43 | } else if (code == ErrorCodes.AM_UNSATISFIED) { 44 | message = 'Unsatisfied' 45 | } else if (code == ErrorCodes.AM_UNBOUND) { 46 | message = 'Unbound' 47 | } else { 48 | message = 'Unknown' 49 | } 50 | super(message) 51 | this.code = code 52 | this.name = 'SolverError' 53 | this.stack = (new Error()).stack 54 | } 55 | } 56 | 57 | function guard(result: number) { 58 | if (result !== ErrorCodes.AM_OK) { 59 | throw new SolverError(result) 60 | } 61 | } 62 | 63 | const solvers: Map = new Map() 64 | 65 | let varcallback 66 | lib.ready.then(() => { 67 | varcallback = lib.Runtime.addFunction(function(solverPtr, varPtr, newValue, oldValue) { 68 | let solver = solvers.get(solverPtr) 69 | if (typeof solver === 'undefined') { 70 | console.log("Got callback for untracked solver pointer: " + solverPtr) 71 | return 72 | } 73 | let variable = solver._vars.get(varPtr) 74 | if (typeof variable === 'undefined') { 75 | console.log("Got callback for untracked variable pointer: " + varPtr) 76 | return 77 | } 78 | variable._valueChanged(newValue, oldValue) 79 | }) 80 | }) 81 | 82 | export class Solver { 83 | _solver: number 84 | _vars: Map 85 | _constraints: Map 86 | 87 | constructor() { 88 | this._solver = lib._am_newsolver(null, null, varcallback); 89 | solvers.set(this._solver, this) 90 | this._vars = new Map() 91 | this._constraints = new Map() 92 | this.setAutoUpdate(true) 93 | } 94 | 95 | reset() { 96 | lib._am_resetsolver(this._solver, 1) 97 | return this 98 | } 99 | 100 | free() { 101 | this._constraints.forEach((constraint, pointer) => constraint.free()) 102 | this._constraints.clear() 103 | this._vars.forEach((variable, pointer) => variable.free()) 104 | this._vars.clear() 105 | if (this._solver) { lib._am_delsolver(this._solver) } 106 | solvers.delete(this._solver); 107 | } 108 | 109 | get autoupdate(): boolean { 110 | return !!lib._am_getautoupdate(this._solver) 111 | } 112 | 113 | setAutoUpdate(au: boolean) { 114 | lib._am_autoupdate(this._solver, au) 115 | return this 116 | } 117 | 118 | updateVars(): Solver { 119 | lib._am_updatevars(this._solver) 120 | return this 121 | } 122 | 123 | newVariable(): Variable { 124 | let variable = new Variable(this, lib._am_newvariable(this._solver)) 125 | this._vars.set(variable._var, variable) 126 | return variable 127 | } 128 | 129 | newConstraint(strength: Strength): Constraint { 130 | let constraint = new Constraint(this, lib._am_newconstraint(this._solver, strength)) 131 | this._constraints.set(constraint._constraint, constraint) 132 | return constraint 133 | } 134 | } 135 | 136 | type Expressionable = Variable | Expression | ExpressionLike | Number 137 | 138 | class ExpressionLike { 139 | solver: Solver 140 | 141 | plus(other: Expressionable) { 142 | return ensureExpression(this.solver, this, true)._plus(ensureExpression(this.solver, other, false)) 143 | } 144 | 145 | subtract(other: Expressionable) { 146 | return ensureExpression(this.solver, this, true)._plus(ensureExpression(this.solver, other, true)._invert()) 147 | } 148 | 149 | times(multiplier: number) { 150 | return ensureExpression(this.solver, this, true)._times(multiplier) 151 | } 152 | 153 | divide(divisor: number) { 154 | return ensureExpression(this.solver, this, true)._times(1 / divisor) 155 | } 156 | 157 | shouldEqual(other: Expressionable) { 158 | return makeConstraint(this, 'equal', other, 'medium') 159 | } 160 | 161 | mustEqual(other: Expressionable) { 162 | return makeConstraint(this, 'equal', other, 'required') 163 | } 164 | 165 | shouldBeLessThanEqual(other: Expressionable) { 166 | return makeConstraint(this, 'lessThanEqual', other, 'medium') 167 | } 168 | 169 | mustBeLessThanEqual(other: Expressionable) { 170 | return makeConstraint(this, 'lessThanEqual', other, 'required') 171 | } 172 | 173 | shouldBeGreaterThanEqual(other: Expressionable) { 174 | return makeConstraint(this, 'greaterThanEqual', other, 'medium') 175 | } 176 | 177 | mustBeGreaterThanEqual(other: Expressionable) { 178 | return makeConstraint(this, 'greaterThanEqual', other, 'required') 179 | } 180 | } 181 | 182 | export class Variable extends ExpressionLike { 183 | solver: Solver 184 | _var: number 185 | eventListeners: Array<(number, number) => void> 186 | 187 | constructor(solver: Solver, _var: number) { 188 | super() 189 | this.solver = solver 190 | this._var = _var 191 | this.eventListeners = [] 192 | } 193 | 194 | get id() { 195 | return lib._am_variableid(this._var) 196 | } 197 | 198 | get value() { 199 | return lib._am_value(this._var) 200 | } 201 | 202 | free() { 203 | if (this._var) { lib._am_delvariable(this._var) } 204 | } 205 | 206 | hasEdit(): boolean { 207 | return !!lib._am_hasedit(this._var) 208 | } 209 | 210 | addEdit(strength: Strength) { 211 | if (!strength) { 212 | strength = 'medium' 213 | } 214 | guard(lib._am_addedit(this._var, Strengths[strength])) 215 | return this 216 | } 217 | 218 | delEdit() { 219 | lib._am_deledit(this._var) 220 | return this 221 | } 222 | 223 | suggest(value: number) { 224 | lib._am_suggest(this._var, value) 225 | return this 226 | } 227 | 228 | onChange(callback: (number, number) => void) { 229 | this.eventListeners.push(callback) 230 | return this 231 | } 232 | 233 | _valueChanged(newValue: number, oldValue: number) { 234 | this.eventListeners.forEach((listener) => listener(newValue, oldValue)) 235 | } 236 | } 237 | 238 | export class Constraint { 239 | solver: Solver 240 | _constraint: number 241 | 242 | constructor(solver: Solver, _constraint: number) { 243 | this.solver = solver 244 | this._constraint = _constraint 245 | } 246 | 247 | clone(strength: Strength) { 248 | return new Constraint(this.solver, lib._am_cloneconstraint(this._constraint, Strengths[strength])) 249 | } 250 | 251 | reset() { 252 | lib._am_resetconstraint(this._constraint) 253 | return this 254 | } 255 | 256 | free() { 257 | if (this._constraint) { lib._am_delconstraint(this._constraint) } 258 | } 259 | 260 | add() { 261 | guard(lib._am_add(this._constraint)) 262 | return this 263 | } 264 | 265 | remove() { 266 | lib._am_remove(this._constraint) 267 | return this 268 | } 269 | 270 | addTerm(variable: Variable, multiplier: number) { 271 | guard(lib._am_addterm(this._constraint, variable._var, multiplier)) 272 | return this 273 | } 274 | 275 | setRelation(relation: Relation) { 276 | guard(lib._am_setrelation(this._constraint, Relations[relation])) 277 | return this 278 | } 279 | 280 | addConstant(constant: number) { 281 | guard(lib._am_addconstant(this._constraint, constant)) 282 | return this 283 | } 284 | 285 | get strength() { 286 | return lib._am_getstrength(this._constraint) 287 | } 288 | 289 | setStrength(strength: Strength) { 290 | guard(lib._am_setstrength(this._constraint, Strengths[strength])) 291 | } 292 | 293 | merge(other: Constraint, multiplier: number) { 294 | lib._am_mergeconstraint(this._constraint, other._constraint, multiplier) 295 | } 296 | } 297 | 298 | export class Expression extends ExpressionLike { 299 | solver: Solver 300 | terms: Map 301 | constant: number 302 | 303 | constructor(solver: Solver) { 304 | super() 305 | this.solver = solver 306 | this.terms = new Map() 307 | this.constant = 0 308 | } 309 | 310 | static fromVariable(variable: Variable): Expression { 311 | return (new Expression(variable.solver)).addTerm(variable, 1) 312 | } 313 | 314 | static fromConstant(solver: Solver, constant: number): Expression { 315 | return (new Expression(solver)).addConstant(constant) 316 | } 317 | 318 | addConstant(constant: number) { 319 | this.constant += constant 320 | return this 321 | } 322 | 323 | addTerm(variable: Variable, multiplier: number) { 324 | if (this.terms.has(variable)) { 325 | this.terms.set(variable, this.terms.get(variable) + multiplier) 326 | } else { 327 | this.terms.set(variable, multiplier) 328 | } 329 | return this 330 | } 331 | 332 | _times(x: number) { 333 | let terms: Map = new Map() 334 | this.terms.forEach((multiplier, variable) => { 335 | terms.set(variable, multiplier * x) 336 | }) 337 | this.terms = terms 338 | this.constant *= x 339 | return this 340 | } 341 | 342 | _plus(other: Expressionable) { 343 | let other_exp = ensureExpression(this.solver, other, false) 344 | other_exp.terms.forEach((multiplier, variable) => { 345 | this.addTerm(variable, multiplier) 346 | }) 347 | this.constant += other_exp.constant 348 | return this 349 | } 350 | 351 | invert(): Expression { 352 | return this.clone()._invert() 353 | } 354 | 355 | _invert() { 356 | this.constant = -this.constant 357 | 358 | let terms = new Map() 359 | this.terms.forEach((multiplier, variable) => { 360 | terms.set(variable, -multiplier) 361 | }) 362 | this.terms = terms 363 | 364 | return this 365 | } 366 | 367 | clone(): Expression { 368 | let exp = new Expression(this.solver) 369 | exp.terms = new Map(this.terms) 370 | exp.constant = this.constant 371 | return exp 372 | } 373 | } 374 | 375 | function makeConstraint(exprLike1: ExpressionLike, relation: Relation, exprLike2: Expressionable, strength: Strength) { 376 | let expr1 = ensureExpression(exprLike1.solver, exprLike1, false), 377 | expr2 = ensureExpression(exprLike1.solver, exprLike2, false) 378 | 379 | if (expr1.solver !== expr2.solver) { 380 | throw new Error("Can't constrain two expressions with different solvers") 381 | } 382 | 383 | let constraint = expr1.solver.newConstraint(strength) 384 | 385 | constraint.addConstant(expr1.constant) 386 | expr1.terms.forEach((multiplier, variable) => 387 | constraint.addTerm(variable, multiplier)) 388 | 389 | constraint.setRelation(relation) 390 | 391 | constraint.addConstant(expr2.constant) 392 | expr2.terms.forEach((multiplier, variable) => 393 | constraint.addTerm(variable, multiplier)) 394 | 395 | return constraint 396 | } 397 | 398 | function ensureExpression(solver: Solver, obj: Expressionable, shouldClone: boolean): Expression { 399 | if (obj instanceof Variable) { 400 | return Expression.fromVariable(obj) 401 | } else if (obj instanceof Expression) { 402 | if (shouldClone) { 403 | return obj.clone() 404 | } else { 405 | return obj 406 | } 407 | } else if (typeof obj === 'number' && Number.isFinite(obj)) { 408 | return Expression.fromConstant(solver, obj) 409 | } else { 410 | throw new Error(`Couldn't convert to expression: ${String(obj)}`) 411 | } 412 | } 413 | --------------------------------------------------------------------------------