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(); private activatedWires = new Set(); private state = new Map(); private activatedOutputs = new Set(); private wireCachedState = new Map(); 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, ) { 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(inputCount).fill(null), new Array(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): 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) { console.log("Lowering to IR"); const comps = new Map(); const comp = this.toIr("
", 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): 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(); for (const [i, input] of inputs.entries()) { inputIdcs.set(input, i); } const outputIdcs = new Map(); 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(); 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(); const jointSet = new Set(); const wireSet = new Set(); const visitor: BoardVisitor = { visitComponent: (comp) => { if (compSet.has(comp)) return "break"; compSet.add(comp); const inputStates = new Map(); 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(); private savedBoards = new Map(); 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, ): 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, jointIdxMap: Map, ): 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(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], }, ];