diff --git a/package-lock.json b/package-lock.json index 7ef3a39..48dbc88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "dependencies": { "@types/node": "18.15.0", + "@types/p5": "^1.5.0", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "eslint": "8.35.0", @@ -20,6 +21,7 @@ "react-p5": "^1.3.33", "reactflow": "^11.5.6", "typescript": "4.9.5", + "valtio": "^1.10.3", "zod": "^3.21.4" }, "devDependencies": { @@ -688,9 +690,9 @@ "integrity": "sha512-z6nr0TTEOBGkzLGmbypWOGnpSpSIBorEhC4L+4HeQ2iezKCi4f77kyslRwvHeNitymGQ+oFyIWGP96l/DPSV9w==" }, "node_modules/@types/p5": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.3.3.tgz", - "integrity": "sha512-PBSFnX6IgV6Pqlx9wocUjSkGlm1I1ymz9tEiTbdNCqig6FOGiWcVUHx13TXRTBfRIhZC9+MqqgztMsgzpueaUg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.5.0.tgz", + "integrity": "sha512-EhaRGjqGcv5lKWvBUfC4Oxi2J5T1C1HuoQnJCdxJJMrRf+HTVdh7hCgBo88nHe6LbUXxkrxVj9tc1zOuemefFA==" }, "node_modules/@types/prop-types": { "version": "15.7.5", @@ -3593,6 +3595,11 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-compare": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.5.0.tgz", + "integrity": "sha512-f1us0OsVAJ3tdIMXGQx2lmseYS4YXe4W+sKF5g5ww/jV+5ogMadPt+sIZ+88Ga9kvMJsrRNWzCrKPpr6pMWYbA==" + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -3679,6 +3686,11 @@ "react-dom": ">= 17.0.1" } }, + "node_modules/react-p5/node_modules/@types/p5": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.3.3.tgz", + "integrity": "sha512-PBSFnX6IgV6Pqlx9wocUjSkGlm1I1ymz9tEiTbdNCqig6FOGiWcVUHx13TXRTBfRIhZC9+MqqgztMsgzpueaUg==" + }, "node_modules/react-p5/node_modules/p5": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/p5/-/p5-1.3.1.tgz", @@ -4267,6 +4279,26 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/valtio": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.10.3.tgz", + "integrity": "sha512-t3Ez/+baJ+Z5tIyeaI6nCAbW/hrmcq2jditwg/X++o5IvCdiGirQKTOv1kJq0glgUo13v5oABCVGcinggBfiKw==", + "dependencies": { + "proxy-compare": "2.5.0", + "use-sync-external-store": "1.2.0" + }, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4895,9 +4927,9 @@ "integrity": "sha512-z6nr0TTEOBGkzLGmbypWOGnpSpSIBorEhC4L+4HeQ2iezKCi4f77kyslRwvHeNitymGQ+oFyIWGP96l/DPSV9w==" }, "@types/p5": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.3.3.tgz", - "integrity": "sha512-PBSFnX6IgV6Pqlx9wocUjSkGlm1I1ymz9tEiTbdNCqig6FOGiWcVUHx13TXRTBfRIhZC9+MqqgztMsgzpueaUg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.5.0.tgz", + "integrity": "sha512-EhaRGjqGcv5lKWvBUfC4Oxi2J5T1C1HuoQnJCdxJJMrRf+HTVdh7hCgBo88nHe6LbUXxkrxVj9tc1zOuemefFA==" }, "@types/prop-types": { "version": "15.7.5", @@ -6899,6 +6931,11 @@ "react-is": "^16.13.1" } }, + "proxy-compare": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/proxy-compare/-/proxy-compare-2.5.0.tgz", + "integrity": "sha512-f1us0OsVAJ3tdIMXGQx2lmseYS4YXe4W+sKF5g5ww/jV+5ogMadPt+sIZ+88Ga9kvMJsrRNWzCrKPpr6pMWYbA==" + }, "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -6947,6 +6984,11 @@ "p5": "1.3.1" }, "dependencies": { + "@types/p5": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@types/p5/-/p5-1.3.3.tgz", + "integrity": "sha512-PBSFnX6IgV6Pqlx9wocUjSkGlm1I1ymz9tEiTbdNCqig6FOGiWcVUHx13TXRTBfRIhZC9+MqqgztMsgzpueaUg==" + }, "p5": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/p5/-/p5-1.3.1.tgz", @@ -7348,6 +7390,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "valtio": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/valtio/-/valtio-1.10.3.tgz", + "integrity": "sha512-t3Ez/+baJ+Z5tIyeaI6nCAbW/hrmcq2jditwg/X++o5IvCdiGirQKTOv1kJq0glgUo13v5oABCVGcinggBfiKw==", + "requires": { + "proxy-compare": "2.5.0", + "use-sync-external-store": "1.2.0" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index b169abd..45c80c6 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@types/node": "18.15.0", + "@types/p5": "^1.5.0", "@types/react": "18.0.28", "@types/react-dom": "18.0.11", "eslint": "8.35.0", @@ -21,6 +22,7 @@ "react-p5": "^1.3.33", "reactflow": "^11.5.6", "typescript": "4.9.5", + "valtio": "^1.10.3", "zod": "^3.21.4" }, "devDependencies": { diff --git a/public/model/constraint.js b/public/model/constraint.js index 52e8a70..b8d083c 100644 --- a/public/model/constraint.js +++ b/public/model/constraint.js @@ -1,139 +1,162 @@ // data structure for representing relations // this base class is a trivial relation that only cares about the number of members -class Constraint { // :Constraint - constructor(arity) { - this.arity = arity; // :index - } - - /////////////////////////////////////// - // methods that should be overridden // - /////////////////////////////////////// - - // checks if two pieces of data are equal for purposes of this constraint - eq(dat1, dat2) { // :T -> T -> bool - return true; - } - - // checks if the given data exactly satisfies the constraint - accepts(data) { // :[T] -> bool - return this.checkArity(data); - } - - // returns an array representation of the dependency structure of the relation - // array values mean: - // false - free/independent/input - // true - bound/dependent/output - getDependencies() { // :-> [bool] - return new Array(this.arity).fill(false); - } - - // exchange free/bound status of two positions - // returns true if successful - // note: we want all constraint classes to have the property that - // if invert(a,b) succeeds than a subsequent invert(b,a) will also succeed - // (we probably also want it to give back the original constraint) - invert(take, give) { // : index -> index -> bool - if (take==give) { return false; } - if (take >= this.arity || give >= this.arity) { return false; } - return true; - } - - // change data in dependent/bound positions to fit the requirements of this constraint - update(data) { // :[T] -> [T] - return data; - } - - ///////////////////// - // utility methods // - ///////////////////// - - checkArity(data) { // :[T] -> bool - return this.arity == data.length; - } +class Constraint { + // :Constraint + constructor(arity) { + this.arity = arity; // :index + } + + /////////////////////////////////////// + // methods that should be overridden // + /////////////////////////////////////// + + // checks if two pieces of data are equal for purposes of this constraint + eq(dat1, dat2) { + // :T -> T -> bool + return true; + } + + // checks if the given data exactly satisfies the constraint + accepts(data) { + // :[T] -> bool + return this.checkArity(data); + } + + // returns an array representation of the dependency structure of the relation + // array values mean: + // false - free/independent/input + // true - bound/dependent/output + getDependencies() { + // :-> [bool] + return new Array(this.arity).fill(false); + } + + // exchange free/bound status of two positions + // returns true if successful + // note: we want all constraint classes to have the property that + // if invert(a,b) succeeds than a subsequent invert(b,a) will also succeed + // (we probably also want it to give back the original constraint) + invert(take, give) { + // : index -> index -> bool + if (take == give) { + return false; + } + if (take >= this.arity || give >= this.arity) { + return false; + } + return true; + } + + // change data in dependent/bound positions to fit the requirements of this constraint + update(data) { + // :[T] -> [T] + return data; + } + + ///////////////////// + // utility methods // + ///////////////////// + + checkArity(data) { + // :[T] -> bool + return this.arity == data.length; + } } // constraint specifying that two data must be equal -class EqualityConstraint extends Constraint { // :Constraint - // eq - notion of equality - // cp - "copy" function, sends data from 1st arg to 2nd arg - constructor(eq, cp) { - super(2); - this.cp = cp; // T -> T -> _ - this.eq = eq; // T -> T -> bool - this.primaryLeft = true; // bool - } - - accepts(data) { - return super.accepts(data) && this.eq(data[0],data[1]); - } - - getDependencies() { - return [!this.primaryLeft, this.primaryLeft]; - } - - invert(take, give) { - if (super.invert(take, give)) { - this.primaryLeft = !this.primaryLeft; - return true; - } else { - return false; - } - } - - update(data) { - let newdata = data.slice(); - if (this.primaryLeft) { - newdata[1] = this.cp(data[1], data[0]); - } else { - newdata[0] = this.cp(data[0], data[1]); - } - return newdata; - } -} - -function makeEqualityConstraintBuilder(eq, cp) { // (T -> T -> bool) -> - // (T -> T -> _) -> - // (-> EqualityConstraint) - return (function() { return new EqualityConstraint(eq, cp); }); +class EqualityConstraint extends Constraint { + // :Constraint + // eq - notion of equality + // cp - "copy" function, sends data from 1st arg to 2nd arg + constructor(eq, cp) { + super(2); + this.cp = cp; // T -> T -> _ + this.eq = eq; // T -> T -> bool + this.primaryLeft = true; // bool + } + + accepts(data) { + return super.accepts(data) && this.eq(data[0], data[1]); + } + + getDependencies() { + return [!this.primaryLeft, this.primaryLeft]; + } + + invert(take, give) { + if (super.invert(take, give)) { + this.primaryLeft = !this.primaryLeft; + return true; + } else { + return false; + } + } + + update(data) { + let newdata = data.slice(); + if (this.primaryLeft) { + newdata[1] = this.cp(data[1], data[0]); + } else { + newdata[0] = this.cp(data[0], data[1]); + } + return newdata; + } } -function defaultEqualityConstraintBuilder() { // :-> EqualityConstraint - return makeEqualityConstraintBuilder(function(x,y) { return x===y; }, - function(xIn, xOut) { xOut = xIn; }); +function makeEqualityConstraintBuilder(eq, cp) { + // (T -> T -> bool) -> + // (T -> T -> _) -> + // (-> EqualityConstraint) + return function () { + return new EqualityConstraint(eq, cp); + }; } -class OperatorConstraint extends Constraint { // :Constraint - constructor(updaters, eq, cp, check) { - super(updaters.length); - this.ops = updaters; // :[[T] -> T] - this.eq = eq; // :T -> T -> bool - this.cp = cp; // :T -> T -> T - this.check = check; // :[T] -> bool - this.bound = updaters.length-1; // :index - } - - accepts(data) { - return super.accepts(data) && this.check(data); - } - - getDependencies() { - let deps = super.getDependencies(); - deps[this.bound] = true; - return deps; - } - - invert(take, give) { - if (take == this.bound && super.invert(take, give)) { - this.bound = give; - return true; - } else { - return false; - } +function defaultEqualityConstraintBuilder() { + // :-> EqualityConstraint + return makeEqualityConstraintBuilder( + function (x, y) { + return x === y; + }, + function (xIn, xOut) { + xOut = xIn; } + ); +} - update(data) { - let newdata = data.slice(); - newdata[this.bound] = this.cp(data[this.bound], this.ops[this.bound](data)); - return newdata; - } +class OperatorConstraint extends Constraint { + // :Constraint + constructor(updaters, eq, cp, check) { + super(updaters.length); + this.ops = updaters; // :[[T] -> T] + this.eq = eq; // :T -> T -> bool + this.cp = cp; // :T -> T -> T + this.check = check; // :[T] -> bool + this.bound = updaters.length - 1; // :index + } + + accepts(data) { + return super.accepts(data) && this.check(data); + } + + getDependencies() { + let deps = super.getDependencies(); + deps[this.bound] = true; + return deps; + } + + invert(take, give) { + if (take == this.bound && super.invert(take, give)) { + this.bound = give; + return true; + } else { + return false; + } + } + + update(data) { + let newdata = data.slice(); + newdata[this.bound] = this.cp(data[this.bound], this.ops[this.bound](data)); + return newdata; + } } diff --git a/src/model/constraint.ts b/src/model/constraint.ts new file mode 100644 index 0000000..d3ac379 --- /dev/null +++ b/src/model/constraint.ts @@ -0,0 +1,195 @@ +// data structure for representing relations +// this base class is a trivial relation that only cares about the number of members + +export type Eq = (dat1: T, dat2: T) => boolean; +export type Cp = (dat1: T, dat2: T) => T; + +export abstract class Constraint { + // :Constraint + + arity: number; // :index + constructor(arity: number) { + this.arity = arity; + } + + /////////////////////////////////////// + // methods that should be overridden // + /////////////////////////////////////// + + // checks if two pieces of data are equal for purposes of this constraint + abstract eq: Eq; // :T -> T -> bool + + // checks if the given data exactly satisfies the constraint + accepts(data: T[]) { + // :[T] -> bool + return this.checkArity(data); + } + + // returns an array representation of the dependency structure of the relation + // array values mean: + // false - free/independent/input + // true - bound/dependent/output + getDependencies() { + // :-> [bool] + return new Array(this.arity).fill(false); + } + + // exchange free/bound status of two positions + // returns true if successful + // note: we want all constraint classes to have the property that + // if invert(a,b) succeeds than a subsequent invert(b,a) will also succeed + // (we probably also want it to give back the original constraint) + invert(take: number, give: number) { + // : index -> index -> bool + if (take == give) { + return false; + } + if (take >= this.arity || give >= this.arity) { + return false; + } + return true; + } + + // change data in dependent/bound positions to fit the requirements of this constraint + update(data: T[]) { + // :[T] -> [T] + return data; + } + + ///////////////////// + // utility methods // + ///////////////////// + + checkArity(data: T[]) { + // :[T] -> bool + return this.arity == data.length; + } +} + +export class NonConstraint extends Constraint { + // :Constraint + + constructor(arity: number) { + super(arity); + } + + eq: Eq = (dat1: T, dat2: T) => true; +} + +// constraint specifying that two data must be equal +export class EqualityConstraint extends Constraint { + // :Constraint + // eq - notion of equality + // cp - "copy" function, sends data from 1st arg to 2nd arg + + cp: Cp; // :T -> T -> _ + eq: Eq; // :T -> T -> bool + primaryLeft: boolean; // :bool + + constructor(eq: Eq, cp: Cp) { + // :T -> T -> bool -> T -> T -> _ + super(2); + this.cp = cp; // T -> T -> _ + this.eq = eq; // T -> T -> bool + this.primaryLeft = true; // bool + } + + accepts(data: T[]) { + // :[T] -> bool + return super.accepts(data) && this.eq(data[0], data[1]); + } + + getDependencies() { + return [!this.primaryLeft, this.primaryLeft]; + } + + invert(take: number, give: number) { + if (super.invert(take, give)) { + this.primaryLeft = !this.primaryLeft; + return true; + } else { + return false; + } + } + + update(data: T[]) { + // :[T] -> [T] + let newdata = data.slice(); + if (this.primaryLeft) { + newdata[1] = this.cp(data[1], data[0]); + } else { + newdata[0] = this.cp(data[0], data[1]); + } + return newdata; + } +} + +export function makeEqualityConstraintBuilder(eq: Eq, cp: Cp) { + // (T -> T -> bool) -> + // (T -> T -> _) -> + // (-> EqualityConstraint) + return function () { + return new EqualityConstraint(eq, cp); + }; +} + +export function defaultEqualityConstraintBuilder() { + // :-> EqualityConstraint + return makeEqualityConstraintBuilder( + function (x, y) { + return x === y; + }, + function (xIn, xOut) { + return xIn; // TODO verify this is correct + } + ); +} + +type Ops = ((data: T[]) => any)[]; +type Check = (data: T[]) => boolean; + +export class OperatorConstraint extends Constraint { + // :Constraint + + ops: Ops; // :[[T] -> T] + eq: Eq; // :T -> T -> bool + cp: Cp; // :T -> T -> T + check: Check; // :[T] -> bool + bound: number; // :index + + constructor(updaters: Ops, eq: Eq, cp: Cp, check: Check) { + super(updaters.length); + this.ops = updaters; // :[[T] -> T] + this.eq = eq; // :T -> T -> bool + this.cp = cp; // :T -> T -> T + this.check = check; // :[T] -> bool + this.bound = updaters.length - 1; // :index + } + + accepts(data: T[]) { + // :[T] -> bool + return super.accepts(data) && this.check(data); + } + + getDependencies() { + let deps = super.getDependencies(); + deps[this.bound] = true; + return deps; + } + + invert(take: number, give: number) { + if (take == this.bound && super.invert(take, give)) { + this.bound = give; + return true; + } else { + return false; + } + } + + update(data: T[]) { + // :[T] -> [T] + let newdata = data.slice(); + newdata[this.bound] = this.cp(data[this.bound], this.ops[this.bound](data)); + return newdata; + } +} diff --git a/src/model/coord.ts b/src/model/coord.ts new file mode 100644 index 0000000..77eeb80 --- /dev/null +++ b/src/model/coord.ts @@ -0,0 +1,230 @@ +import { CENTER_X, CENTER_Y, settings } from "./settings"; +import { p } from "./sketch"; + +export class Coord { + x: number; + y: number; + + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } + + getX() { + return this.x; + } + getReal() { + return this.x; + } + getY() { + return this.y; + } + getImaginary() { + return this.y; + } + + getXPx() { + return this.x * settings.globalScale + CENTER_X; + } + getYPx() { + return CENTER_Y - this.y * settings.globalScale; + } + + getR() { + return p!.sqrt(this.x * this.x + this.y * this.y); + } + getTh() { + return p!.atan2(this.y, this.x); + } + getThDegrees() { + return p!.map(this.getTh(), -p!.PI, p!.PI, -180, 180); + } + + translate(vector: any) { + return new Coord(this.x + vector.getX(), this.y + vector.getY()); + } + + mut_translate(vector: any) { + this.x += vector.getX(); + this.y += vector.getY(); + return this; + } + + subtract(vector: any) { + return new Coord(this.x - vector.getX(), this.y - vector.getY()); + } + + mut_subtract(vector: any) { + this.x -= vector.getX(); + this.y -= vector.getY(); + return this; + } + + scale(factor: number) { + return new Coord(this.x * factor, this.y * factor); + } + + mut_scale(factor: number) { + this.x *= factor; + this.y *= factor; + return this; + } + + conjugate() { + return new Coord(this.x, -this.y); + } + + mut_conjugate() { + this.y *= -1; + return this; + } + + multiply(vector: any) { + let r = this.getR() * vector.getR(); + let th = this.getTh() + vector.getTh(); + + return new Coord(r * p!.cos(th), r * p!.sin(th)); + } + + mut_multiply(vector: any) { + let r = this.getR() * vector.getR(); + let th = this.getTh() + vector.getTh(); + this.x = r * p!.cos(th); + this.y = r * p!.sin(th); + return this; + } + + divide(vector: any) { + let r = this.getR() / vector.getR(); + let th = this.getTh() - vector.getTh(); + + return new Coord(r * p!.cos(th), r * p!.sin(th)); + } + + mut_divide(vector: any) { + let r = this.getR() / vector.getR(); + let th = this.getTh() - vector.getTh(); + this.x = r * p!.cos(th); + this.y = r * p!.sin(th); + return this; + } + + avg(vector: any) { + let x = (this.x + vector.getX()) / 2; + let y = (this.y + vector.getY()) / 2; + return new Coord(x, y); + } + + mut_avg(vector: any) { + this.x = (this.x + vector.getX()) / 2; + this.y = (this.y + vector.getY()) / 2; + return this; + } + + exp() { + let r = p!.exp(this.x); + let th = this.y; + return new Polar(r, th); + } + + mut_exp() { + return this.mut_sendTo(this.exp()); + } + + log(n = 0) { + let x = p!.log(this.getR()); + let y = this.getTh() + 2 * p!.PI * n; + return new Coord(x, y); + } + + mut_log(n = 0) { + return this.mut_sendTo(this.log(n)); + } + + copy() { + return new Coord(this.x, this.y); + } + + mut_sendTo(vector: any) { + this.x = vector.getX(); + this.y = vector.getY(); + return this; + } + + toString(precision = 1) { + // hack so that nfc doesn't display small floating point values wrong + let x = this.x; + if (p!.abs(x) < 0.01) { + x = 0; + } + let y = this.y; + if (p!.abs(y) < 0.01) { + y = 0; + } + + return "(" + p!.nfc(x, precision) + ", " + p!.nfc(y, precision) + "i)"; + } + + isOrigin() { + return this.x == 0 && this.y == 0; + } + + equals(vector: any) { + return this.x == vector.getX() && this.y == vector.getY(); + } + + distance(vector: any) { + return p!.dist(this.x, this.y, vector.x, vector.y); + } + + isNear(coord: Coord, tolerance: number) { + return this.distance(coord) < tolerance; + } + + isNearPx(coord: Coord, tolerancePx: number) { + return axisToPixel(this).distance(coord) < tolerancePx; + } +} + +// alternate constructor for building a Coord with polar components +export class Polar extends Coord { + constructor(r: number, th: number) { + super(r * p!.cos(th), r * p!.sin(th)); + } +} + +function pixelToAxis(coord: Coord) { + let x = coord.getX(); + let y = coord.getY(); + return new Coord((x - CENTER_X) / settings.globalScale, (CENTER_Y - y) / settings.globalScale); +} + +function axisToPixel(coord: Coord) { + let x = coord.getX(); + let y = coord.getY(); + return new Coord(x * settings.globalScale + CENTER_X, CENTER_Y - y * settings.globalScale); +} + +export function pixelToAxisX(coord: number) { + return (coord - CENTER_X) / settings.globalScale; +} + +export function pixelToAxisY(coord: number) { + return (CENTER_Y - coord) / settings.globalScale; +} + +export function axisToPixelX(coord: number) { + return coord * settings.globalScale + CENTER_X; +} + +export function axisToPixelY(coord: number) { + return CENTER_Y - coord * settings.globalScale; +} + +export function getMouse() { + return pixelToAxis(new Coord(p!.mouseX, p!.mouseY)); +} + +export function getMousePx() { + return new Coord(p!.mouseX, p!.mouseY); +} diff --git a/src/model/graph/edge.ts b/src/model/graph/edge.ts new file mode 100644 index 0000000..5926baa --- /dev/null +++ b/src/model/graph/edge.ts @@ -0,0 +1,90 @@ +import { Constraint } from "../constraint"; +import { Vertex } from "./vertex"; + +export class Edge { + vertices: Vertex[]; // :[Vertex] + constraint: Constraint; // :Constraint + id: number; // :index(graph.edges) + // :Edge + constructor(v: Vertex[], c: Constraint, id: number) { + this.vertices = v; + this.constraint = c; + this.id = id; + } + + // is the given position a bound/output position for this edge? + dependentAt(i: number) { + // :index(this.vertices) -> bool + return this.constraint.getDependencies()[i]; + } + + getFreeVertices() { + // :-> [Vertex] + let deps = this.constraint.getDependencies(); + let free = []; + for (let i = 0; i < this.vertices.length; i++) { + if (!deps[i]) { + free.push(this.vertices[i]); + } + } + return free; + } + + getBoundVertices() { + // :-> [Vertex] + let deps = this.constraint.getDependencies(); + let bound = []; + for (let i = 0; i < this.vertices.length; i++) { + if (deps[i]) { + bound.push(this.vertices[i]); + } + } + return bound; + } + + updateDependencies() { + // :-> void + let eid = this.id; + for (let v of this.vertices) { + v.deps = v.deps.filter(function (p) { + return p[1] != eid; + }); + } + let free = this.getFreeVertices().map(function (v) { + return [v.id, eid] as [number, number]; + }); + for (let v of this.getBoundVertices()) { + v.deps = v.deps.concat(free.slice()); + } + } + + // invert this edge's constraint by exchanging free/bound status of two positions + invert(take: number, give: number) { + // :index(this.vertices) -> index(this.vertices) -> bool + if (this.constraint.invert(take, give)) { + this.updateDependencies(); + return true; + } else { + return false; + } + } + + // use the constraint to update vertex data in bound positions + // returns true if any data was changed + // argument is the notion of equality by which changes are checked + update(eq = this.constraint.eq) { + // : (T -> T -> bool) -> bool + let changed = false; + let olddata = this.vertices.map(function (v) { + return v.value; + }); + let newdata = this.constraint.update(olddata); + for (let i = 0; i < this.vertices.length; i++) { + if (!eq(olddata[i], newdata[i])) { + this.vertices[i].value = newdata[i]; + changed = true; + } + } + return changed; + } +} diff --git a/src/model/graph/relGraph.ts b/src/model/graph/relGraph.ts new file mode 100644 index 0000000..100e32f --- /dev/null +++ b/src/model/graph/relGraph.ts @@ -0,0 +1,312 @@ +// graph structure that tracks free/bound dependency for multiple relations + +import { + Constraint, + defaultEqualityConstraintBuilder, + EqualityConstraint, + NonConstraint, +} from "../constraint"; +import { Edge } from "./edge"; +import { Vertex } from "./vertex"; + +// (technically this structure is a directed hypergraph, not strictly a graph) +console.log("graph.js loaded"); + +export class RelGraph { + // :RelGraph + // eq :T -> T -> bool - notion of equality for vertex data + // cp :T -> T -> void - function that copies data from 1st arg to 2nd arg + // TODO: default for cp doesn't really make sense + + vertices: Vertex[]; // :[Vertex] + edges: Edge[]; // :[Edge] + buildEqualityConstraint: () => EqualityConstraint; // :-> EqualityConstraint + history: number[]; // :[index(this.edges)] + + constructor(eq = defaultEqualityConstraintBuilder()) { + this.vertices = []; // :[Vertex] + this.edges = []; // :[Edge] + + // internal + this.buildEqualityConstraint = eq; // :-> EqualityConstraint + this.history = []; // :[index(this.edges)] + } + + // add a new vertex that is not connected to any relation + // returns the vertex + addFree(datum: T) { + // :T -> Vertex + let v = new Vertex(datum, this.vertices.length, []); + this.vertices.push(v); + return v; + } + + // add a set of vertices and a constraint relating them to the graph as an edge + // returns the edge, or null if given data do not satisfy given constraint + addRelated(data: T[], constraint: Constraint) { + // :[T] -> Constraint -> Edge + if (constraint.accepts(data)) { + // add all the needed vertices to the graph + let vs = []; // :[Vertex] + for (let i = 0; i < data.length; i++) { + vs.push(this.addFree(data[i])); + } + return this._addEdge(vs, constraint); + } else { + return null; + } + } + + getFreeVertices() { + // :-> [Vertex] + return this.vertices.filter(function (v) { + return v.isFree(); + }); + } + + getBoundVertices() { + // :-> [Vertex] + return this.vertices.filter(function (v) { + return v.isBound(); + }); + } + + // link two free vertices to the same datum by adding an equality constraint + // returns the new Edge, or null if unification could not be performed + unify(v1: Vertex, v2: Vertex) { + // :Vertex -> Vertex -> Edge + if (v1.isFree() && v2.isFree()) { + return this._unify(v1, v2); + } else { + return null; // can only unify free vertices + } + } + + // remove the most recently created unification involving vertex v + // returns true if disunification successful, false if not + // WARNING: repeated unification & disunification creates a small memory leak + disunify(v: Vertex) { + // :Vertex -> bool + return this._disunify(v); + } + + // returns a list of vertices that should be able to invert with the given bound vertex + getDepends(v: Vertex) { + // :Vertex -> [Vertex] + let vs = this.vertices; + return this._leafDeps(v).map(function (p) { + return vs[p[0]]; + }); + } + + // attempt to gain control of a vertex by giving up control of another vertex + // returns true if successful + invert(take: Vertex, give: Vertex) { + // :Vertex -> Vertex -> bool + // if vertex to take is already free, nothing to do + if (take.isFree()) { + return true; + } + // can't give up control of an already-bound vertex + if (give.isBound()) { + return false; + } + + return this._invert(take, give, true); + } + + // tell all the edges to run their constraints a given number of times + // if given a negative argument, run until equilibrium + // note multiple iterations are often necessary because of interdepenent contraints + // BE CAREFUL RUNNING INDEFINITE ITERATIONS, YOU PROBABLY DON'T WANT THIS + // IF YOUR GRAPH HAS ANY INSTABILITY OR MARGIN FOR ERROR + update(iters = 1) { + // : nat -> void + if (iters < 0) { + let changes = true; + while (changes) { + changes = false; + for (let e of this.edges) { + let changed = e.update(); + changes = changes || changed; + } + } + } else { + for (let i = 0; i < iters; i++) { + for (let e of this.edges) { + e.update(); + } + } + } + } + + ////////////////////// + // internal methods // + ////////////////////// + + // find free nodes in the given vertex's dependency tree + _leafDeps(v: Vertex, seen: number[] = []) { + // :Vertex -> [index(this.vertices)] + // -> [index(this.vertices) x index(this.edges)] + if (seen.includes(v.id)) { + // loop detection + return []; + } + let deps: [number, number][] = []; + for (const p of v.deps) { + if (this.vertices[p[0]].isFree()) { + deps.push(p); + } else { + seen.push(v.id); + deps = deps.concat(this._leafDeps(this.vertices[p[0]], seen)); + } + } + return deps; + } + + // find bound nodes in the given vertex's dependency tree + _intermedDeps(v: Vertex, seen: number[] = []) { + // :Vertex -> [index(this.vertices)] + // -> [index(this.vertices) x index(this.edges)] + if (seen.includes(v.id)) { + // loop detection + return []; + } + let deps: [number, number][] = []; + for (const p of v.deps) { + if (this.vertices[p[0]].isFree()) { + continue; + } else { + deps.push(p); + seen.push(v.id); + deps = deps.concat(this._intermedDeps(this.vertices[p[0]], seen)); + } + } + return deps; + } + + _addEdge(vs: Vertex[], constraint: Constraint) { + // :[Vertex] -> Constraint -> Edge + let e = new Edge(vs, constraint, this.edges.length); + this.edges.push(e); + e.updateDependencies(); + return e; + } + + _unify(v1: Vertex, v2: Vertex) { + // :Vertex -> Vertex -> Edge + this.history.unshift(this.edges.length); // history is LIFO + let e = new Edge([v1, v2], this.buildEqualityConstraint(), this.edges.length); // TODO: added extra parens to buildEqualityConstraint + this.edges.push(e); + e.updateDependencies(); + return e; + } + + _disunify(v: Vertex) { + // :Vertex -> bool + for (let i = 0; i < this.history.length; i++) { + let idxE = this.history[i]; // :index(this.edges) + let e = this.edges[idxE]; + if (!(e.constraint instanceof EqualityConstraint)) { + continue; + } + + // since we know e has an EqualityConstraint, pos can only be -1, 0, or 1 + let pos = e.vertices.indexOf(v); // :index(e.vertices) + + if (pos < 0) { + continue; + } // not the edge we're looking for + + let v2 = e.vertices[1 - pos]; // :Vertex + + // we've found the unification to remove: e relates v with v2 + + // remove the undone unification from the history + this.history.splice(i, 1); + + // we don't want to actually delete an edge since we're tracking stuff + // based on indexing into this.edges, so we'll just replace the + // equality constraint with a base constraint (this is the memory leak) + this.edges[idxE].constraint = new NonConstraint(2); + this.edges[idxE].updateDependencies(); + return true; + } + return false; + } + + _invert(take: Vertex, give: Vertex, recur: boolean) { + // :Vertex -> Vertex -> bool -> bool + // check argument validity + if (take.isFree() || give.isBound()) { + // should not get here: external method has already checked this, + // and recursive calls should be well-behaved + console.log("Warning: Tried to invert inappropriate vertices."); + return false; + } + + // see if a direct (single-step) dependency exists + let idxE = take.bindingEdge(give); // :index(this.edges) + if (idxE >= 0) { + let e = this.edges[idxE]; + let idxT = e.vertices.indexOf(take); // :index(e.vertices) + let idxG = e.vertices.indexOf(give); // :index(e.vertices) + if ( + idxT < 0 || + !e.dependentAt(idxT) || // "take" should be present and bound + idxG < 0 || + e.dependentAt(idxG) + ) { + // "give" should be present and free + // should not get here, edge disagrees with vertex + console.log("Warning: Relational graph is out of sync with itself."); + return false; + } + + if (e.invert(idxT, idxG)) { + return true; + } else { + // should not get here; issue is probably with e.constraint + console.log("Warning: Unexpected failure to perform inversion."); + return false; + } + } else if (recur) { + // try to find an intermediate vertex to invert through + for (const p of this._intermedDeps(take)) { + // see if this vertex can invert with the target in one step + if (this._invert(this.vertices[p[0]], give, false)) { + // success! now do the rest + if (this._invert(take, this.vertices[p[0]], true)) { + return true; + } else { + // recursive step failed, so undo the last step + if (this._invert(give, this.vertices[p[0]], false)) { + console.log("Warning: Failure during multi-step inversion."); + } else { + console.log( + "Warning: Could not restore original state" + " after failure during inversion." + ); + } + return false; + } + } else { + continue; + } + } + } + // no error, we just failed to find a way to invert as requested + return false; + } + + _printDeps() { + let str = "Dependencies:\n"; + for (let i = 0; i < this.vertices.length; i++) { + str = str + i + ": "; + for (let j = 0; j < this.vertices[i].deps.length; j++) { + str = str + "[" + this.vertices[i].deps[j] + "]"; + } + str = str + "\n"; + } + console.log(str); + } +} diff --git a/src/model/graph/vertex.ts b/src/model/graph/vertex.ts new file mode 100644 index 0000000..00cb7d7 --- /dev/null +++ b/src/model/graph/vertex.ts @@ -0,0 +1,32 @@ +export class Vertex { + // :Vertex + + value: T; // :T + id: number; // :index(graph.vertices) + deps: [number, number][]; // :[index(graph.vertices) x index(graph.edges)] + constructor(datum: T, id: number, deps: [number, number][]) { + this.value = datum; // :T + this.id = id; // :index(graph.vertices) + this.deps = deps; // :[index(graph.vertices) x index(graph.edges)] + } + + isFree() { + return this.deps.length == 0; + } + isBound() { + return this.deps.length > 0; + } + + // get index of the edge that captures this vertex's dependency on the given vertex + // (i.e. return the edge index paired with v.id in this.deps) + // returns -1 if this vertex is not dependent on the given vertex + bindingEdge(v: Vertex) { + // :Vertex -> index(graph.edges) + for (const p of this.deps) { + if (p[0] == v.id) { + return p[1]; + } + } + return -1; + } +} diff --git a/src/model/graphics.ts b/src/model/graphics.ts new file mode 100644 index 0000000..e0bcfe1 --- /dev/null +++ b/src/model/graphics.ts @@ -0,0 +1,85 @@ +import { Coord } from "./coord"; +import { LinkageOp } from "./linkages/linkageop"; +import { CENTER_X, CENTER_Y, settings } from "./settings"; +import { mainGraph, p } from "./sketch"; + +export function drawGrid() { + //background grid + for (let i = -30; i < 30; i++) { + p!.strokeWeight(1); + p!.stroke(75); + p!.noFill(); + p!.line(CENTER_X + i * settings.globalScale, 0, CENTER_X + i * settings.globalScale, p!.height); + p!.line(0, CENTER_Y + i * settings.globalScale, p!.width, CENTER_Y + i * settings.globalScale); + } + + //axes,unit circle + p!.noFill(); + p!.stroke(200); + p!.strokeWeight(1); + p!.line(0, CENTER_Y, p!.width, CENTER_Y); + p!.line(CENTER_X, 0, CENTER_X, p!.height); + p!.ellipse(CENTER_X, CENTER_Y, 2 * settings.globalScale, 2 * settings.globalScale); // unit circle + + //coordinate data + p!.textSize(15); + p!.textAlign(p!.CENTER, p!.CENTER); + for (let i = -30; i < 30; i++) { + p!.fill(150); + p!.noStroke(); + p!.ellipse(CENTER_X + i * settings.globalScale, CENTER_Y, 5, 5); + p!.ellipse(CENTER_X, CENTER_Y + i * settings.globalScale, 5, 5); + if (!settings.supressCoords) { + p!.text(i, CENTER_X + i * settings.globalScale, CENTER_Y - 16); + p!.text(-i + "i", CENTER_X - 20, CENTER_Y + i * settings.globalScale); + } + } +} + +export const CLEAR_BUTTON = new Coord(30, 30); +export const ADDER_BUTTON = new Coord(30, 60); +export const MULTR_BUTTON = new Coord(30, 90); +export const CONJ_BUTTON = new Coord(30, 120); +export const EXP_BUTTON = new Coord(30, 150); + +export function drawButtons() { + p!.textSize(15); + p!.textAlign(p!.LEFT, p!.CENTER); + + p!.noStroke(); + p!.fill(200); + p!.ellipse(30, 30, 20, 20); + p!.text("clear", 45, 30); + + p!.fill(30, 200, 255); + p!.ellipse(30, 60, 20, 20); + p!.text("adder", 45, 60); + + p!.fill(255, 100, 0); + p!.ellipse(30, 90, 20, 20); + p!.text("multiplier", 45, 90); + + p!.fill(30, 30, 200); + p!.ellipse(30, 120, 20, 20); + p!.text("conjugator", 45, 120); + + p!.fill(100, 100, 0); + p!.ellipse(30, 150, 20, 20); + p!.text("exponential", 45, 150); +} + +export function printToPlot() { + //On-canvas DRO for operators... + p!.textAlign(p!.CENTER, p!.CENTER); + p!.textSize(30); + + let h = p!.height - 40; + for (let i = 0; i < mainGraph!.edges.length; i++) { + if (mainGraph!.edges[i] instanceof LinkageOp) { + p!.fill(150); + p!.noStroke(); + h = h - 40; + p!.text(mainGraph!.edges[i].toString(), 200, h); + } + } +} diff --git a/src/model/linkages/linkagegraph.ts b/src/model/linkages/linkagegraph.ts new file mode 100644 index 0000000..184ce6c --- /dev/null +++ b/src/model/linkages/linkagegraph.ts @@ -0,0 +1,192 @@ +import { makeEqualityConstraintBuilder } from "../constraint"; +import { Coord } from "../coord"; +import { RelGraph } from "../graph/relGraph"; +import { Vertex } from "../graph/vertex"; +import { UPDATE_IDEAL } from "../settings"; +import { p } from "../sketch"; +import { ADDER, CONJUGATOR, EXPONENTIAL, LinkageOp, MULTIPLIER } from "./linkageop"; +import { LinkagePoint } from "./linkagepoint"; + +export class LinkageGraph extends RelGraph { + // :RelGraph + + focus: Vertex | null; + mode: number; + + // for reviewing history + frames: string[][]; + record: number; + + constructor(updateMode = UPDATE_IDEAL) { + let f_eq = function (z1: LinkagePoint, z2: LinkagePoint) { + return z1.equals(z2) && z1.delta.equals(z2.delta); + }; + let f_cp = function (zOld: LinkagePoint, zNew: LinkagePoint) { + let z = zOld.copy(); + z.mut_sendTo(zNew); + z.delta.mut_avg(zNew.delta); + return z; + }; + let eq = makeEqualityConstraintBuilder(f_eq, f_cp); + // if (updateMode == UPDATE_ITERATIVE) { + // eq = makeIterativeComplexEqualityConstraintBuilder(f_eq, f_cp); + // } + super(eq); + + this.focus = null; + this.mode = updateMode; + + // for reviewing history + this.frames = []; + this.record = 0; + } + + // use this instead of addRelated + addOperation(type: number) { + let vs: Vertex[] = []; + if (type == ADDER) { + vs.push(this.addFreeXY(0, 0)); + vs.push(this.addFreeXY(0, 0)); + vs.push(this.addFreeXY(0, 0)); + } else if (type == MULTIPLIER) { + vs.push(this.addFreeXY(1, 0)); + vs.push(this.addFreeXY(1, 0)); + vs.push(this.addFreeXY(1, 0)); + } else if (type == CONJUGATOR) { + vs.push(this.addFreeXY(0, 1)); + vs.push(this.addFreeXY(0, -1)); + } else if (type == EXPONENTIAL) { + vs.push(this.addFreeXY(0, p!.PI)); + vs.push(this.addFreeXY(-1, 0)); + } else { + return null; + } + + let e = new LinkageOp(vs, type, this.mode, this.edges.length); + this.edges.push(e); + e.updateDependencies(); + return e; + } + + addFreeXY(x: number, y: number) { + let z = super.addFree(new LinkagePoint(x, y)); + z.value.canDrag = function () { + return z.isFree(); + }; + return z; + } + + // must provide the hidden vertex in order to resume display + disunify(v: Vertex) { + if (this._disunify(v)) { + v.value.hidden = false; + return true; + } else { + return false; + } + } + + applyDifferential(delta: Coord) { + for (let v of this.vertices) { + v.value.mut_applyDifferential(delta); + } + } + + display(reversing = false) { + for (const e of this.edges) { + if (e instanceof LinkageOp) { + e.display(); + } + } + for (const v of this.vertices) { + if (reversing && this.focus && this.getDepends(this.focus).includes(v)) { + v.value.display(reversing); + } else { + // need more than 2 states! want depends to look more distinct + // from other free vertices + v.value.display(); + } + } + } + + // returns the first free vertex close to the cursor + // if none, returns a bound vertex close to the cursor + // if no vertices are close to the cursor, returns null + findMouseover() { + let result: Vertex | null = null; + for (const v of this.vertices) { + if (v.value.checkMouseover()) { + if (v.isFree()) { + return v; + } else { + result = v; + } + } + } + return result; + } + + startReversal() { + this.focus = this.findMouseover(); + if (this.focus && this.focus.isBound()) { + return true; + } else { + this.focus = null; + return false; + } + } + + cancelReversal() { + this.focus = null; + } + + completeReversal() { + if (this.focus) { + let target = this.findMouseover(); + if (target && this.invert(this.focus, target)) { + this.focus = null; + } else { + this.cancelReversal(); + } + } + } + + findUnify() { + let v1 = this.findMouseover(); + if (v1) { + v1.value.hidden = true; + let v2 = this.findMouseover(); + v1.value.hidden = false; + if (v2 && this.unify(v1, v2)) { + v2.value.hidden = true; + return true; + } + } + return false; + } + + saveFrame() { + if (this.record > 0) { + let fr: string[] = []; + for (const v of this.vertices) { + fr.push(v.value.delta.toString()); + } + this.frames.push(fr); + this.record--; + } + } + + update(iters = 1) { + if (this.record > 0) { + this.saveFrame(); + } + super.update(iters); + } + + recordNext(nframes: number, clearOld = false) { + this.record = nframes; + if (clearOld) { + this.frames = []; + } + } +} diff --git a/src/model/linkages/linkageop.ts b/src/model/linkages/linkageop.ts new file mode 100644 index 0000000..b6d595a --- /dev/null +++ b/src/model/linkages/linkageop.ts @@ -0,0 +1,209 @@ +import { Constraint, NonConstraint } from "../constraint"; +import { Coord, Polar } from "../coord"; +import { Edge } from "../graph/edge"; +import { Vertex } from "../graph/vertex"; +import { + DifferentialComplexAdder, + DifferentialComplexConjugator, + DifferentialComplexExponent, + DifferentialComplexMultiplier, + IdealComplexAdder, + IdealComplexConjugator, + IdealComplexExponent, + IdealComplexMultiplier, + IterativeComplexAdder, + IterativeComplexConjugator, + IterativeComplexExponent, + IterativeComplexMultiplier, +} from "../operations"; +import { + CENTER_X, + CENTER_Y, + iterations, + searchSize, + settings, + UPDATE_DIFFERENTIAL, + UPDATE_IDEAL, + UPDATE_ITERATIVE, +} from "../settings"; +import { p } from "../sketch"; +import { LinkagePoint } from "./linkagepoint"; + +// operator type +export const ADDER = 0; +export const MULTIPLIER = 1; +export const CONJUGATOR = 2; +export const EXPONENTIAL = 3; + +// these should be in settings.js +export const STEP_SIZE = searchSize; +export const ITERATIONS = iterations; + +export class LinkageOp extends Edge { + // :Edge + + type: number; // :operator type + hidden: boolean; // :whether to draw this operator + + constructor(v: Vertex[], type: number, mode: number, id: number) { + let c = null; + switch (mode) { + case UPDATE_IDEAL: + switch (type) { + case ADDER: + c = new IdealComplexAdder(); + break; + case MULTIPLIER: + c = new IdealComplexMultiplier(); + break; + case CONJUGATOR: + c = new IdealComplexConjugator(); + break; + case EXPONENTIAL: + c = new IdealComplexExponent(false); + break; + default: + console.log("Warning: Unsupported Operator Type"); + c = new NonConstraint(2); + } + break; + case UPDATE_ITERATIVE: + switch (type) { + case ADDER: + c = new IterativeComplexAdder(STEP_SIZE, ITERATIONS); + break; + case MULTIPLIER: + c = new IterativeComplexMultiplier(STEP_SIZE, ITERATIONS); + break; + case CONJUGATOR: + c = new IterativeComplexConjugator(STEP_SIZE, ITERATIONS); + break; + case EXPONENTIAL: + c = new IterativeComplexExponent(STEP_SIZE, ITERATIONS); + break; + default: + console.log("Warning: Unsupported Operator Type"); + c = new NonConstraint(2); + } + break; + case UPDATE_DIFFERENTIAL: + switch (type) { + case ADDER: + c = new DifferentialComplexAdder(); + break; + case MULTIPLIER: + c = new DifferentialComplexMultiplier(); + break; + case CONJUGATOR: + c = new DifferentialComplexConjugator(); + break; + case EXPONENTIAL: + c = new DifferentialComplexExponent(); + break; + default: + console.log("Warning: Unsupported Operator Type"); + c = new NonConstraint(2); + } + break; + default: + console.log("Warning: Invalid Update Mode"); + c = new NonConstraint(2); + } + super(v, c as Constraint, id); + this.type = type; + + this.hidden = false; + } + + // display connecting lines related to this operation + // note: does not draw the vertices themselves + display() { + if (this.hidden) { + return; + } + p!.noFill(); + p!.strokeWeight(1); + + if (this.type == ADDER) { + p!.stroke(30, 200, 255); + p!.beginShape(); + p!.vertex(CENTER_X, CENTER_Y); + p!.vertex(this.vertices[0].value.getXPx(), this.vertices[0].value.getYPx()); + p!.vertex(this.vertices[2].value.getXPx(), this.vertices[2].value.getYPx()); + p!.vertex(this.vertices[1].value.getXPx(), this.vertices[1].value.getYPx()); + p!.vertex(CENTER_X, CENTER_Y); + p!.endShape(); + } else if (this.type == MULTIPLIER) { + p!.stroke(255, 0, 0); + p!.line(CENTER_X, CENTER_Y, this.vertices[2].value.getXPx(), this.vertices[2].value.getYPx()); + p!.stroke(255, 100, 0); + p!.line(CENTER_X, CENTER_Y, this.vertices[0].value.getXPx(), this.vertices[0].value.getYPx()); + p!.line(CENTER_X, CENTER_Y, this.vertices[1].value.getXPx(), this.vertices[1].value.getYPx()); + } else if (this.type == CONJUGATOR) { + p!.stroke(30, 30, 200); + p!.line(CENTER_X, CENTER_Y, this.vertices[0].value.getXPx(), this.vertices[0].value.getYPx()); + p!.line(CENTER_X, CENTER_Y, this.vertices[1].value.getXPx(), this.vertices[1].value.getYPx()); + p!.line( + this.vertices[0].value.getXPx(), + this.vertices[0].value.getYPx(), + this.vertices[1].value.getXPx(), + this.vertices[1].value.getYPx() + ); + } else if (this.type == EXPONENTIAL) { + p!.stroke(200, 100, 200); + if (this.vertices[0].value.dragging || this.vertices[1].value.dragging) { + // still need to work out correct values for a and b + let b = this.vertices[0].value.getX() / this.vertices[0].value.getY(); + let a = 1; + let theta = -12 * p!.PI; + p!.beginShape(); + for (let i = 0; i < 12000; i++) { + theta += p!.PI / 100; + if (b * theta < 5) { + // don't try to plot coords further out than e^5 + let polar = new Polar(a * p!.exp(b * theta), theta); + p!.vertex(polar.getXPx(), polar.getYPx()); + } + } + p!.endShape(); + } + p!.line(this.vertices[0].value.getXPx(), 0, this.vertices[0].value.getXPx(), p!.height); + p!.ellipse( + CENTER_X, + CENTER_Y, + 2 * settings.globalScale * this.vertices[1].value.getR(), + 2 * settings.globalScale * this.vertices[1].value.getR() + ); + } else { + // bad + } + } + + toString() { + if (this.type == ADDER) { + return ( + this.vertices[0].value.toString(0) + + " + " + + this.vertices[1].value.toString(0) + + " = " + + this.vertices[2].value.toString(0) + ); + } else if (this.type == MULTIPLIER) { + return ( + this.vertices[0].value.toString(0) + + " x " + + this.vertices[1].value.toString(0) + + " = " + + this.vertices[2].value.toString(0) + ); + } else if (this.type == CONJUGATOR) { + return ( + "conj" + this.vertices[0].value.toString(0) + " = " + this.vertices[1].value.toString(0) + ); + } else if (this.type == EXPONENTIAL) { + return ( + "exp" + this.vertices[0].value.toString(0) + " = " + this.vertices[1].value.toString(0) + ); + } + } +} diff --git a/src/model/linkages/linkagepoint.ts b/src/model/linkages/linkagepoint.ts new file mode 100644 index 0000000..73a229a --- /dev/null +++ b/src/model/linkages/linkagepoint.ts @@ -0,0 +1,117 @@ +import { Coord, getMouse, getMousePx } from "../coord"; +import { Vertex } from "../graph/vertex"; +import { settings } from "../settings"; +import { p } from "../sketch"; + +export class LinkagePoint extends Coord { + canDrag: any; + dragging: boolean; + hidden: boolean; + delta: Coord; + + constructor(x: number, y: number) { + // position of point + super(x, y); + + // is this point user-moveable? + this.canDrag = null; + + this.dragging = false; + this.hidden = false; + + this.delta = new Coord(0, 0); + } + + copy(parent: Vertex | null = null) { + let z = new LinkagePoint(this.x, this.y); + if (parent) { + z.canDrag = function () { + return parent.isFree(); + }; + } else { + z.canDrag = this.canDrag; + } + z.dragging = this.dragging; + z.hidden = this.hidden; + z.delta = this.delta.copy(); + return z; + } + + checkMouseover() { + // :-> bool + if (this.hidden) { + return false; + } + return this.isNearPx(getMousePx(), 25); + } + + notifyClick() { + // :-> bool + if (this.hidden) { + return false; + } + if (this.canDrag) { + // only call drag check method if it exists + if (this.canDrag() && this.checkMouseover()) { + this.dragging = true; + this.delta = new Coord(1, 0); + } + } else if (this.checkMouseover()) { + this.dragging = true; + this.delta = new Coord(1, 0); + } + return this.dragging; + } + + notifyRelease() { + this.dragging = false; + this.delta = new Coord(0, 0); + } + + sendToMouse() { + if (this.dragging) { + this.mut_sendTo(getMouse()); + } + } + + mut_applyDifferential(delta: Coord) { + this.mut_translate(this.delta.multiply(delta)); + } + + _drawNode(reversing = false) { + p!.noStroke(); + if (reversing) { + p!.fill(255); + } else { + p!.fill(100, 150, 255); + } + p!.ellipse(this.getXPx(), this.getYPx(), 15, 15); + } + + _drawRing() { + p!.noFill(); + p!.stroke(255, 200); + p!.strokeWeight(3); + p!.ellipse(this.getXPx(), this.getYPx(), 20, 20); + } + + display(reversing = false) { + // :bool -> void + if (this.hidden) { + return; + } + + this._drawNode(reversing); + + if (this.canDrag && this.canDrag()) { + // check for method existing, then call + this._drawRing(); + } + + if (settings.showDifferentials) { + p!.fill(255); + p!.noStroke(); + p!.text(this.delta.toString(), this.getXPx() + 10, this.getYPx() - 20); + } + } +} diff --git a/src/model/operations.ts b/src/model/operations.ts new file mode 100644 index 0000000..1dc0855 --- /dev/null +++ b/src/model/operations.ts @@ -0,0 +1,495 @@ +////////////////////////////////////////////////////////////////////////////////// +// "ideal" constraints that simply perform the relevant calculation all at once // + +import { Cp, Eq, EqualityConstraint, OperatorConstraint } from "./constraint"; +import { Coord, Polar } from "./coord"; +import { LinkagePoint } from "./linkages/linkagepoint"; +import { p } from "./sketch"; + +////////////////////////////////////////////////////////////////////////////////// +export class IdealComplexAdder extends OperatorConstraint { + // :Constraint + constructor() { + let subL = function (d: Coord[]) { + return d[2].subtract(d[1]); + }; + let subR = function (d: Coord[]) { + return d[2].subtract(d[0]); + }; + let add = function (d: Coord[]) { + return d[0].translate(d[1]); + }; + let eq = function (z1: Coord, z2: Coord) { + return z1.equals(z2); + }; + let cp = function (zOld: Coord, zNew: Coord) { + return zOld.copy().mut_sendTo(zNew); + }; + let check = function (d: Coord[]) { + return eq(add(d), d[2]); + }; + super([subL, subR, add], eq, cp, check); + } +} + +export class IdealComplexMultiplier extends OperatorConstraint { + // :Constraint + constructor() { + let divL = function (d: Coord[]) { + return d[2].divide(d[1]); + }; + let divR = function (d: Coord[]) { + return d[2].divide(d[0]); + }; + let mult = function (d: Coord[]) { + return d[0].multiply(d[1]); + }; + let eq = function (z1: Coord, z2: Coord) { + return z1.equals(z2); + }; + let cp = function (zOld: Coord, zNew: Coord) { + return zOld.copy().mut_sendTo(zNew); + }; + let check = function (d: Coord[]) { + return eq(mult(d), d[2]); + }; + super([divL, divR, mult], eq, cp, check); + } + + accepts(data: Coord[]) { + // if constraint is in a division mode, check for 0 divisor + if ((this.bound == 0 && data[1].isOrigin()) || (this.bound == 1 && data[0].isOrigin())) { + return false; + } else { + return super.accepts(data); + } + } +} + +export class IdealComplexConjugator extends OperatorConstraint { + // :Constraint + constructor() { + let conjL = function (d: Coord[]) { + return d[1].conjugate(); + }; + let conjR = function (d: Coord[]) { + return d[0].conjugate(); + }; + let eq = function (z1: Coord, z2: Coord) { + return z1.equals(z2); + }; + let cp = function (zOld: Coord, zNew: Coord) { + return zOld.copy().mut_sendTo(zNew); + }; + let check = function (d: Coord[]) { + return eq(conjL(d), d[0]); + }; + super([conjL, conjR], eq, cp, check); + } +} + +export class IdealComplexExponent extends OperatorConstraint { + // :Constraint + constructor(alwaysUsePrincipal = true) { + let zlog = function (d: Coord[]) { + let n = IdealComplexExponent._nearestN(d[1].log(0), d[0]); + return d[1].log(n); + }; + if (alwaysUsePrincipal) { + zlog = function (d) { + return d[1].log(0); + }; + } + let zexp = function (d: Coord[]) { + return d[0].exp(); + }; + let eq = function (z1: Coord, z2: Coord) { + return z1.equals(z2); + }; + let cp = function (zOld: Coord, zNew: Coord) { + return zOld.copy().mut_sendTo(zNew); + }; + let check = function (d: Coord[]) { + return eq(zexp(d), d[1]); + }; + super([zlog, zexp], eq, cp, check); + } + + static _nearestN(principal: Coord, guess: Coord) { + let ySol = principal.getY(); + let yGuess = guess.getY(); + let circles = 0; + let diff = ySol - yGuess; + while (p!.abs(diff) > p!.PI) { + let yPos = ySol + 2 * p!.PI; + let yNeg = ySol - 2 * p!.PI; + if (p!.abs(yGuess - yPos) < p!.abs(yGuess - yNeg)) { + circles++; + ySol = yPos; + } else { + circles--; + ySol = yNeg; + } + diff = ySol - yGuess; + } + return circles; + } +} + +/////////////////////////////////////////////////////////////////////// +// "naive" constraints that update iteratively at a given resolution // +/////////////////////////////////////////////////////////////////////// + +export class IterativeComplexAdder extends IdealComplexAdder { + // :Constraint + + stepSize: number; + iters: number; + + constructor(stepSize: number, iters: number) { + super(); + this.stepSize = stepSize; // :number + this.iters = iters; // :nat + } + + update(data: Coord[]) { + for (let i = 0; i < this.iters; i++) { + data = this.iterate(data); + } + return data; + } + + iterate(data: Coord[]) { + switch (this.bound) { + case 0: + data[0] = this.iterateDiff(data[2], data[1], data[0]); + break; + case 1: + data[1] = this.iterateDiff(data[2], data[0], data[1]); + break; + case 2: + data[2] = this.iterateSum(data[0], data[1], data[2]); + break; + default: + // should not get here + } + return data; + } + + iterateSum(z1: Coord, z2: Coord, guess: Coord) { + let sum = z1.translate(z2); + if (sum.isNear(guess, this.stepSize)) { + return guess.mut_sendTo(sum); + } else { + let theta = sum.subtract(guess).getTh(); + return guess.mut_translate(new Polar(this.stepSize, theta)); + } + } + + iterateDiff(z: Coord, zsub: Coord, guess: Coord) { + let diff = z.subtract(zsub); + if (diff.isNear(guess, this.stepSize)) { + return guess.mut_sendTo(diff); + } else { + let theta = diff.subtract(guess).getTh(); + return guess.mut_translate(new Polar(this.stepSize, theta)); + } + } +} + +export class IterativeComplexMultiplier extends IdealComplexMultiplier { + // :Constraint + + stepSize: number; + iters: number; + + constructor(stepSize: number, iters: number) { + super(); + this.stepSize = stepSize; // :number + this.iters = iters; // :nat + } + + update(data: Coord[]) { + for (let i = 0; i < this.iters; i++) { + data = this.iterate(data); + } + return data; + } + + iterate(data: Coord[]) { + switch (this.bound) { + case 0: + data[0] = this.iterateQuot(data[2], data[1], data[0]); + break; + case 1: + data[1] = this.iterateQuot(data[2], data[0], data[1]); + break; + case 2: + data[2] = this.iterateProd(data[0], data[1], data[2]); + break; + default: + // should not get here + } + return data; + } + + iterateProd(z1: Coord, z2: Coord, guess: Coord) { + let prod = z1.multiply(z2); + if (prod.isNear(guess, this.stepSize)) { + return guess.mut_sendTo(prod); + } else { + let theta = prod.subtract(guess).getTh(); + return guess.mut_translate(new Polar(this.stepSize, theta)); + } + } + + iterateQuot(z: Coord, zdiv: Coord, guess: Coord) { + // if dividing by 0, just move the quotient towards infinity + if (zdiv.isOrigin()) { + return guess.mut_translate(new Polar(this.stepSize, guess.getTh())); + } + + let quot = z.divide(zdiv); + if (quot.isNear(guess, this.stepSize)) { + return guess.mut_sendTo(quot); + } else { + let theta = quot.subtract(guess).getTh(); + return guess.mut_translate(new Polar(this.stepSize, theta)); + } + } +} + +export class IterativeComplexConjugator extends IdealComplexConjugator { + // :Constraint + + stepSize: number; + iters: number; + constructor(stepSize: number, iters: number) { + super(); + this.stepSize = stepSize; // :number + this.iters = iters; // :nat + } + + update(data: Coord[]) { + for (let i = 0; i < this.iters; i++) { + data = this.iterate(data); + } + return data; + } + + iterate(data: Coord[]) { + data[this.bound] = this.iterateConj(data[1 - this.bound], data[this.bound]); + return data; + } + + iterateConj(z: Coord, guess: Coord) { + let conj = z.conjugate(); + if (conj.isNear(guess, this.stepSize)) { + return guess.mut_sendTo(conj); + } else { + let theta = conj.subtract(guess).getTh(); + return guess.mut_translate(new Polar(this.stepSize, theta)); + } + } +} + +export class IterativeComplexExponent extends IdealComplexExponent { + // :Constraint + + stepSize: number; + iters: number; + + constructor(stepSize: number, iters: number) { + super(false); + this.stepSize = stepSize; // :number + this.iters = iters; // :nat + } + + update(data: Coord[]) { + for (let i = 0; i < this.iters; i++) { + data = this.iterate(data); + } + return data; + } + + iterate(data: Coord[]) { + switch (this.bound) { + case 0: + data[0] = this.iterateLog(data[1], data[0]); + break; + case 1: + data[1] = this.iterateExp(data[0], data[1]); + break; + default: + // should not get here + } + return data; + } + + iterateLog(z: Coord, guess: Coord) { + let zlog = z.log(IdealComplexExponent._nearestN(z.log(0), guess)); + if (zlog.isNear(guess, this.stepSize)) { + return guess.mut_sendTo(zlog); + } else { + let theta = zlog.subtract(guess).getTh(); + return guess.mut_translate(new Polar(this.stepSize, theta)); + } + } + + iterateExp(z: Coord, guess: Coord) { + let zexp = z.exp(); + if (zexp.isNear(guess, this.stepSize)) { + return guess.mut_sendTo(zexp); + } else { + let theta = zexp.subtract(guess).getTh(); + return guess.mut_translate(new Polar(this.stepSize, theta)); + } + } +} + +export class IterativeComplexEqualityConstraint extends EqualityConstraint { + stepSize: number; + iters: number; + + constructor(eq: Eq, cp: Cp, stepSize: number, iters: number) { + super(eq, cp); + this.stepSize = stepSize; + this.iters = iters; + } + + update(data: Coord[]) { + for (let i = 0; i < this.iters; i++) { + let newdata = data.slice(); + if (this.primaryLeft) { + newdata[1] = this.iterate(data[0], data[1]); + } else { + newdata[0] = this.iterate(data[1], data[0]); + } + data = newdata; + } + return data; + } + + iterate(z: Coord, guess: Coord) { + if (z.isNear(guess, this.stepSize)) { + return guess.mut_sendTo(z); + } else { + let theta = z.subtract(guess).getTh(); + return guess.mut_translate(new Polar(this.stepSize, theta)); + } + } +} + +export function makeIterativeComplexEqualityConstraintBuilder( + eq: Eq, + cp: Cp, + STEP_SIZE: number, + ITERATIONS: number +) { + return function () { + return new IterativeComplexEqualityConstraint(eq, cp, STEP_SIZE, ITERATIONS); + }; +} + +//////////////////////////////////////////////////////////////////////////// +// "differential" constraints that update with automatic differentiation // +// (note that this algorithm is not compatible with basic Coord class) // +// right now calling update on this constraint ONLY updates differentials // +//////////////////////////////////////////////////////////////////////////// + +export class DifferentialComplexAdder extends IdealComplexAdder { + // :Constraint + constructor() { + super(); + } + + update(data: LinkagePoint[]) { + switch (this.bound) { + case 0: + data[0].delta = data[2].delta.subtract(data[1].delta); + break; + case 1: + data[1].delta = data[2].delta.subtract(data[0].delta); + break; + case 2: + data[2].delta = data[0].delta.translate(data[1].delta); + break; + default: + // should not get here + } + return data; + } +} + +export class DifferentialComplexMultiplier extends IdealComplexMultiplier { + // :Constraint + constructor() { + super(); + } + + update(data: LinkagePoint[]) { + let fprimeg, fgprime, gsquare; + switch (this.bound) { + case 0: + fprimeg = data[2].delta.multiply(data[1]); + fgprime = data[2].multiply(data[1].delta); + gsquare = data[1].multiply(data[1]); + data[0].delta = fprimeg.subtract(fgprime).divide(gsquare); + break; + case 1: + fprimeg = data[2].delta.multiply(data[0]); + fgprime = data[2].multiply(data[0].delta); + gsquare = data[0].multiply(data[0]); + data[1].delta = fprimeg.subtract(fgprime).divide(gsquare); + break; + case 2: + fprimeg = data[0].delta.multiply(data[1]); + fgprime = data[0].multiply(data[1].delta); + data[2].delta = fprimeg.translate(fgprime); + break; + default: + // should not get here + } + return data; + } +} + +export class DifferentialComplexConjugator extends IdealComplexConjugator { + // :Constraint + constructor() { + super(); + } + + update(data: LinkagePoint[]) { + // we need the conjugate at the next step, not here! + // delta gets multiplied by the actual movement of the input, + // but there's no factor we can give here to do that + let deltaIn = data[1 - this.bound].delta; + data[this.bound].delta = new Coord(deltaIn.getX(), deltaIn.getY() * -1); + return data; + } +} + +export class DifferentialComplexExponent extends IdealComplexExponent { + // :Constraint + + one: Coord; + constructor() { + super(); + this.one = new Coord(1, 0); + } + + update(data: LinkagePoint[]) { + switch (this.bound) { + case 0: + data[0].delta = data[1].delta.divide(data[1]); + break; + case 1: + data[1].delta = data[1].multiply(data[0].delta); + break; + default: + // should not get here + } + return data; + } +} diff --git a/src/model/settings.ts b/src/model/settings.ts new file mode 100644 index 0000000..69d192d --- /dev/null +++ b/src/model/settings.ts @@ -0,0 +1,60 @@ +import { Vertex } from "./graph/vertex"; +import { LinkagePoint } from "./linkages/linkagepoint"; + +export const UPDATE_IDEAL = 0; +export const UPDATE_ITERATIVE = 1; +export const UPDATE_DIFFERENTIAL = 2; + +////////////////////////////////// + +export const UPDATE_MODE = UPDATE_DIFFERENTIAL; + +//background color and debug variable +export const indicator = 50; + +//how far to look in each direction +export const searchSize = 0.02; + +//how many iterations to run before updating +export const iterations = 1; + +// how many adjustment iterations to run after automatic differentiation +export const DIFF_ITERS = 1; + +//extra number of loops for updating positions, helps with rigidity +export const updateCycles = 5; + +//center coords. +export const CENTER_X = 650; +export const CENTER_Y = 450; + +//global scale (standard, 50px = 1 unit) +export const DEFAULT_SCALE = 50; + +export const settings = { + globalScale: DEFAULT_SCALE, + + //double tap reference (sketch level) + tappedOnce: false, + currentTime: -1, + doubleTapTimer: 300, + + //press and hold references + pressAndHold: false, + timerStart: -1, + holdLength: 700, + + // mode-switch boolean, for going into state of switching a dependency + reversingOperator: false, + + indicatorFlash: false, + + //turns off cartesian coordinates when focusing on polar coordinates + supressCoords: false, + + // show little derivatives near nodes + showDifferentials: true, + + // activeVertex + activeVertex: null as null | Vertex, +}; diff --git a/src/model/sketch.ts b/src/model/sketch.ts new file mode 100644 index 0000000..407d80a --- /dev/null +++ b/src/model/sketch.ts @@ -0,0 +1,154 @@ +import p5 from "p5"; +import { getMouse, getMousePx } from "./coord"; +import { + ADDER_BUTTON, + CLEAR_BUTTON, + CONJ_BUTTON, + drawButtons, + drawGrid, + EXP_BUTTON, + MULTR_BUTTON, + printToPlot, +} from "./graphics"; +import { LinkageGraph } from "./linkages/linkagegraph"; +import { ADDER, CONJUGATOR, EXPONENTIAL, MULTIPLIER } from "./linkages/linkageop"; +import { LinkagePoint } from "./linkages/linkagepoint"; +import { indicator, settings, updateCycles, UPDATE_DIFFERENTIAL, UPDATE_MODE } from "./settings"; + +export let mainGraph: LinkageGraph | null = null; +export let p: p5 | null = null; +export const activeVertex = null; + +function setup(p5: p5, canvasParentRef: Element) { + p = p5; + p5.createCanvas(1600, 900).parent(canvasParentRef); + mainGraph = new LinkageGraph(UPDATE_MODE); +} + +function draw(p: p5) { + //manage double tap + if (settings.tappedOnce) { + if (p!.millis() - settings.currentTime > settings.doubleTapTimer) { + settings.tappedOnce = false; + } + } + + //look for bind opportunities + if (settings.pressAndHold) { + if (p!.millis() - settings.timerStart > settings.holdLength) { + settings.indicatorFlash = mainGraph!.findUnify(); + } + } + + mainGraph!.update(updateCycles); + + p!.background(indicator); + + drawGrid(); + drawButtons(); + + //display mode while alternative dependency... + if (settings.reversingOperator) { + p!.background(0, 150); + } + + mainGraph!.display(settings.reversingOperator); + + //digital readout for existing operators + printToPlot(); + + if (settings.indicatorFlash) { + p!.background(0); + settings.indicatorFlash = false; + } + + //make tutorials run on top of this interactive canvas... + // runTutorial(); +} + +console.log("setup defined"); + +// TODO: commented this out since myLevels is not defined +// function keyPressed() { +// //n for 'next' +// if (p!.keyCode === 78 && p!.level != myLevels.length - 1) { +// level++; +// } + +// //p for 'previous' +// if (keyCode === 80 && level != 0) { +// level--; +// } +// } + +// let activeVertex: Vertex | null = null; + +export function touchStarted() { + if (settings.reversingOperator) { + mainGraph!.completeReversal(); + settings.reversingOperator = false; + return; + } + + if (CLEAR_BUTTON.isNear(getMousePx(), 10)) { + mainGraph = new LinkageGraph(UPDATE_MODE); + return; + } + if (ADDER_BUTTON.isNear(getMousePx(), 10)) { + mainGraph!.addOperation(ADDER); + return; + } + if (MULTR_BUTTON.isNear(getMousePx(), 10)) { + mainGraph!.addOperation(MULTIPLIER); + return; + } + if (CONJ_BUTTON.isNear(getMousePx(), 10)) { + mainGraph!.addOperation(CONJUGATOR); + return; + } + if (EXP_BUTTON.isNear(getMousePx(), 10)) { + mainGraph!.addOperation(EXPONENTIAL); + return; + } + + settings.pressAndHold = true; + settings.timerStart = p!.millis(); + + if (!settings.tappedOnce) { + settings.tappedOnce = true; + settings.currentTime = p!.millis(); + } else { + settings.reversingOperator = mainGraph!.startReversal(); + settings.tappedOnce = false; + } + + settings.activeVertex = mainGraph!.findMouseover(); + if (settings.activeVertex && settings.activeVertex.value instanceof LinkagePoint) { + settings.activeVertex.value.notifyClick(); // should probably check this returned true + } + + //update tutorial... + // tutorialClick(); +} + +function touchMoved() { + settings.pressAndHold = false; + if (settings.activeVertex) { + if (mainGraph!.mode == UPDATE_DIFFERENTIAL) { + let mouse = getMouse(); + mainGraph!.applyDifferential(mouse.subtract(settings.activeVertex.value)); + } else { + settings.activeVertex.value.sendToMouse(); + } + } + return false; +} + +function touchEnded() { + settings.pressAndHold = false; + if (settings.activeVertex) { + settings.activeVertex.value.notifyRelease(); + settings.activeVertex = null; + // mainGraph.update(updateCycles*500); + } +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 7d6afa8..70d1e8e 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -1,9 +1,12 @@ import CircuitBoard from "@/components/circuitBoard"; import dynamic from "next/dynamic"; import Head from "next/head"; +import { proxy } from "valtio"; const DynamicLinkages = dynamic(() => import("@/components/linkages"), { ssr: false }); +const graphStore = proxy(new (window as any).LinkageGraph((window as any).UPDATE_MODE)); + export default function Home() { return ( <> @@ -14,7 +17,7 @@ export default function Home() {
-
+

The Digital Abacus

@@ -22,11 +25,10 @@ export default function Home() {
-
+
-
diff --git a/tsconfig.json b/tsconfig.json index 86e587a..aea5c0f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": true, + "strict": false, "forceConsistentCasingInFileNames": true, "noEmit": true, "esModuleInterop": true, @@ -18,12 +18,6 @@ "@/*": ["./src/*"] } }, - "include": [ - "next-env.d.ts", - "**/*.ts", - "**/*.tsx", - "src/model/graph/vertex.js", - "src/model/graph/edge.js" - ], - "exclude": ["node_modules"] + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "src/model/graph/edge.js"], + "exclude": ["node_modules", "src/model"] }