931 lines
25 KiB
TypeScript

import type { Selection } from "./Selection";
import type { Renderer } from "./Renderer";
import {
lineSegmentPointDistance,
pointInsideRect,
rectsCollide,
v2,
V2,
} from "./V2";
import * as ir from "./ir";
import { Sim } from "./sim";
import * as ser from "./serialize";
export class Board {
private components: Component[] = [];
private joints: Joint[] = [];
private wires: Wire[] = [];
private hoveredOverInput: [Component, number] | null = null;
private hoveredOverOutput: [Component, number] | null = null;
private hoveredOverJoint: Joint | null = null;
private hoveredOverWire: Wire | null = null;
private stateWireMap = new Map<ir.State, Wire[]>();
private activatedWires = new Set<Wire>();
private state = new Map<ir.State, boolean>();
private activatedOutputs = new Set<Component>();
private wireCachedState = new Map<Wire, ir.State>();
constructor(private repo: ComponentRepo) {}
static withExample(repo: ComponentRepo): Board {
const board = new Board(repo);
board.placeComponent(repo.get("input"), v2(100, 100));
board.placeComponent(repo.get("input"), v2(100, 200));
board.placeComponent(repo.get("and"), v2(300, 150));
board.placeComponent(repo.get("output"), v2(500, 150));
board.addWire(
{ tag: "OutputPin", comp: board.components[0], i: 0 },
{ tag: "InputPin", comp: board.components[2], i: 0 },
);
board.addWire(
{ tag: "OutputPin", comp: board.components[1], i: 0 },
{ tag: "InputPin", comp: board.components[2], i: 1 },
);
board.addWire(
{ tag: "OutputPin", comp: board.components[2], i: 0 },
{ tag: "InputPin", comp: board.components[3], i: 0 },
);
return board;
}
static fromSerialized(data: ser.Board, repo: ComponentRepo): Board {
const board = new Board(repo);
board.components = data.components.map((c) =>
Component.fromSerialized(c, repo.defs),
);
board.joints = data.joints.map((j) => Joint.fromSerialized(j));
board.wires = data.wires.map((w) =>
Wire.fromSerialized(w, board.components, board.joints),
);
return board;
}
serialize(): ser.Board {
return {
components: this.components.map((c) => c.serialize()),
joints: this.joints.map((j) => j.serialize()),
wires: this.wires.map((w) =>
w.serialize(
new Map(this.components.map((v, i) => [v, i])),
new Map(this.joints.map((v, i) => [v, i])),
),
),
};
}
canPlaceComponent(kind: ComponentKind, pos: V2): boolean {
return !this.components.some((comp) =>
rectsCollide(comp.pos, comp.kind.size, pos, kind.size),
);
}
placeComponent(kind: ComponentKind, pos: V2) {
this.components.push(new Component(kind, pos));
}
render(
r: Renderer,
selection: Selection | null,
inputStates: Map<Component, boolean>,
) {
for (const comp of this.components) {
const { pos, kind } = comp;
const isSelected = selection?.isComponentSelected(comp);
for (const wire of this.wires) {
if (this.hoveredOverWire == wire) {
r.drawWireHovered(wire.beginPos(), wire.endPos());
} else {
r.drawWire(
wire.beginPos(),
wire.endPos(),
this.activatedWires.has(wire),
);
}
}
if (comp.kind.label === "input") {
const active = inputStates.get(comp) ?? false;
if (isSelected) {
r.drawInputComponentBodySelected(pos, kind, active);
} else {
r.drawInputComponentBody(pos, kind, active);
}
} else if (comp.kind.label === "output") {
const active = this.activatedOutputs.has(comp);
if (isSelected) {
r.drawOutputComponentBodySelected(pos, kind, active);
} else {
r.drawOutputComponentBody(pos, kind, active);
}
} else {
if (isSelected) {
r.drawComponentBodySelected(pos, kind);
} else {
r.drawComponentBody(pos, kind);
}
}
for (const joint of this.joints) {
if (selection?.isJointSelected(joint)) {
r.drawJointSelected(joint.pos);
} else {
r.drawJoint(joint.pos);
}
if (this.hoveredOverJoint === joint) {
r.drawJointHover(joint.pos);
}
}
for (const [i, pinOffset] of kind.inputPinOffsets().entries()) {
if (kind.inputs[i] !== null) {
throw new Error("pin text not implemented");
}
r.drawComponentInputPin(pos, pinOffset);
if (
this.hoveredOverInput?.[0] === comp &&
this.hoveredOverInput[1] === i
) {
r.drawComponentInputPinHover(pos, pinOffset);
}
}
for (const [i, pinOffset] of kind.outputPinOffsets().entries()) {
if (kind.outputs[i] !== null) {
throw new Error("pin text not implemented");
}
r.drawComponentOutputPin(pos, kind, pinOffset);
if (
this.hoveredOverOutput?.[0] === comp &&
this.hoveredOverOutput[1] === i
) {
r.drawComponentOutputPinHover(pos, kind, pinOffset);
}
}
}
}
updateMouseHover(pos: V2) {
this.hoveredOverInput = null;
this.hoveredOverOutput = null;
this.hoveredOverJoint = null;
this.hoveredOverWire = null;
for (const comp of this.components) {
const mouseOverResult = comp.mouseOver(pos);
switch (mouseOverResult?.tag) {
case "InputPin":
this.hoveredOverInput = [comp, mouseOverResult.i];
return;
case "OutputPin":
this.hoveredOverOutput = [comp, mouseOverResult.i];
return;
}
}
for (const joint of this.joints) {
if (joint.isMouseOver(pos)) {
this.hoveredOverJoint = joint;
return;
}
}
for (const wire of this.wires) {
if (wire.isMouseOver(pos)) {
this.hoveredOverWire = wire;
return;
}
}
}
handleMouseClick(
pos: V2,
actions: {
onInputPinClicked?(comp: Component, i: number): void;
onOutputPinClicked?(comp: Component, i: number): void;
onComponentClicked?(comp: Component): void;
onJointClicked?(joint: Joint): void;
onWireClicked?(wire: Wire): void;
},
): "handled" | "not handled" {
for (const comp of this.components) {
const mouseOverResult = comp.mouseOver(pos);
switch (mouseOverResult?.tag) {
case "Component":
actions.onComponentClicked?.(comp);
return "handled";
case "InputPin":
actions.onInputPinClicked?.(comp, mouseOverResult.i);
return "handled";
case "OutputPin":
actions.onOutputPinClicked?.(comp, mouseOverResult.i);
return "handled";
}
}
for (const joint of this.joints) {
if (joint.isMouseOver(pos)) {
actions.onJointClicked?.(joint);
return "handled";
}
}
for (const wire of this.wires) {
if (wire.isMouseOver(pos)) {
actions.onWireClicked?.(wire);
return "handled";
}
}
return "not handled";
}
componentsInRect(pos: V2, size: V2): Component[] {
return this.components.filter((comp) =>
rectsCollide(pos, size, comp.pos, comp.kind.size),
);
}
jointsInRect(pos: V2, size: V2): Joint[] {
return this.joints.filter((joint) => pointInsideRect(joint.pos, pos, size));
}
addJoint(pos: V2): Joint {
const t = new Joint(pos);
this.joints.push(t);
return t;
}
addWire(begin: WireConnection, end: WireConnection): Wire {
const wire = new Wire(begin, end);
this.wires.push(wire);
return wire;
}
deleteSelection(selection: Selection) {
this.components = this.components.filter(
(comp) => !selection.isComponentSelected(comp),
);
this.joints = this.joints.filter(
(joint) => !selection.isJointSelected(joint),
);
this.wires = this.wires.filter((wire) => !wire.isSelected(selection));
}
toComponentKind(name: string): ComponentKind {
const inputCount = this.components.filter(
(comp) => comp.kind.label === "input",
).length;
const outputCount = this.components.filter(
(comp) => comp.kind.label === "output",
).length;
const pinMax = Math.max(inputCount, outputCount);
return new ComponentKind(
v2(60 + name.length * 10, 40 + 10 * pinMax),
name,
new Array<null>(inputCount).fill(null),
new Array<null>(outputCount).fill(null),
);
}
inputsOrdered(): Component[] {
return this.components
.filter((c) => c.kind.label === "input")
.toSorted((a, b) => a.pos.y - b.pos.y);
}
outputsOrdered(): Component[] {
return this.components
.filter((c) => c.kind.label === "output")
.toSorted((a, b) => a.pos.y - b.pos.y);
}
inputArray(activatedInputs: Map<Component, boolean>): boolean[] {
return this.inputsOrdered().map((c) => activatedInputs.get(c) ?? false);
}
outputCount(): number {
return this.components.filter((c) => c.kind.label === "output").length;
}
simulate(inputStates: Map<Component, boolean>) {
console.log("Lowering to IR");
const comps = new Map<string, ir.Component>();
const comp = this.toIr("<main>", comps);
console.log("Before optimizing");
for (const [_label, comp] of comps) {
console.log(...new ir.ComponentPrinter().stringifyToConsole(comp));
}
console.log(...new ir.ComponentPrinter().stringifyToConsole(comp));
for (const [_label, comp] of comps) {
new ir.ComponentOptimizer(comp, []).optimizeComponent();
}
const replacedStates: [ir.State, ir.State][] = [];
new ir.ComponentOptimizer(comp, replacedStates).optimizeMain();
for (const [oldState, newState] of replacedStates) {
this.stateWireMap
.set(newState, [...(this.stateWireMap.get(newState) ?? []), ...(this.stateWireMap.get(oldState) ?? [])])
this.stateWireMap.delete(oldState);
}
console.log("After optimizing");
for (const [_label, comp] of comps) {
console.log(...new ir.ComponentPrinter().stringifyToConsole(comp));
}
console.log(...new ir.ComponentPrinter().stringifyToConsole(comp));
const inputs = this.inputArray(inputStates);
const outputs = this.outputsOrdered();
const outputStates = outputs.map(() => false);
const sim = new Sim(comp, inputs, outputStates, this.state);
sim.simulate();
this.activatedWires.clear();
for (const state of sim.activatedState()) {
for (const wire of this.stateWireMap.get(state) ?? []) {
this.activatedWires.add(wire);
}
}
this.activatedOutputs.clear();
for (const [i, active] of outputStates.entries()) {
if (active) {
this.activatedOutputs.add(outputs[i]);
}
}
}
toIr(label: string, comps: Map<string, ir.Component>): ir.Component {
for (const comp of this.components) {
comp.markedWiresConnected = [];
}
for (const joint of this.joints) {
joint.markedWiresConnected = [];
}
for (const wire of this.wires) {
wire.markConnections();
}
const inputs = this.components.filter(
(comp) => comp.kind.label === "input",
);
const outputs = this.components.filter(
(comp) => comp.kind.label === "output",
);
const inputIdcs = new Map<Component, number>();
for (const [i, input] of inputs.entries()) {
inputIdcs.set(input, i);
}
const outputIdcs = new Map<Component, number>();
for (const [i, output] of outputs.entries()) {
outputIdcs.set(output, i);
}
const b = new ir.ComponentBuilder(inputs.length, outputs.length, label);
this.stateWireMap.clear();
const wireStates = new Map<Wire, ir.State>();
for (const wire of this.wires) {
const state = this.wireCachedState.get(wire) ?? b.makeState();
this.wireCachedState.set(wire, state);
wireStates.set(wire, state);
this.stateWireMap.set(state, [wire]);
}
const compSet = new Set<Component>();
const jointSet = new Set<Joint>();
const wireSet = new Set<Wire>();
const visitor: BoardVisitor = {
visitComponent: (comp) => {
if (compSet.has(comp)) return "break";
compSet.add(comp);
const inputStates = new Map<number, ir.State>();
for (const [wire, connection] of comp.markedWiresConnected) {
if (connection.tag === "InputPin") {
inputStates.set(connection.i, wireStates.get(wire)!);
}
}
const inputStmt = (i: number) => {
return inputStates.has(i)
? b.makeGetState(inputStates.get(i)!)
: b.makeNull();
};
const stmt = (() => {
switch (comp.kind.label) {
case "input":
return b.makeInput(inputIdcs.get(comp)!);
case "output":
return b.makeOutput(outputIdcs.get(comp)!, inputStmt(0));
case "not":
return b.makeNot(inputStmt(0));
case "and":
return b.makeBinary("And", inputStmt(0), inputStmt(1));
case "or":
return b.makeBinary("Or", inputStmt(0), inputStmt(1));
default: {
const savedBoard = this.repo.getSavedBoard(comp.kind.label);
if (!savedBoard) {
throw new Error(`no component '${comp.kind.label}'`);
}
const label = comp.kind.label;
const board = Board.fromSerialized(savedBoard, this.repo);
const ir = comps.get(label) ?? board.toIr(label, comps);
comps.set(label, ir);
return b.makeCall(
ir,
comp.kind.inputs.map((_, i) => inputStmt(i)),
);
}
}
})();
for (const [wire, connection] of comp.markedWiresConnected) {
if (connection.tag === "OutputPin") {
b.makeSetState(
wireStates.get(wire)!,
stmt.kind.tag === "Call" ? b.makeElem(stmt, connection.i) : stmt,
);
}
}
},
visitJoint: (joint) => {
if (jointSet.has(joint)) return "break";
jointSet.add(joint);
const visited = joint.markedWiresConnected.filter(([wire]) =>
wireSet.has(wire),
);
if (visited.length > 1) {
throw new Error("joint has more than 1 input");
}
const notVisited = joint.markedWiresConnected.filter(
([wire]) => !wireSet.has(wire),
);
const sourceState = wireStates.get(visited[0][0]);
if (!sourceState) {
throw new Error("assert");
}
const src = b.makeGetState(sourceState);
for (const [wire] of notVisited) {
const dst = wireStates.get(wire);
if (!dst) {
throw new Error("assert");
}
b.makeSetState(dst, src);
}
},
visitWire: (wire) => {
if (wireSet.has(wire)) return "break";
wireSet.add(wire);
},
};
for (const comp of this.components) {
comp.visitForward(visitor);
}
return b.build();
}
}
export interface BoardVisitor {
visitComponent(comp: Component): void | "break";
visitJoint(joint: Joint): void | "break";
visitWire(wire: Wire): void | "break";
}
export class ComponentRepo {
public defs = new Map<string, ComponentKind>();
private savedBoards = new Map<string, ser.Board>();
static withDefaults(): ComponentRepo {
const repo = new ComponentRepo();
for (const { label, size, inputs, outputs } of defaultDefs) {
repo.add(label, new ComponentKind(size, label, inputs, outputs));
}
return repo;
}
static fromSerialized(data: ser.ComponentRepo): ComponentRepo {
const repo = new ComponentRepo();
repo.defs = new Map(
data.defs.map((e) => [e[0], ComponentKind.fromSerialized(e[1])]),
);
repo.savedBoards = new Map(data.savedBoards);
return repo;
}
serialize(): ser.ComponentRepo {
return {
defs: [...this.defs.entries()].map((e) => [e[0], e[1].serialize()]),
savedBoards: [...this.savedBoards],
};
}
available(): string[] {
return [...this.defs.keys()];
}
add(ident: string, kind: ComponentKind) {
this.defs.set(ident, kind);
}
get(ident: string): ComponentKind {
const kind = this.defs.get(ident);
if (!kind) {
throw new Error("should be defined");
}
return kind;
}
addSavedBoard(ident: string, savedBoard: ser.Board) {
this.savedBoards.set(ident, savedBoard);
}
getSavedBoard(ident: string): ser.Board | null {
return this.savedBoards.get(ident) ?? null;
}
}
export class Component {
public markedWiresConnected: [Wire, WireConnection][] = [];
constructor(
public kind: ComponentKind,
public pos: V2,
) {}
static fromSerialized(
data: ser.Component,
kindMap: Map<string, ComponentKind>,
): Component {
return new Component(
kindMap.get(data.kindKey)!,
V2.fromSerialized(data.pos),
);
}
serialize(): ser.Component {
return {
kindKey: this.kind.label,
pos: this.pos.serialize(),
};
}
mouseOver(pos: V2): ComponentMouseOverResult | null {
const {
pos: { x, y },
kind: {
size: { x: w },
},
} = this;
if (
!pointInsideRect(
pos,
this.pos.sub(v2(5, 5)),
this.kind.size.add(v2(10, 10)),
)
) {
return null;
}
for (const [i, pinOffset] of this.kind.inputPinOffsets().entries()) {
if (v2(x, y + pinOffset).distance(pos) < 6) {
return { tag: "InputPin", i };
}
}
for (const [i, pinOffset] of this.kind.outputPinOffsets().entries()) {
if (v2(x + w, y + pinOffset).distance(pos) < 6) {
return { tag: "OutputPin", i };
}
}
return { tag: "Component" };
}
inputPinPos(i: number): V2 {
return this.pos.add(v2(0, this.kind.inputPinOffsets()[i]));
}
outputPinPos(i: number): V2 {
return this.pos.add(v2(this.kind.size.x, this.kind.outputPinOffsets()[i]));
}
visitForward(visitor: BoardVisitor) {
if (visitor.visitComponent(this) === "break") return;
for (const [wire, connection] of this.markedWiresConnected) {
switch (connection.tag) {
case "OutputPin":
wire.visitForward(visitor, connection);
break;
}
}
}
}
type ComponentMouseOverResult =
| { tag: "Component" }
| { tag: "InputPin" | "OutputPin"; i: number };
export class ComponentKind {
constructor(
public size: V2,
public label: string,
public inputs: (string | null)[],
public outputs: (string | null)[],
) {}
static fromSerialized(data: ser.ComponentKind): ComponentKind {
return new ComponentKind(
V2.fromSerialized(data.size),
data.label,
data.inputs,
data.outputs,
);
}
serialize(): ser.ComponentKind {
return {
size: this.size.serialize(),
label: this.label,
inputs: this.inputs,
outputs: this.outputs,
};
}
inputPinOffsets(): number[] {
return this.inputs.map(
(_, i) => ((i + 1) * this.size.y) / (this.inputs.length + 1),
);
}
outputPinOffsets(): number[] {
return this.outputs.map(
(_, i) => ((i + 1) * this.size.y) / (this.outputs.length + 1),
);
}
}
export class Joint {
public markedWiresConnected: [Wire, WireConnection][] = [];
constructor(public pos: V2) {}
static fromSerialized(data: ser.Joint): Joint {
return new Joint(V2.fromSerialized(data.pos));
}
serialize(): ser.Joint {
return { pos: this.pos.serialize() };
}
isMouseOver(pos: V2): boolean {
return this.pos.distance(pos) < 6;
}
visitForward(visitor: BoardVisitor, entryWire: Wire) {
if (visitor.visitJoint(this) === "break") return;
for (const [wire, connection] of this.markedWiresConnected) {
if (wire === entryWire) {
continue;
}
wire.visitForward(visitor, connection);
}
}
}
export class Wire {
constructor(
private begin: WireConnection,
private end: WireConnection,
) {}
static fromSerialized(
data: ser.Wire,
comps: Component[],
joints: Joint[],
): Wire {
const [begin, end] = [data.begin, data.end].map((conn): WireConnection => {
switch (conn.tag) {
case "Joint":
return { tag: "Joint", joint: joints[conn.jointIdx] };
case "InputPin":
case "OutputPin":
return {
tag: conn.tag,
comp: comps[conn.compIdx],
i: conn.i,
};
}
});
return new Wire(begin, end);
}
serialize(
compIdxMap: Map<Component, number>,
jointIdxMap: Map<Joint, number>,
): ser.Wire {
const [begin, end] = [this.begin, this.end].map(
(conn): ser.WireConnection => {
switch (conn.tag) {
case "Joint":
return { tag: "Joint", jointIdx: jointIdxMap.get(conn.joint)! };
case "InputPin":
case "OutputPin":
return {
tag: conn.tag,
compIdx: compIdxMap.get(conn.comp)!,
i: conn.i,
};
}
},
);
return { begin, end };
}
isInput(): boolean {
return this.mapConns((connection) => connection.tag === "InputPin").some(
(v) => v,
);
}
markConnections() {
this.mapConns((connection) => {
switch (connection.tag) {
case "InputPin":
case "OutputPin":
connection.comp.markedWiresConnected.push([this, connection]);
break;
case "Joint":
connection.joint.markedWiresConnected.push([this, connection]);
break;
}
});
}
isMouseOver(pos: V2): boolean {
const distance = lineSegmentPointDistance(
this.beginPos(),
this.endPos(),
pos,
);
return distance !== null && distance < 6;
}
isSelected(selection: Selection): boolean {
return this.mapConns((connection) => {
switch (connection.tag) {
case "InputPin":
case "OutputPin":
return selection.isComponentSelected(connection.comp);
case "Joint":
return selection.isJointSelected(connection.joint);
}
}).some((v) => v);
}
connectedToComponent(comp: Component): boolean {
return this.mapConns((connection) => {
switch (connection.tag) {
case "InputPin":
case "OutputPin":
return connection.comp === comp;
case "Joint":
return false;
}
}).some((v) => v);
}
connectedToJoint(joint: Joint): boolean {
return this.mapConns((connection) => {
switch (connection.tag) {
case "InputPin":
case "OutputPin":
return false;
case "Joint":
return connection.joint === joint;
}
}).some((v) => v);
}
connectedComponents(): Component[] {
return this.mapConns((connection) => {
switch (connection.tag) {
case "InputPin":
case "OutputPin":
return [connection.comp];
case "Joint":
return [];
}
}).flat();
}
connectedJoints(): Joint[] {
return this.mapConns((connection) => {
switch (connection.tag) {
case "InputPin":
case "OutputPin":
return [];
case "Joint":
return [connection.joint];
}
}).flat();
}
private mapConns<R>(mapper: (connection: WireConnection) => R): [R, R] {
return [mapper(this.begin), mapper(this.end)];
}
beginPos(): V2 {
return this.connectionPos(this.begin);
}
endPos(): V2 {
return this.connectionPos(this.end);
}
private connectionPos(connection: WireConnection): V2 {
switch (connection.tag) {
case "InputPin":
return connection.comp.inputPinPos(connection.i);
case "OutputPin":
return connection.comp.outputPinPos(connection.i);
case "Joint":
return connection.joint.pos;
}
}
visitForward(visitor: BoardVisitor, prev: WireConnection) {
if (visitor.visitWire(this) === "break") return;
const connection = this.begin === prev ? this.end : this.begin;
switch (connection.tag) {
case "InputPin":
connection.comp.visitForward(visitor);
break;
case "Joint":
connection.joint.visitForward(visitor, this);
break;
}
}
}
export type WireConnection =
| { tag: "InputPin"; comp: Component; i: number }
| { tag: "OutputPin"; comp: Component; i: number }
| { tag: "Joint"; joint: Joint };
const defaultDefs = [
{
label: "input",
size: v2(120, 40),
inputs: [],
outputs: [null],
},
{
label: "output",
size: v2(140, 40),
inputs: [null],
outputs: [],
},
{
label: "and",
size: v2(80, 40),
inputs: [null, null],
outputs: [null],
},
{
label: "or",
size: v2(80, 40),
inputs: [null, null],
outputs: [null],
},
{
label: "not",
size: v2(80, 40),
inputs: [null],
outputs: [null],
},
];