931 lines
25 KiB
TypeScript
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],
|
|
},
|
|
];
|