save components

This commit is contained in:
sfja 2026-06-11 02:14:29 +02:00
parent c1e72f9ae2
commit cbc416da46
14 changed files with 595 additions and 99 deletions

View File

@ -56,7 +56,13 @@ function Tabbar({ editor, canvasRef }: Props): ReactElement {
>
Rename
</button>
<button onClick={() => {}}>Close</button>
<button
onClick={() => {
editor.events.send({ tag: "CloseComponent" });
}}
>
Close
</button>
</div>
</div>
</>

View File

@ -4,10 +4,12 @@ import type { Editor } from "./editor/Editor";
type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null> };
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}
</button>

View File

@ -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<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() {}
static withExample(repo: ComponentRepo): Board {
@ -42,6 +52,34 @@ export class Board {
return board;
}
static fromSerialized(
data: ser.Board,
kindMap: Map<string, ComponentKind>,
): 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<Component, boolean>,
) {
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<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 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<Wire, ir.State>();
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<Component>();
@ -362,7 +490,7 @@ export interface BoardVisitor {
}
export class ComponentRepo {
private defs = new Map<string, ComponentKind>();
public defs = new Map<string, ComponentKind>();
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<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 },
@ -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<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,
@ -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: [],
},

View File

@ -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<Component, boolean>();
public keysPressed = new Set<string>();
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));
},

View File

@ -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<typeof setTimeout>;
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}`);
}
},
);

View File

@ -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<string, ser.Board>,
) {
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 = {

View File

@ -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);

View File

@ -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);
}

View File

@ -5,7 +5,9 @@ export class ViewPos {
public offset = v2(0, 0);
constructor(private events: EventBus) {
this.events.subscribe(["MouseDown", "MouseMove"], (ev) => {
this.events.subscribe(
["MouseDown", "MouseMove", "MouseClick", "MouseDoubleClick"],
(ev) => {
const absPos = ev.pos;
const pos = this.canvasToBoard(absPos);
switch (ev.tag) {
@ -19,8 +21,15 @@ export class ViewPos {
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 {

View File

@ -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 };

View File

@ -142,7 +142,10 @@ export class ComponentBuilder {
}
class StmtsMutater {
constructor(private comp: Component) {}
constructor(
private comp: Component,
private replacedStates: [State, State][],
) {}
[Symbol.iterator](): Iterator<Stmt> {
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<State, Stmt>();
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<Stmt, State>();
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<State>();
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);

View File

@ -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];

View File

@ -5,22 +5,25 @@ export class Sim {
private comp: ir.Component,
private inputs: boolean[],
private outputs: boolean[],
private state: Map<ir.State, boolean>,
) {}
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<boolean>(comp.stmts.length).fill(false);
const stateDependents = new Map<ir.State, number>();
const operation = <Ops extends ir.Stmt[]>(
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);
}
}

View File

@ -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;