From cbc416da46d729ebbb73885b9b99578703e0723a Mon Sep 17 00:00:00 2001 From: sfja Date: Thu, 11 Jun 2026 02:14:29 +0200 Subject: [PATCH] save components --- editor/src/Tabbar.tsx | 8 +- editor/src/Toolbar.tsx | 6 + editor/src/editor/Board.ts | 256 +++++++++++++++++++++++++++++++-- editor/src/editor/Editor.ts | 95 ++++++++---- editor/src/editor/Mouse.ts | 11 +- editor/src/editor/Project.ts | 88 ++++++++++-- editor/src/editor/Renderer.ts | 68 +++++++-- editor/src/editor/V2.ts | 8 ++ editor/src/editor/ViewPos.ts | 41 +++--- editor/src/editor/events.ts | 5 +- editor/src/editor/ir.ts | 21 ++- editor/src/editor/serialize.ts | 49 +++++++ editor/src/editor/sim.ts | 36 ++++- editor/src/style.css | 2 - 14 files changed, 595 insertions(+), 99 deletions(-) create mode 100644 editor/src/editor/serialize.ts diff --git a/editor/src/Tabbar.tsx b/editor/src/Tabbar.tsx index ecbef66..b257ad0 100644 --- a/editor/src/Tabbar.tsx +++ b/editor/src/Tabbar.tsx @@ -56,7 +56,13 @@ function Tabbar({ editor, canvasRef }: Props): ReactElement { > Rename - + diff --git a/editor/src/Toolbar.tsx b/editor/src/Toolbar.tsx index b15f3c1..9fed4be 100644 --- a/editor/src/Toolbar.tsx +++ b/editor/src/Toolbar.tsx @@ -4,10 +4,12 @@ import type { Editor } from "./editor/Editor"; type Props = { editor: Editor; canvasRef: RefObject }; function Toolbar({ editor, canvasRef }: Props): ReactElement { + const [updateId, update] = useState(0); const [selectedTool, setSelectedTool] = useState("select"); useEffect(() => editor.events.subscribe(["ShowSelectedTool"], (ev) => { + update(updateId + 1); setSelectedTool(ev.tool); }), ); @@ -25,6 +27,10 @@ function Toolbar({ editor, canvasRef }: Props): ReactElement { editor.events.send({ tag: "SelectTool", tool }); canvasRef.current?.focus(); }} + onDoubleClick={() => { + editor.events.send({ tag: "OpenTabWithTool", tool }); + canvasRef.current?.focus(); + }} > {tool} diff --git a/editor/src/editor/Board.ts b/editor/src/editor/Board.ts index e3edd99..d82dad0 100644 --- a/editor/src/editor/Board.ts +++ b/editor/src/editor/Board.ts @@ -8,6 +8,8 @@ import { V2, } from "./V2"; import * as ir from "./ir"; +import { Sim } from "./sim"; +import * as ser from "./serialize"; export class Board { private components: Component[] = []; @@ -19,6 +21,14 @@ export class Board { 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() {} static withExample(repo: ComponentRepo): Board { @@ -42,6 +52,34 @@ export class Board { return board; } + static fromSerialized( + data: ser.Board, + kindMap: Map, + ): Board { + const board = new Board(); + board.components = data.components.map((c) => + Component.fromSerialized(c, kindMap), + ); + 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), @@ -52,20 +90,47 @@ export class Board { this.components.push(new Component(kind, pos)); } - render(r: Renderer, selection: Selection | null) { + render( + r: Renderer, + selection: Selection | null, + inputStates: Map, + ) { for (const comp of this.components) { const { pos, kind } = comp; - if (selection?.isComponentSelected(comp)) { - r.drawComponentBodySelected(pos, kind); - } else { - r.drawComponentBody(pos, kind); - } + + 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()); + 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); } } @@ -231,9 +296,68 @@ export class Board { ); } - toIr(): ir.Component { - console.log("Lowering to IR"); + 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 comp = this.toIr(); + console.log("Before optimizing"); + console.log(...new ir.ComponentPrinter().stringifyToConsole(comp)); + + const replacedStates: [ir.State, ir.State][] = []; + new ir.ComponentOptimizer(comp, replacedStates).optimize(); + + for (const [oldState, newState] of replacedStates) { + this.stateWireMap + .get(newState)! + .push(...this.stateWireMap.get(oldState)!); + this.stateWireMap.delete(oldState); + } + + console.log("After optimizing"); + 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(): ir.Component { for (const comp of this.components) { comp.markedWiresConnected = []; } @@ -262,9 +386,13 @@ export class Board { const b = new ir.ComponentBuilder(inputs.length, outputs.length, "main"); + this.stateWireMap.clear(); const wireStates = new Map(); for (const wire of this.wires) { - wireStates.set(wire, b.makeState()); + 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(); @@ -362,7 +490,7 @@ export interface BoardVisitor { } export class ComponentRepo { - private defs = new Map(); + public defs = new Map(); static withDefaults(): ComponentRepo { const repo = new ComponentRepo(); @@ -374,6 +502,20 @@ export class ComponentRepo { 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])]), + ); + return repo; + } + + serialize(): ser.ComponentRepo { + return { + defs: [...this.defs.entries()].map((e) => [e[0], e[1].serialize()]), + }; + } + available(): string[] { return [...this.defs.keys()]; } @@ -399,6 +541,23 @@ export class Component { 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 }, @@ -462,6 +621,24 @@ export class ComponentKind { 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), @@ -479,6 +656,14 @@ export class Joint { 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; } @@ -500,6 +685,51 @@ export class Wire { 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, @@ -633,13 +863,13 @@ export type WireConnection = const defaultDefs = [ { label: "input", - size: v2(80, 40), + size: v2(120, 40), inputs: [], outputs: [null], }, { label: "output", - size: v2(80, 40), + size: v2(140, 40), inputs: [null], outputs: [], }, diff --git a/editor/src/editor/Editor.ts b/editor/src/editor/Editor.ts index 2b206c6..a50e415 100644 --- a/editor/src/editor/Editor.ts +++ b/editor/src/editor/Editor.ts @@ -1,4 +1,4 @@ -import { Board, ComponentRepo } from "./Board"; +import { Board, Component, ComponentRepo } from "./Board"; import { SelectionBox } from "./SelectionBox"; import { ComponentPlacer } from "./ComponentPlacer"; import { Selection } from "./Selection"; @@ -11,6 +11,8 @@ import { ViewPos } from "./ViewPos"; import { type ComponentKind } from "./Board"; import type { EventUnsub } from "./events"; import { Project } from "./Project"; +import * as ir from "./ir"; +import { Sim } from "./sim"; export class Editor { public events = new EventBus(); @@ -25,6 +27,8 @@ export class Editor { public selection: Selection | null = null; public connectingWire: ConnectingWire | null = null; + public inputStates = new Map(); + public keysPressed = new Set(); private state: State = new Normal(this); @@ -38,10 +42,14 @@ export class Editor { "KeyDown", "KeyUp", "SelectTool", + "OpenTabWithTool", "CreateTab", + "CloseComponent", "SelectTab", "SaveComponent", "RenameComponent", + "SimulateRequest", + "SaveRequest", ], (ev) => { switch (ev.tag) { @@ -54,6 +62,11 @@ export class Editor { case "SelectTool": this.onSelectTool(ev.tool); break; + case "OpenTabWithTool": { + const idx = this.project.tabWithTool(ev.tool); + this.switchTab(idx); + break; + } case "CreateTab": { const idx = this.project.newTab(); this.switchTab(idx); @@ -63,6 +76,10 @@ export class Editor { this.switchTab(ev.idx); break; } + case "CloseComponent": { + this.switchTab(this.project.closeTab()); + break; + } case "SaveComponent": { this.project.saveComponent(); break; @@ -71,6 +88,14 @@ export class Editor { this.project.renameComponent(ev.newName); break; } + case "SimulateRequest": { + // this.runSimulation(); + break; + } + case "SaveRequest": { + this.project.save(); + break; + } } this.events.send({ tag: "RenderRequest" }); }, @@ -84,7 +109,7 @@ export class Editor { r.clear(); r.drawGrid(); - this.board.render(r, this.selection); + this.board.render(r, this.selection, this.inputStates); this.selectionBox?.render(r); this.componentPlacer?.render(r); this.connectingWire?.render(r); @@ -120,15 +145,10 @@ export class Editor { } runSimulation() { - // const comp = this.board.toIr(); - // console.log("Before optimizing"); - // console.log(...new ir.ComponentPrinter().stringifyToConsole(comp)); - // new ir.ComponentOptimizer(comp).optimize(); - // console.log("After optimizing"); - // console.log(...new ir.ComponentPrinter().stringifyToConsole(comp)); - // const sim = new Sim(comp, [], []); - // sim.simulate(); + this.board.simulate(this.inputStates); + this.events.send({ tag: "RenderRequest" }); } + private onSelectTool(tool: string) { switch (tool) { case "pan": @@ -142,7 +162,7 @@ export class Editor { this.transitionTo(new Placing(this, tool)); break; default: - this.transitionTo(new Normal(this)); + this.transitionTo(new Placing(this, tool)); } this.events.send({ tag: "ShowSelectedTool", tool }); } @@ -150,6 +170,7 @@ export class Editor { private switchTab(idx: number) { this.project.switchTab(idx); this.events.send({ tag: "ShowSelectedTab", idx }); + this.events.send({ tag: "ShowSelectedTool", tool: "" }); this.selectionBox = null; this.componentPlacer = null; this.selection = null; @@ -173,17 +194,29 @@ class Normal implements State { enter(): void { this.unsubscribe = this.cx.events.subscribe( [ - "MouseDownOffset", + "MouseClickOffset", + "MouseDoubleClickOffset", "MouseMoveOffset", "MouseDragBegin", "KeyDown", - "MouseDoubleClick", ], (ev) => { switch (ev.tag) { - case "MouseDownOffset": - this.onMouseDown(ev.pos); + case "MouseClickOffset": + this.onMouseClick(ev.pos); break; + case "MouseDoubleClickOffset": { + this.cx.board.handleMouseClick(ev.pos, { + onComponentClicked: (comp) => { + if (comp.kind.label === "input") { + const val = this.cx.inputStates.get(comp) ?? false; + this.cx.inputStates.set(comp, !val); + this.cx.events.send({ tag: "SimulateRequest" }); + } + }, + }); + break; + } case "MouseMoveOffset": this.cx.board.updateMouseHover(ev.pos); break; @@ -199,28 +232,18 @@ class Normal implements State { } break; } - case "MouseDoubleClick": { - this.cx.board.handleMouseClick(ev.pos, { - onComponentClicked: (comp) => { - if (comp.kind.label === "input") { - } - }, - }); - break; - } } }, ); this.cx.events.send({ tag: "ShowSelectedTool", tool: "select" }); - this.cx.runSimulation(); } leave(): void { this.unsubscribe(); } - private onMouseDown(pos: V2): void { + private onMouseClick(pos: V2): void { this.cx.board.handleMouseClick(pos, { onInputPinClicked: (comp, i) => { this.cx.connectingWire = new ConnectingWire( @@ -319,6 +342,7 @@ class Placing implements State { const boardPos = ev.pos; if (this.cx.board.canPlaceComponent(this.compDef, boardPos)) { this.cx.board.placeComponent(this.compDef, boardPos); + this.cx.events.send({ tag: "SaveRequest" }); this.cx.transitionTo(new Normal(this.cx)); } break; @@ -378,6 +402,7 @@ class Selecting implements State { } this.cx.board.deleteSelection(this.cx.selection); this.cx.selection = null; + this.cx.events.send({ tag: "SaveRequest" }); this.cx.transitionTo(new Normal(this.cx)); } break; @@ -429,6 +454,8 @@ class Selecting implements State { class Moving implements State { private unsubscribe!: EventUnsub; + private hasMoved = false; + constructor(private cx: Editor) {} enter(): void { @@ -440,6 +467,7 @@ class Moving implements State { this.cx.transitionTo(new Selecting(this.cx)); break; case "MouseMove": + this.hasMoved = true; this.cx.selection?.move(ev.deltaPos); break; } @@ -448,6 +476,9 @@ class Moving implements State { } leave(): void { + if (this.hasMoved) { + this.cx.events.send({ tag: "SaveRequest" }); + } this.unsubscribe(); } } @@ -514,11 +545,11 @@ class Wiring implements State { enter(): void { this.unsubscribe = this.cx.events.subscribe( - ["MouseDownOffset", "MouseMoveOffset", "KeyDown"], + ["MouseClickOffset", "MouseMoveOffset", "KeyDown"], (ev) => { switch (ev.tag) { - case "MouseDownOffset": - this.onMouseDown(ev.pos); + case "MouseClickOffset": + this.onMouseClick(ev.pos); break; case "MouseMoveOffset": { if (!this.cx.connectingWire) { @@ -545,21 +576,25 @@ class Wiring implements State { this.unsubscribe(); } - private onMouseDown(pos: V2): void { + private onMouseClick(pos: V2): void { + this.cx.connectingWire?.move(pos); if ( this.cx.board.handleMouseClick(pos, { onInputPinClicked: (comp, i) => { this.cx.connectingWire!.connectToInput(this.cx.board, comp, i); + this.cx.events.send({ tag: "SaveRequest" }); this.cx.connectingWire = null; this.cx.transitionTo(new Normal(this.cx)); }, onOutputPinClicked: (comp, i) => { this.cx.connectingWire!.connectToOutput(this.cx.board, comp, i); + this.cx.events.send({ tag: "SaveRequest" }); this.cx.connectingWire = null; this.cx.transitionTo(new Normal(this.cx)); }, onJointClicked: (joint) => { this.cx.connectingWire!.connectToJoint(this.cx.board, joint); + this.cx.events.send({ tag: "SaveRequest" }); this.cx.connectingWire = null; this.cx.transitionTo(new Normal(this.cx)); }, diff --git a/editor/src/editor/Mouse.ts b/editor/src/editor/Mouse.ts index 81bd77e..b79c1c8 100644 --- a/editor/src/editor/Mouse.ts +++ b/editor/src/editor/Mouse.ts @@ -1,7 +1,7 @@ import type { EventBus, EventUnsub } from "./events"; import { v2, type V2 } from "./V2"; -const doubleClickDelay = 200; +const doubleClickDelay = 100; export class Mouse { private state: State; @@ -103,6 +103,7 @@ class FirstRelease implements State { private unsubscribe: EventUnsub; private timeout: ReturnType; + private totalDelta = v2(0, 0); constructor( private cx: Mouse, @@ -116,6 +117,11 @@ class FirstRelease implements State { this.cx.transitionTo(new SecondPress(this.cx, this.pos)); break; case "MouseMove": + this.totalDelta = this.totalDelta.add(ev.deltaPos); + if (this.totalDelta.len() > 5) { + this.cx.eventBus.send({ tag: "MouseClick", pos: this.pos }); + this.cx.transitionTo(new Normal(this.cx)); + } break; case "MouseLeave": this.cx.transitionTo(new Normal(this.cx)); @@ -127,6 +133,7 @@ class FirstRelease implements State { ); this.timeout = setTimeout(() => { + this.cx.eventBus.send({ tag: "MouseClick", pos: this.pos }); this.cx.transitionTo(new Normal(this.cx)); }, doubleClickDelay); } @@ -205,8 +212,6 @@ class Dragging implements State { case "MouseLeave": this.cx.transitionTo(new Normal(this.cx)); break; - default: - throw new Error(`unexpected event ${ev.tag}`); } }, ); diff --git a/editor/src/editor/Project.ts b/editor/src/editor/Project.ts index 9b83f6b..e825039 100644 --- a/editor/src/editor/Project.ts +++ b/editor/src/editor/Project.ts @@ -1,5 +1,6 @@ -import { Board, ComponentRepo, type Component } from "./Board"; +import { Board, Component, ComponentRepo } from "./Board"; import { type EventBus } from "./events"; +import * as ser from "./serialize"; export class Project { private current: BoardEditor; @@ -8,15 +9,16 @@ export class Project { private constructor( private events: EventBus, private boardEditors: BoardEditor[], - private components: Component[], public componentRepo: ComponentRepo, + private savedBoards: Map, ) { this.current = boardEditors[this.selectedIdx]; } static loadLocalStoreOrInitNew(events: EventBus): Project { + // globalThis.localStorage.removeItem("nandsim"); if (globalThis.localStorage.getItem("nandsim")) { - return this.loadLocalStorage(); + return this.loadLocalStorage(events); } else { return this.initNew(events); } @@ -31,13 +33,57 @@ export class Project { board: Board.withExample(repo), }, ], - [], repo, + new Map(), ); } - static loadLocalStorage(): Project { - throw new Error("not implemented"); + static loadLocalStorage(events: EventBus): Project { + const data = JSON.parse( + globalThis.localStorage.getItem("nandsim")!, + ) as ser.Project; + return Project.fromSerialized(data, events); + } + + save() { + console.log("Saving"); + const data = this.serialize(); + globalThis.localStorage.setItem( + "nandsim", + JSON.stringify(this.serialize()), + ); + console.log(data); + } + + private static fromSerialized(data: ser.Project, events: EventBus): Project { + const repo = ComponentRepo.fromSerialized(data.componentRepo); + const project = new Project( + events, + data.boardEditors.map( + (data): BoardEditor => ({ + name: data.name, + board: Board.fromSerialized(data.board, repo.defs), + }), + ), + repo, + new Map(data.savedBoards), + ); + return project; + } + + private serialize(): ser.Project { + const componentRepo = this.componentRepo.serialize(); + return { + boardEditors: this.boardEditors.map( + (b): ser.BoardEditor => ({ + name: b.name, + board: b.board.serialize(), + }), + ), + currentBoardEditorIdx: this.selectedIdx, + componentRepo, + savedBoards: [...this.savedBoards], + }; } currentBoard(): Board { @@ -70,19 +116,21 @@ export class Project { this.events.send({ tag: "ShowSelectedTool", tool: this.current.name }); } - closeTab(idx: number) { - this.boardEditors.splice(idx, 1); + closeTab(): number { + const [removed] = this.boardEditors.splice(this.selectedIdx, 1); + this.savedBoards.set(removed.name, removed.board.serialize()); + this.events.send({ tag: "SaveRequest" }); + if (this.boardEditors.length === 0) { this.newTab(); } - this.selectedIdx = 0; - this.current = this.boardEditors[this.selectedIdx]; - this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx }); + return 0; } renameComponent(newName: string) { this.current.name = newName; this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx }); + this.events.send({ tag: "ShowSelectedTool", tool: this.current.name }); } saveComponent() { @@ -92,6 +140,24 @@ export class Project { ); this.events.send({ tag: "ShowSelectedTool", tool: this.current.name }); } + + tabWithTool(name: string): number { + const foundIdx = this.boardEditors.findIndex((b) => b.name === name); + + if (foundIdx != -1) { + return foundIdx; + } + + const saved = this.savedBoards.get(name); + if (!saved) throw new Error(`cannot open '${name}'`); + + this.boardEditors.push({ + name: name, + board: Board.fromSerialized(saved, this.componentRepo.defs), + }); + this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx }); + return this.boardEditors.length - 1; + } } type BoardEditor = { diff --git a/editor/src/editor/Renderer.ts b/editor/src/editor/Renderer.ts index c875fba..79c2afc 100644 --- a/editor/src/editor/Renderer.ts +++ b/editor/src/editor/Renderer.ts @@ -64,7 +64,11 @@ export class Renderer { c.strokeRect(x, y, w, h); } - drawComponentBody(pos: V2, kind: ComponentKind) { + private drawComponentBodyInternal( + pos: V2, + kind: ComponentKind, + label: string, + ) { const { c, offset } = this; const { x, y } = pos.add(offset); const { x: w, y: h } = kind.size; @@ -77,14 +81,18 @@ export class Renderer { c.fillStyle = `#333333`; c.font = "bold 16px monospace"; - const textMetrix = c.measureText(kind.label); + const textMetrix = c.measureText(label); c.fillText( - kind.label, + label, x + w / 2 - textMetrix.width / 2, y + 13 + h / 2 - 16 / 2, ); } - drawComponentBodySelected(pos: V2, kind: ComponentKind) { + private drawComponentBodySelectedInternal( + pos: V2, + kind: ComponentKind, + label: string, + ) { const { c, offset } = this; const { x, y } = pos.add(offset); const { x: w, y: h } = kind.size; @@ -97,14 +105,58 @@ export class Renderer { c.fillStyle = `#333333`; c.font = "bold 16px monospace"; - const textMetrix = c.measureText(kind.label); + const textMetrix = c.measureText(label); c.fillText( - kind.label, + label, x + w / 2 - textMetrix.width / 2, y + 13 + h / 2 - 16 / 2, ); } + drawComponentBody(pos: V2, kind: ComponentKind) { + this.drawComponentBodyInternal(pos, kind, kind.label); + } + drawComponentBodySelected(pos: V2, kind: ComponentKind) { + this.drawComponentBodySelectedInternal(pos, kind, kind.label); + } + + drawInputComponentBody(pos: V2, kind: ComponentKind, active: boolean) { + this.drawComponentBodyInternal( + pos, + kind, + `input (${active ? "on" : "off"})`, + ); + } + drawInputComponentBodySelected( + pos: V2, + kind: ComponentKind, + active: boolean, + ) { + this.drawComponentBodySelectedInternal( + pos, + kind, + `input (${active ? "on" : "off"})`, + ); + } + drawOutputComponentBody(pos: V2, kind: ComponentKind, active: boolean) { + this.drawComponentBodyInternal( + pos, + kind, + `output (${active ? "on" : "off"})`, + ); + } + drawOutputComponentBodySelected( + pos: V2, + kind: ComponentKind, + active: boolean, + ) { + this.drawComponentBodySelectedInternal( + pos, + kind, + `output (${active ? "on" : "off"})`, + ); + } + drawComponentInputPin(pos: V2, pinOffset: number) { const { c, offset } = this; const { x, y } = pos.add(offset); @@ -153,12 +205,12 @@ export class Renderer { c.stroke(); } - drawWire(begin: V2, end: V2) { + drawWire(begin: V2, end: V2, active: boolean) { const { c, offset } = this; const { x: x0, y: y0 } = begin.add(offset); const { x: x1, y: y1 } = end.add(offset); - c.strokeStyle = `#333333`; + c.strokeStyle = active ? `#bb3333` : `#333333`; c.lineWidth = 3; c.beginPath(); c.moveTo(x0, y0); diff --git a/editor/src/editor/V2.ts b/editor/src/editor/V2.ts index 78facdf..e0566d6 100644 --- a/editor/src/editor/V2.ts +++ b/editor/src/editor/V2.ts @@ -4,6 +4,14 @@ export class V2 { public y: number, ) {} + static fromSerialized(data: [number, number]): V2 { + return new V2(data[0], data[1]); + } + + serialize(): [number, number] { + return [this.x, this.y]; + } + add(other: V2): V2 { return new V2(this.x + other.x, this.y + other.y); } diff --git a/editor/src/editor/ViewPos.ts b/editor/src/editor/ViewPos.ts index 56deb85..6afe2b5 100644 --- a/editor/src/editor/ViewPos.ts +++ b/editor/src/editor/ViewPos.ts @@ -5,22 +5,31 @@ export class ViewPos { public offset = v2(0, 0); constructor(private events: EventBus) { - this.events.subscribe(["MouseDown", "MouseMove"], (ev) => { - const absPos = ev.pos; - const pos = this.canvasToBoard(absPos); - switch (ev.tag) { - case "MouseDown": - this.events.send({ tag: "MouseDownOffset", pos, absPos }); - break; - case "MouseMove": - this.events.send({ - tag: "MouseMoveOffset", - pos, - deltaPos: ev.deltaPos, - }); - break; - } - }); + this.events.subscribe( + ["MouseDown", "MouseMove", "MouseClick", "MouseDoubleClick"], + (ev) => { + const absPos = ev.pos; + const pos = this.canvasToBoard(absPos); + switch (ev.tag) { + case "MouseDown": + this.events.send({ tag: "MouseDownOffset", pos, absPos }); + break; + case "MouseMove": + this.events.send({ + tag: "MouseMoveOffset", + pos, + deltaPos: ev.deltaPos, + }); + break; + case "MouseClick": + this.events.send({ tag: "MouseClickOffset", pos, absPos }); + break; + case "MouseDoubleClick": + this.events.send({ tag: "MouseDoubleClickOffset", pos, absPos }); + break; + } + }, + ); } canvasToBoard(pos: V2): V2 { diff --git a/editor/src/editor/events.ts b/editor/src/editor/events.ts index 9d61094..c3eb414 100644 --- a/editor/src/editor/events.ts +++ b/editor/src/editor/events.ts @@ -15,12 +15,13 @@ export type Event = deltaPos: V2; } | { tag: "KeyDown" | "KeyUp"; key: string } - | { tag: "SelectTool" | "ShowSelectedTool"; tool: string } + | { tag: "SelectTool" | "ShowSelectedTool" | "OpenTabWithTool"; tool: string } | { tag: "CreateTab" } | { tag: "SelectTab" | "ShowSelectedTab"; idx: number } | { tag: "MouseDownOffset"; pos: V2; absPos: V2 } | { tag: "MouseMoveOffset"; pos: V2; deltaPos: V2 } - | { tag: "RenderRequest" } + | { tag: "MouseClickOffset" | "MouseDoubleClickOffset"; pos: V2; absPos: V2 } + | { tag: "RenderRequest" | "SimulateRequest" | "SaveRequest" } | { tag: "SaveComponent" | "CloseComponent" } | { tag: "RenameComponent"; newName: string }; diff --git a/editor/src/editor/ir.ts b/editor/src/editor/ir.ts index d987f54..a8e7f36 100644 --- a/editor/src/editor/ir.ts +++ b/editor/src/editor/ir.ts @@ -142,7 +142,10 @@ export class ComponentBuilder { } class StmtsMutater { - constructor(private comp: Component) {} + constructor( + private comp: Component, + private replacedStates: [State, State][], + ) {} [Symbol.iterator](): Iterator { return this.comp.stmts[Symbol.iterator](); @@ -155,6 +158,7 @@ class StmtsMutater { } replaceState(oldState: State, newState: State) { + this.replacedStates.push([oldState, newState]); for (const stmt of this.comp.stmts) { stmt.replaceState(oldState, newState); } @@ -177,7 +181,10 @@ class StmtsMutater { } export class ComponentOptimizer { - constructor(private comp: Component) {} + constructor( + private comp: Component, + private replacedStates: [State, State][], + ) {} optimize() { const score = () => this.comp.stmts.length * 100 + this.comp.states.length; @@ -196,7 +203,7 @@ export class ComponentOptimizer { } eliminateRedundantState() { - const mut = new StmtsMutater(this.comp); + const mut = new StmtsMutater(this.comp, this.replacedStates); const immediatelyReadStateStmt = new Map(); for (const [i, stmt] of this.comp.stmts.entries()) { @@ -226,7 +233,7 @@ export class ComponentOptimizer { } moveSetStateToSource() { - const mut = new StmtsMutater(this.comp); + const mut = new StmtsMutater(this.comp, this.replacedStates); for (const [baseIdx, stmt] of this.comp.stmts.entries()) { const indices = this.indexMap(); @@ -249,7 +256,7 @@ export class ComponentOptimizer { } collapseStates() { - const mut = new StmtsMutater(this.comp); + const mut = new StmtsMutater(this.comp, this.replacedStates); const sourceStates = new MultiMap(); for (const stmt of this.comp.stmts) { @@ -266,7 +273,7 @@ export class ComponentOptimizer { } eliminateUnusedStates() { - const mut = new StmtsMutater(this.comp); + const mut = new StmtsMutater(this.comp, this.replacedStates); const usedStates = new Set(); for (const stmt of mut) { @@ -287,7 +294,7 @@ export class ComponentOptimizer { } eliminateRedundantSetState() { - const mut = new StmtsMutater(this.comp); + const mut = new StmtsMutater(this.comp, this.replacedStates); for (let i = this.comp.stmts.length - 1; i > 0; --i) { const [first, second] = this.comp.stmts.slice(i - 1, i + 1); diff --git a/editor/src/editor/serialize.ts b/editor/src/editor/serialize.ts new file mode 100644 index 0000000..630228b --- /dev/null +++ b/editor/src/editor/serialize.ts @@ -0,0 +1,49 @@ +export type Project = { + boardEditors: BoardEditor[]; + currentBoardEditorIdx: number; + componentRepo: ComponentRepo; + savedBoards: [string, Board][]; +}; + +export type BoardEditor = { + name: string; + board: Board; +}; + +export type Board = { + components: Component[]; + joints: Joint[]; + wires: Wire[]; +}; + +export type Component = { + kindKey: string; + pos: V2; +}; + +export type Joint = { + pos: V2; +}; + +export type Wire = { + begin: WireConnection; + end: WireConnection; +}; + +export type WireConnection = + | { tag: "InputPin"; compIdx: number; i: number } + | { tag: "OutputPin"; compIdx: number; i: number } + | { tag: "Joint"; jointIdx: number }; + +export type ComponentRepo = { + defs: [string, ComponentKind][]; +}; + +export type ComponentKind = { + size: V2; + label: string; + inputs: (string | null)[]; + outputs: (string | null)[]; +}; + +export type V2 = [number, number]; diff --git a/editor/src/editor/sim.ts b/editor/src/editor/sim.ts index 3601605..a875cec 100644 --- a/editor/src/editor/sim.ts +++ b/editor/src/editor/sim.ts @@ -5,22 +5,25 @@ export class Sim { private comp: ir.Component, private inputs: boolean[], private outputs: boolean[], + private state: Map, ) {} simulate() { const { comp, inputs, outputs } = this; const stmtIdcs = new Map(comp.stmts.map((stmt, i) => [stmt, i])); - const state = new Map(comp.states.map((state) => [state, false])); const regs = new Array(comp.stmts.length).fill(false); + const stateDependents = new Map(); + const operation = ( action: (...ops: boolean[]) => boolean, ...ops: Ops ) => action(...ops.map((op) => regs[stmtIdcs.get(op)!])); - for (const [i, stmt] of comp.stmts.entries()) { + for (let i = 0; i < comp.stmts.length; ++i) { + const stmt = comp.stmts[i]; const k = stmt.kind; switch (k.tag) { case "Null": @@ -30,14 +33,29 @@ export class Sim { regs[i] = inputs[k.i]; break; case "Output": - outputs[k.i] = regs[i]; + outputs[k.i] = regs[stmtIdcs.get(k.src)!]; break; case "GetState": - regs[i] = state.get(k.state)!; + regs[i] = this.state.get(k.state)! ?? false; + stateDependents.set( + k.state, + Math.min( + i, + stateDependents.get(k.state) ?? Number.MAX_SAFE_INTEGER, + ), + ); break; - case "SetState": - state.set(k.state, regs[i]); + case "SetState": { + const prev = this.state.get(k.state) ?? false; + const val = regs[stmtIdcs.get(k.src)!]; + this.state.set(k.state, val); + if (val !== prev) { + if (stateDependents.has(k.state)) { + i = stateDependents.get(k.state)! - 1; + } + } break; + } case "Not": regs[i] = operation((v) => !v, k.op); break; @@ -50,6 +68,12 @@ export class Sim { case "Component": throw new Error("not implemented"); } + + // console.log("Sim:", i, stmt.kind.tag, inputs, outputs, this.state); } } + + activatedState(): ir.State[] { + return [...this.state].filter(([_s, v]) => v).map(([s, _v]) => s); + } } diff --git a/editor/src/style.css b/editor/src/style.css index 9980017..7d34b1a 100644 --- a/editor/src/style.css +++ b/editor/src/style.css @@ -23,7 +23,6 @@ padding-bottom: 5px; font-size: 1rem; - text-transform: capitalize; font-weight: bold; min-width: 200px; @@ -61,7 +60,6 @@ padding-bottom: 5px; font-size: 1rem; - text-transform: capitalize; max-width: 200px; text-align: left;