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;