save components
This commit is contained in:
parent
c1e72f9ae2
commit
cbc416da46
@ -56,7 +56,13 @@ function Tabbar({ editor, canvasRef }: Props): ReactElement {
|
|||||||
>
|
>
|
||||||
Rename
|
Rename
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => {}}>Close</button>
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
editor.events.send({ tag: "CloseComponent" });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -4,10 +4,12 @@ import type { Editor } from "./editor/Editor";
|
|||||||
type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null> };
|
type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null> };
|
||||||
|
|
||||||
function Toolbar({ editor, canvasRef }: Props): ReactElement {
|
function Toolbar({ editor, canvasRef }: Props): ReactElement {
|
||||||
|
const [updateId, update] = useState(0);
|
||||||
const [selectedTool, setSelectedTool] = useState("select");
|
const [selectedTool, setSelectedTool] = useState("select");
|
||||||
|
|
||||||
useEffect(() =>
|
useEffect(() =>
|
||||||
editor.events.subscribe(["ShowSelectedTool"], (ev) => {
|
editor.events.subscribe(["ShowSelectedTool"], (ev) => {
|
||||||
|
update(updateId + 1);
|
||||||
setSelectedTool(ev.tool);
|
setSelectedTool(ev.tool);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -25,6 +27,10 @@ function Toolbar({ editor, canvasRef }: Props): ReactElement {
|
|||||||
editor.events.send({ tag: "SelectTool", tool });
|
editor.events.send({ tag: "SelectTool", tool });
|
||||||
canvasRef.current?.focus();
|
canvasRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
|
onDoubleClick={() => {
|
||||||
|
editor.events.send({ tag: "OpenTabWithTool", tool });
|
||||||
|
canvasRef.current?.focus();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{tool}
|
{tool}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import {
|
|||||||
V2,
|
V2,
|
||||||
} from "./V2";
|
} from "./V2";
|
||||||
import * as ir from "./ir";
|
import * as ir from "./ir";
|
||||||
|
import { Sim } from "./sim";
|
||||||
|
import * as ser from "./serialize";
|
||||||
|
|
||||||
export class Board {
|
export class Board {
|
||||||
private components: Component[] = [];
|
private components: Component[] = [];
|
||||||
@ -19,6 +21,14 @@ export class Board {
|
|||||||
private hoveredOverJoint: Joint | null = null;
|
private hoveredOverJoint: Joint | null = null;
|
||||||
private hoveredOverWire: Wire | 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() {}
|
constructor() {}
|
||||||
|
|
||||||
static withExample(repo: ComponentRepo): Board {
|
static withExample(repo: ComponentRepo): Board {
|
||||||
@ -42,6 +52,34 @@ export class Board {
|
|||||||
return 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 {
|
canPlaceComponent(kind: ComponentKind, pos: V2): boolean {
|
||||||
return !this.components.some((comp) =>
|
return !this.components.some((comp) =>
|
||||||
rectsCollide(comp.pos, comp.kind.size, pos, kind.size),
|
rectsCollide(comp.pos, comp.kind.size, pos, kind.size),
|
||||||
@ -52,20 +90,47 @@ export class Board {
|
|||||||
this.components.push(new Component(kind, pos));
|
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) {
|
for (const comp of this.components) {
|
||||||
const { pos, kind } = comp;
|
const { pos, kind } = comp;
|
||||||
if (selection?.isComponentSelected(comp)) {
|
|
||||||
r.drawComponentBodySelected(pos, kind);
|
const isSelected = selection?.isComponentSelected(comp);
|
||||||
} else {
|
|
||||||
r.drawComponentBody(pos, kind);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const wire of this.wires) {
|
for (const wire of this.wires) {
|
||||||
if (this.hoveredOverWire == wire) {
|
if (this.hoveredOverWire == wire) {
|
||||||
r.drawWireHovered(wire.beginPos(), wire.endPos());
|
r.drawWireHovered(wire.beginPos(), wire.endPos());
|
||||||
} else {
|
} 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 {
|
inputsOrdered(): Component[] {
|
||||||
console.log("Lowering to IR");
|
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) {
|
for (const comp of this.components) {
|
||||||
comp.markedWiresConnected = [];
|
comp.markedWiresConnected = [];
|
||||||
}
|
}
|
||||||
@ -262,9 +386,13 @@ export class Board {
|
|||||||
|
|
||||||
const b = new ir.ComponentBuilder(inputs.length, outputs.length, "main");
|
const b = new ir.ComponentBuilder(inputs.length, outputs.length, "main");
|
||||||
|
|
||||||
|
this.stateWireMap.clear();
|
||||||
const wireStates = new Map<Wire, ir.State>();
|
const wireStates = new Map<Wire, ir.State>();
|
||||||
for (const wire of this.wires) {
|
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>();
|
const compSet = new Set<Component>();
|
||||||
@ -362,7 +490,7 @@ export interface BoardVisitor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ComponentRepo {
|
export class ComponentRepo {
|
||||||
private defs = new Map<string, ComponentKind>();
|
public defs = new Map<string, ComponentKind>();
|
||||||
|
|
||||||
static withDefaults(): ComponentRepo {
|
static withDefaults(): ComponentRepo {
|
||||||
const repo = new ComponentRepo();
|
const repo = new ComponentRepo();
|
||||||
@ -374,6 +502,20 @@ export class ComponentRepo {
|
|||||||
return repo;
|
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[] {
|
available(): string[] {
|
||||||
return [...this.defs.keys()];
|
return [...this.defs.keys()];
|
||||||
}
|
}
|
||||||
@ -399,6 +541,23 @@ export class Component {
|
|||||||
public pos: V2,
|
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 {
|
mouseOver(pos: V2): ComponentMouseOverResult | null {
|
||||||
const {
|
const {
|
||||||
pos: { x, y },
|
pos: { x, y },
|
||||||
@ -462,6 +621,24 @@ export class ComponentKind {
|
|||||||
public outputs: (string | null)[],
|
public outputs: (string | null)[],
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
static fromSerialized(data: ser.ComponentKind): ComponentKind {
|
||||||
|
return new ComponentKind(
|
||||||
|
V2.fromSerialized(data.size),
|
||||||
|
data.label,
|
||||||
|
data.inputs,
|
||||||
|
data.outputs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): ser.ComponentKind {
|
||||||
|
return {
|
||||||
|
size: this.size.serialize(),
|
||||||
|
label: this.label,
|
||||||
|
inputs: this.inputs,
|
||||||
|
outputs: this.outputs,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
inputPinOffsets(): number[] {
|
inputPinOffsets(): number[] {
|
||||||
return this.inputs.map(
|
return this.inputs.map(
|
||||||
(_, i) => ((i + 1) * this.size.y) / (this.inputs.length + 1),
|
(_, i) => ((i + 1) * this.size.y) / (this.inputs.length + 1),
|
||||||
@ -479,6 +656,14 @@ export class Joint {
|
|||||||
|
|
||||||
constructor(public pos: V2) {}
|
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 {
|
isMouseOver(pos: V2): boolean {
|
||||||
return this.pos.distance(pos) < 6;
|
return this.pos.distance(pos) < 6;
|
||||||
}
|
}
|
||||||
@ -500,6 +685,51 @@ export class Wire {
|
|||||||
private end: WireConnection,
|
private end: WireConnection,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
static fromSerialized(
|
||||||
|
data: ser.Wire,
|
||||||
|
comps: Component[],
|
||||||
|
joints: Joint[],
|
||||||
|
): Wire {
|
||||||
|
const [begin, end] = [data.begin, data.end].map((conn): WireConnection => {
|
||||||
|
switch (conn.tag) {
|
||||||
|
case "Joint":
|
||||||
|
return { tag: "Joint", joint: joints[conn.jointIdx] };
|
||||||
|
case "InputPin":
|
||||||
|
case "OutputPin":
|
||||||
|
return {
|
||||||
|
tag: conn.tag,
|
||||||
|
comp: comps[conn.compIdx],
|
||||||
|
i: conn.i,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Wire(begin, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(
|
||||||
|
compIdxMap: Map<Component, number>,
|
||||||
|
jointIdxMap: Map<Joint, number>,
|
||||||
|
): ser.Wire {
|
||||||
|
const [begin, end] = [this.begin, this.end].map(
|
||||||
|
(conn): ser.WireConnection => {
|
||||||
|
switch (conn.tag) {
|
||||||
|
case "Joint":
|
||||||
|
return { tag: "Joint", jointIdx: jointIdxMap.get(conn.joint)! };
|
||||||
|
case "InputPin":
|
||||||
|
case "OutputPin":
|
||||||
|
return {
|
||||||
|
tag: conn.tag,
|
||||||
|
compIdx: compIdxMap.get(conn.comp)!,
|
||||||
|
i: conn.i,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return { begin, end };
|
||||||
|
}
|
||||||
|
|
||||||
isInput(): boolean {
|
isInput(): boolean {
|
||||||
return this.mapConns((connection) => connection.tag === "InputPin").some(
|
return this.mapConns((connection) => connection.tag === "InputPin").some(
|
||||||
(v) => v,
|
(v) => v,
|
||||||
@ -633,13 +863,13 @@ export type WireConnection =
|
|||||||
const defaultDefs = [
|
const defaultDefs = [
|
||||||
{
|
{
|
||||||
label: "input",
|
label: "input",
|
||||||
size: v2(80, 40),
|
size: v2(120, 40),
|
||||||
inputs: [],
|
inputs: [],
|
||||||
outputs: [null],
|
outputs: [null],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "output",
|
label: "output",
|
||||||
size: v2(80, 40),
|
size: v2(140, 40),
|
||||||
inputs: [null],
|
inputs: [null],
|
||||||
outputs: [],
|
outputs: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Board, ComponentRepo } from "./Board";
|
import { Board, Component, ComponentRepo } from "./Board";
|
||||||
import { SelectionBox } from "./SelectionBox";
|
import { SelectionBox } from "./SelectionBox";
|
||||||
import { ComponentPlacer } from "./ComponentPlacer";
|
import { ComponentPlacer } from "./ComponentPlacer";
|
||||||
import { Selection } from "./Selection";
|
import { Selection } from "./Selection";
|
||||||
@ -11,6 +11,8 @@ import { ViewPos } from "./ViewPos";
|
|||||||
import { type ComponentKind } from "./Board";
|
import { type ComponentKind } from "./Board";
|
||||||
import type { EventUnsub } from "./events";
|
import type { EventUnsub } from "./events";
|
||||||
import { Project } from "./Project";
|
import { Project } from "./Project";
|
||||||
|
import * as ir from "./ir";
|
||||||
|
import { Sim } from "./sim";
|
||||||
|
|
||||||
export class Editor {
|
export class Editor {
|
||||||
public events = new EventBus();
|
public events = new EventBus();
|
||||||
@ -25,6 +27,8 @@ export class Editor {
|
|||||||
public selection: Selection | null = null;
|
public selection: Selection | null = null;
|
||||||
public connectingWire: ConnectingWire | null = null;
|
public connectingWire: ConnectingWire | null = null;
|
||||||
|
|
||||||
|
public inputStates = new Map<Component, boolean>();
|
||||||
|
|
||||||
public keysPressed = new Set<string>();
|
public keysPressed = new Set<string>();
|
||||||
|
|
||||||
private state: State = new Normal(this);
|
private state: State = new Normal(this);
|
||||||
@ -38,10 +42,14 @@ export class Editor {
|
|||||||
"KeyDown",
|
"KeyDown",
|
||||||
"KeyUp",
|
"KeyUp",
|
||||||
"SelectTool",
|
"SelectTool",
|
||||||
|
"OpenTabWithTool",
|
||||||
"CreateTab",
|
"CreateTab",
|
||||||
|
"CloseComponent",
|
||||||
"SelectTab",
|
"SelectTab",
|
||||||
"SaveComponent",
|
"SaveComponent",
|
||||||
"RenameComponent",
|
"RenameComponent",
|
||||||
|
"SimulateRequest",
|
||||||
|
"SaveRequest",
|
||||||
],
|
],
|
||||||
(ev) => {
|
(ev) => {
|
||||||
switch (ev.tag) {
|
switch (ev.tag) {
|
||||||
@ -54,6 +62,11 @@ export class Editor {
|
|||||||
case "SelectTool":
|
case "SelectTool":
|
||||||
this.onSelectTool(ev.tool);
|
this.onSelectTool(ev.tool);
|
||||||
break;
|
break;
|
||||||
|
case "OpenTabWithTool": {
|
||||||
|
const idx = this.project.tabWithTool(ev.tool);
|
||||||
|
this.switchTab(idx);
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "CreateTab": {
|
case "CreateTab": {
|
||||||
const idx = this.project.newTab();
|
const idx = this.project.newTab();
|
||||||
this.switchTab(idx);
|
this.switchTab(idx);
|
||||||
@ -63,6 +76,10 @@ export class Editor {
|
|||||||
this.switchTab(ev.idx);
|
this.switchTab(ev.idx);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "CloseComponent": {
|
||||||
|
this.switchTab(this.project.closeTab());
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "SaveComponent": {
|
case "SaveComponent": {
|
||||||
this.project.saveComponent();
|
this.project.saveComponent();
|
||||||
break;
|
break;
|
||||||
@ -71,6 +88,14 @@ export class Editor {
|
|||||||
this.project.renameComponent(ev.newName);
|
this.project.renameComponent(ev.newName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "SimulateRequest": {
|
||||||
|
// this.runSimulation();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "SaveRequest": {
|
||||||
|
this.project.save();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.events.send({ tag: "RenderRequest" });
|
this.events.send({ tag: "RenderRequest" });
|
||||||
},
|
},
|
||||||
@ -84,7 +109,7 @@ export class Editor {
|
|||||||
|
|
||||||
r.clear();
|
r.clear();
|
||||||
r.drawGrid();
|
r.drawGrid();
|
||||||
this.board.render(r, this.selection);
|
this.board.render(r, this.selection, this.inputStates);
|
||||||
this.selectionBox?.render(r);
|
this.selectionBox?.render(r);
|
||||||
this.componentPlacer?.render(r);
|
this.componentPlacer?.render(r);
|
||||||
this.connectingWire?.render(r);
|
this.connectingWire?.render(r);
|
||||||
@ -120,15 +145,10 @@ export class Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runSimulation() {
|
runSimulation() {
|
||||||
// const comp = this.board.toIr();
|
this.board.simulate(this.inputStates);
|
||||||
// console.log("Before optimizing");
|
this.events.send({ tag: "RenderRequest" });
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSelectTool(tool: string) {
|
private onSelectTool(tool: string) {
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case "pan":
|
case "pan":
|
||||||
@ -142,7 +162,7 @@ export class Editor {
|
|||||||
this.transitionTo(new Placing(this, tool));
|
this.transitionTo(new Placing(this, tool));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.transitionTo(new Normal(this));
|
this.transitionTo(new Placing(this, tool));
|
||||||
}
|
}
|
||||||
this.events.send({ tag: "ShowSelectedTool", tool });
|
this.events.send({ tag: "ShowSelectedTool", tool });
|
||||||
}
|
}
|
||||||
@ -150,6 +170,7 @@ export class Editor {
|
|||||||
private switchTab(idx: number) {
|
private switchTab(idx: number) {
|
||||||
this.project.switchTab(idx);
|
this.project.switchTab(idx);
|
||||||
this.events.send({ tag: "ShowSelectedTab", idx });
|
this.events.send({ tag: "ShowSelectedTab", idx });
|
||||||
|
this.events.send({ tag: "ShowSelectedTool", tool: "" });
|
||||||
this.selectionBox = null;
|
this.selectionBox = null;
|
||||||
this.componentPlacer = null;
|
this.componentPlacer = null;
|
||||||
this.selection = null;
|
this.selection = null;
|
||||||
@ -173,17 +194,29 @@ class Normal implements State {
|
|||||||
enter(): void {
|
enter(): void {
|
||||||
this.unsubscribe = this.cx.events.subscribe(
|
this.unsubscribe = this.cx.events.subscribe(
|
||||||
[
|
[
|
||||||
"MouseDownOffset",
|
"MouseClickOffset",
|
||||||
|
"MouseDoubleClickOffset",
|
||||||
"MouseMoveOffset",
|
"MouseMoveOffset",
|
||||||
"MouseDragBegin",
|
"MouseDragBegin",
|
||||||
"KeyDown",
|
"KeyDown",
|
||||||
"MouseDoubleClick",
|
|
||||||
],
|
],
|
||||||
(ev) => {
|
(ev) => {
|
||||||
switch (ev.tag) {
|
switch (ev.tag) {
|
||||||
case "MouseDownOffset":
|
case "MouseClickOffset":
|
||||||
this.onMouseDown(ev.pos);
|
this.onMouseClick(ev.pos);
|
||||||
break;
|
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":
|
case "MouseMoveOffset":
|
||||||
this.cx.board.updateMouseHover(ev.pos);
|
this.cx.board.updateMouseHover(ev.pos);
|
||||||
break;
|
break;
|
||||||
@ -199,28 +232,18 @@ class Normal implements State {
|
|||||||
}
|
}
|
||||||
break;
|
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.events.send({ tag: "ShowSelectedTool", tool: "select" });
|
||||||
this.cx.runSimulation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
leave(): void {
|
leave(): void {
|
||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMouseDown(pos: V2): void {
|
private onMouseClick(pos: V2): void {
|
||||||
this.cx.board.handleMouseClick(pos, {
|
this.cx.board.handleMouseClick(pos, {
|
||||||
onInputPinClicked: (comp, i) => {
|
onInputPinClicked: (comp, i) => {
|
||||||
this.cx.connectingWire = new ConnectingWire(
|
this.cx.connectingWire = new ConnectingWire(
|
||||||
@ -319,6 +342,7 @@ class Placing implements State {
|
|||||||
const boardPos = ev.pos;
|
const boardPos = ev.pos;
|
||||||
if (this.cx.board.canPlaceComponent(this.compDef, boardPos)) {
|
if (this.cx.board.canPlaceComponent(this.compDef, boardPos)) {
|
||||||
this.cx.board.placeComponent(this.compDef, boardPos);
|
this.cx.board.placeComponent(this.compDef, boardPos);
|
||||||
|
this.cx.events.send({ tag: "SaveRequest" });
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -378,6 +402,7 @@ class Selecting implements State {
|
|||||||
}
|
}
|
||||||
this.cx.board.deleteSelection(this.cx.selection);
|
this.cx.board.deleteSelection(this.cx.selection);
|
||||||
this.cx.selection = null;
|
this.cx.selection = null;
|
||||||
|
this.cx.events.send({ tag: "SaveRequest" });
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -429,6 +454,8 @@ class Selecting implements State {
|
|||||||
class Moving implements State {
|
class Moving implements State {
|
||||||
private unsubscribe!: EventUnsub;
|
private unsubscribe!: EventUnsub;
|
||||||
|
|
||||||
|
private hasMoved = false;
|
||||||
|
|
||||||
constructor(private cx: Editor) {}
|
constructor(private cx: Editor) {}
|
||||||
|
|
||||||
enter(): void {
|
enter(): void {
|
||||||
@ -440,6 +467,7 @@ class Moving implements State {
|
|||||||
this.cx.transitionTo(new Selecting(this.cx));
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
break;
|
break;
|
||||||
case "MouseMove":
|
case "MouseMove":
|
||||||
|
this.hasMoved = true;
|
||||||
this.cx.selection?.move(ev.deltaPos);
|
this.cx.selection?.move(ev.deltaPos);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -448,6 +476,9 @@ class Moving implements State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
leave(): void {
|
leave(): void {
|
||||||
|
if (this.hasMoved) {
|
||||||
|
this.cx.events.send({ tag: "SaveRequest" });
|
||||||
|
}
|
||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -514,11 +545,11 @@ class Wiring implements State {
|
|||||||
|
|
||||||
enter(): void {
|
enter(): void {
|
||||||
this.unsubscribe = this.cx.events.subscribe(
|
this.unsubscribe = this.cx.events.subscribe(
|
||||||
["MouseDownOffset", "MouseMoveOffset", "KeyDown"],
|
["MouseClickOffset", "MouseMoveOffset", "KeyDown"],
|
||||||
(ev) => {
|
(ev) => {
|
||||||
switch (ev.tag) {
|
switch (ev.tag) {
|
||||||
case "MouseDownOffset":
|
case "MouseClickOffset":
|
||||||
this.onMouseDown(ev.pos);
|
this.onMouseClick(ev.pos);
|
||||||
break;
|
break;
|
||||||
case "MouseMoveOffset": {
|
case "MouseMoveOffset": {
|
||||||
if (!this.cx.connectingWire) {
|
if (!this.cx.connectingWire) {
|
||||||
@ -545,21 +576,25 @@ class Wiring implements State {
|
|||||||
this.unsubscribe();
|
this.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMouseDown(pos: V2): void {
|
private onMouseClick(pos: V2): void {
|
||||||
|
this.cx.connectingWire?.move(pos);
|
||||||
if (
|
if (
|
||||||
this.cx.board.handleMouseClick(pos, {
|
this.cx.board.handleMouseClick(pos, {
|
||||||
onInputPinClicked: (comp, i) => {
|
onInputPinClicked: (comp, i) => {
|
||||||
this.cx.connectingWire!.connectToInput(this.cx.board, comp, i);
|
this.cx.connectingWire!.connectToInput(this.cx.board, comp, i);
|
||||||
|
this.cx.events.send({ tag: "SaveRequest" });
|
||||||
this.cx.connectingWire = null;
|
this.cx.connectingWire = null;
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
},
|
},
|
||||||
onOutputPinClicked: (comp, i) => {
|
onOutputPinClicked: (comp, i) => {
|
||||||
this.cx.connectingWire!.connectToOutput(this.cx.board, comp, i);
|
this.cx.connectingWire!.connectToOutput(this.cx.board, comp, i);
|
||||||
|
this.cx.events.send({ tag: "SaveRequest" });
|
||||||
this.cx.connectingWire = null;
|
this.cx.connectingWire = null;
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
},
|
},
|
||||||
onJointClicked: (joint) => {
|
onJointClicked: (joint) => {
|
||||||
this.cx.connectingWire!.connectToJoint(this.cx.board, joint);
|
this.cx.connectingWire!.connectToJoint(this.cx.board, joint);
|
||||||
|
this.cx.events.send({ tag: "SaveRequest" });
|
||||||
this.cx.connectingWire = null;
|
this.cx.connectingWire = null;
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { EventBus, EventUnsub } from "./events";
|
import type { EventBus, EventUnsub } from "./events";
|
||||||
import { v2, type V2 } from "./V2";
|
import { v2, type V2 } from "./V2";
|
||||||
|
|
||||||
const doubleClickDelay = 200;
|
const doubleClickDelay = 100;
|
||||||
|
|
||||||
export class Mouse {
|
export class Mouse {
|
||||||
private state: State;
|
private state: State;
|
||||||
@ -103,6 +103,7 @@ class FirstRelease implements State {
|
|||||||
private unsubscribe: EventUnsub;
|
private unsubscribe: EventUnsub;
|
||||||
|
|
||||||
private timeout: ReturnType<typeof setTimeout>;
|
private timeout: ReturnType<typeof setTimeout>;
|
||||||
|
private totalDelta = v2(0, 0);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cx: Mouse,
|
private cx: Mouse,
|
||||||
@ -116,6 +117,11 @@ class FirstRelease implements State {
|
|||||||
this.cx.transitionTo(new SecondPress(this.cx, this.pos));
|
this.cx.transitionTo(new SecondPress(this.cx, this.pos));
|
||||||
break;
|
break;
|
||||||
case "MouseMove":
|
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;
|
break;
|
||||||
case "MouseLeave":
|
case "MouseLeave":
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
@ -127,6 +133,7 @@ class FirstRelease implements State {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.timeout = setTimeout(() => {
|
this.timeout = setTimeout(() => {
|
||||||
|
this.cx.eventBus.send({ tag: "MouseClick", pos: this.pos });
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
}, doubleClickDelay);
|
}, doubleClickDelay);
|
||||||
}
|
}
|
||||||
@ -205,8 +212,6 @@ class Dragging implements State {
|
|||||||
case "MouseLeave":
|
case "MouseLeave":
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
break;
|
break;
|
||||||
default:
|
|
||||||
throw new Error(`unexpected event ${ev.tag}`);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { Board, ComponentRepo, type Component } from "./Board";
|
import { Board, Component, ComponentRepo } from "./Board";
|
||||||
import { type EventBus } from "./events";
|
import { type EventBus } from "./events";
|
||||||
|
import * as ser from "./serialize";
|
||||||
|
|
||||||
export class Project {
|
export class Project {
|
||||||
private current: BoardEditor;
|
private current: BoardEditor;
|
||||||
@ -8,15 +9,16 @@ export class Project {
|
|||||||
private constructor(
|
private constructor(
|
||||||
private events: EventBus,
|
private events: EventBus,
|
||||||
private boardEditors: BoardEditor[],
|
private boardEditors: BoardEditor[],
|
||||||
private components: Component[],
|
|
||||||
public componentRepo: ComponentRepo,
|
public componentRepo: ComponentRepo,
|
||||||
|
private savedBoards: Map<string, ser.Board>,
|
||||||
) {
|
) {
|
||||||
this.current = boardEditors[this.selectedIdx];
|
this.current = boardEditors[this.selectedIdx];
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadLocalStoreOrInitNew(events: EventBus): Project {
|
static loadLocalStoreOrInitNew(events: EventBus): Project {
|
||||||
|
// globalThis.localStorage.removeItem("nandsim");
|
||||||
if (globalThis.localStorage.getItem("nandsim")) {
|
if (globalThis.localStorage.getItem("nandsim")) {
|
||||||
return this.loadLocalStorage();
|
return this.loadLocalStorage(events);
|
||||||
} else {
|
} else {
|
||||||
return this.initNew(events);
|
return this.initNew(events);
|
||||||
}
|
}
|
||||||
@ -31,13 +33,57 @@ export class Project {
|
|||||||
board: Board.withExample(repo),
|
board: Board.withExample(repo),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[],
|
|
||||||
repo,
|
repo,
|
||||||
|
new Map(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadLocalStorage(): Project {
|
static loadLocalStorage(events: EventBus): Project {
|
||||||
throw new Error("not implemented");
|
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 {
|
currentBoard(): Board {
|
||||||
@ -70,19 +116,21 @@ export class Project {
|
|||||||
this.events.send({ tag: "ShowSelectedTool", tool: this.current.name });
|
this.events.send({ tag: "ShowSelectedTool", tool: this.current.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
closeTab(idx: number) {
|
closeTab(): number {
|
||||||
this.boardEditors.splice(idx, 1);
|
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) {
|
if (this.boardEditors.length === 0) {
|
||||||
this.newTab();
|
this.newTab();
|
||||||
}
|
}
|
||||||
this.selectedIdx = 0;
|
return 0;
|
||||||
this.current = this.boardEditors[this.selectedIdx];
|
|
||||||
this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renameComponent(newName: string) {
|
renameComponent(newName: string) {
|
||||||
this.current.name = newName;
|
this.current.name = newName;
|
||||||
this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx });
|
this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx });
|
||||||
|
this.events.send({ tag: "ShowSelectedTool", tool: this.current.name });
|
||||||
}
|
}
|
||||||
|
|
||||||
saveComponent() {
|
saveComponent() {
|
||||||
@ -92,6 +140,24 @@ export class Project {
|
|||||||
);
|
);
|
||||||
this.events.send({ tag: "ShowSelectedTool", tool: this.current.name });
|
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 = {
|
type BoardEditor = {
|
||||||
|
|||||||
@ -64,7 +64,11 @@ export class Renderer {
|
|||||||
c.strokeRect(x, y, w, h);
|
c.strokeRect(x, y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawComponentBody(pos: V2, kind: ComponentKind) {
|
private drawComponentBodyInternal(
|
||||||
|
pos: V2,
|
||||||
|
kind: ComponentKind,
|
||||||
|
label: string,
|
||||||
|
) {
|
||||||
const { c, offset } = this;
|
const { c, offset } = this;
|
||||||
const { x, y } = pos.add(offset);
|
const { x, y } = pos.add(offset);
|
||||||
const { x: w, y: h } = kind.size;
|
const { x: w, y: h } = kind.size;
|
||||||
@ -77,14 +81,18 @@ export class Renderer {
|
|||||||
|
|
||||||
c.fillStyle = `#333333`;
|
c.fillStyle = `#333333`;
|
||||||
c.font = "bold 16px monospace";
|
c.font = "bold 16px monospace";
|
||||||
const textMetrix = c.measureText(kind.label);
|
const textMetrix = c.measureText(label);
|
||||||
c.fillText(
|
c.fillText(
|
||||||
kind.label,
|
label,
|
||||||
x + w / 2 - textMetrix.width / 2,
|
x + w / 2 - textMetrix.width / 2,
|
||||||
y + 13 + h / 2 - 16 / 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 { c, offset } = this;
|
||||||
const { x, y } = pos.add(offset);
|
const { x, y } = pos.add(offset);
|
||||||
const { x: w, y: h } = kind.size;
|
const { x: w, y: h } = kind.size;
|
||||||
@ -97,14 +105,58 @@ export class Renderer {
|
|||||||
|
|
||||||
c.fillStyle = `#333333`;
|
c.fillStyle = `#333333`;
|
||||||
c.font = "bold 16px monospace";
|
c.font = "bold 16px monospace";
|
||||||
const textMetrix = c.measureText(kind.label);
|
const textMetrix = c.measureText(label);
|
||||||
c.fillText(
|
c.fillText(
|
||||||
kind.label,
|
label,
|
||||||
x + w / 2 - textMetrix.width / 2,
|
x + w / 2 - textMetrix.width / 2,
|
||||||
y + 13 + h / 2 - 16 / 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) {
|
drawComponentInputPin(pos: V2, pinOffset: number) {
|
||||||
const { c, offset } = this;
|
const { c, offset } = this;
|
||||||
const { x, y } = pos.add(offset);
|
const { x, y } = pos.add(offset);
|
||||||
@ -153,12 +205,12 @@ export class Renderer {
|
|||||||
c.stroke();
|
c.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
drawWire(begin: V2, end: V2) {
|
drawWire(begin: V2, end: V2, active: boolean) {
|
||||||
const { c, offset } = this;
|
const { c, offset } = this;
|
||||||
const { x: x0, y: y0 } = begin.add(offset);
|
const { x: x0, y: y0 } = begin.add(offset);
|
||||||
const { x: x1, y: y1 } = end.add(offset);
|
const { x: x1, y: y1 } = end.add(offset);
|
||||||
|
|
||||||
c.strokeStyle = `#333333`;
|
c.strokeStyle = active ? `#bb3333` : `#333333`;
|
||||||
c.lineWidth = 3;
|
c.lineWidth = 3;
|
||||||
c.beginPath();
|
c.beginPath();
|
||||||
c.moveTo(x0, y0);
|
c.moveTo(x0, y0);
|
||||||
|
|||||||
@ -4,6 +4,14 @@ export class V2 {
|
|||||||
public y: number,
|
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 {
|
add(other: V2): V2 {
|
||||||
return new V2(this.x + other.x, this.y + other.y);
|
return new V2(this.x + other.x, this.y + other.y);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,9 @@ export class ViewPos {
|
|||||||
public offset = v2(0, 0);
|
public offset = v2(0, 0);
|
||||||
|
|
||||||
constructor(private events: EventBus) {
|
constructor(private events: EventBus) {
|
||||||
this.events.subscribe(["MouseDown", "MouseMove"], (ev) => {
|
this.events.subscribe(
|
||||||
|
["MouseDown", "MouseMove", "MouseClick", "MouseDoubleClick"],
|
||||||
|
(ev) => {
|
||||||
const absPos = ev.pos;
|
const absPos = ev.pos;
|
||||||
const pos = this.canvasToBoard(absPos);
|
const pos = this.canvasToBoard(absPos);
|
||||||
switch (ev.tag) {
|
switch (ev.tag) {
|
||||||
@ -19,8 +21,15 @@ export class ViewPos {
|
|||||||
deltaPos: ev.deltaPos,
|
deltaPos: ev.deltaPos,
|
||||||
});
|
});
|
||||||
break;
|
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 {
|
canvasToBoard(pos: V2): V2 {
|
||||||
|
|||||||
@ -15,12 +15,13 @@ export type Event =
|
|||||||
deltaPos: V2;
|
deltaPos: V2;
|
||||||
}
|
}
|
||||||
| { tag: "KeyDown" | "KeyUp"; key: string }
|
| { tag: "KeyDown" | "KeyUp"; key: string }
|
||||||
| { tag: "SelectTool" | "ShowSelectedTool"; tool: string }
|
| { tag: "SelectTool" | "ShowSelectedTool" | "OpenTabWithTool"; tool: string }
|
||||||
| { tag: "CreateTab" }
|
| { tag: "CreateTab" }
|
||||||
| { tag: "SelectTab" | "ShowSelectedTab"; idx: number }
|
| { tag: "SelectTab" | "ShowSelectedTab"; idx: number }
|
||||||
| { tag: "MouseDownOffset"; pos: V2; absPos: V2 }
|
| { tag: "MouseDownOffset"; pos: V2; absPos: V2 }
|
||||||
| { tag: "MouseMoveOffset"; pos: V2; deltaPos: V2 }
|
| { tag: "MouseMoveOffset"; pos: V2; deltaPos: V2 }
|
||||||
| { tag: "RenderRequest" }
|
| { tag: "MouseClickOffset" | "MouseDoubleClickOffset"; pos: V2; absPos: V2 }
|
||||||
|
| { tag: "RenderRequest" | "SimulateRequest" | "SaveRequest" }
|
||||||
| { tag: "SaveComponent" | "CloseComponent" }
|
| { tag: "SaveComponent" | "CloseComponent" }
|
||||||
| { tag: "RenameComponent"; newName: string };
|
| { tag: "RenameComponent"; newName: string };
|
||||||
|
|
||||||
|
|||||||
@ -142,7 +142,10 @@ export class ComponentBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class StmtsMutater {
|
class StmtsMutater {
|
||||||
constructor(private comp: Component) {}
|
constructor(
|
||||||
|
private comp: Component,
|
||||||
|
private replacedStates: [State, State][],
|
||||||
|
) {}
|
||||||
|
|
||||||
[Symbol.iterator](): Iterator<Stmt> {
|
[Symbol.iterator](): Iterator<Stmt> {
|
||||||
return this.comp.stmts[Symbol.iterator]();
|
return this.comp.stmts[Symbol.iterator]();
|
||||||
@ -155,6 +158,7 @@ class StmtsMutater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
replaceState(oldState: State, newState: State) {
|
replaceState(oldState: State, newState: State) {
|
||||||
|
this.replacedStates.push([oldState, newState]);
|
||||||
for (const stmt of this.comp.stmts) {
|
for (const stmt of this.comp.stmts) {
|
||||||
stmt.replaceState(oldState, newState);
|
stmt.replaceState(oldState, newState);
|
||||||
}
|
}
|
||||||
@ -177,7 +181,10 @@ class StmtsMutater {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ComponentOptimizer {
|
export class ComponentOptimizer {
|
||||||
constructor(private comp: Component) {}
|
constructor(
|
||||||
|
private comp: Component,
|
||||||
|
private replacedStates: [State, State][],
|
||||||
|
) {}
|
||||||
|
|
||||||
optimize() {
|
optimize() {
|
||||||
const score = () => this.comp.stmts.length * 100 + this.comp.states.length;
|
const score = () => this.comp.stmts.length * 100 + this.comp.states.length;
|
||||||
@ -196,7 +203,7 @@ export class ComponentOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
eliminateRedundantState() {
|
eliminateRedundantState() {
|
||||||
const mut = new StmtsMutater(this.comp);
|
const mut = new StmtsMutater(this.comp, this.replacedStates);
|
||||||
const immediatelyReadStateStmt = new Map<State, Stmt>();
|
const immediatelyReadStateStmt = new Map<State, Stmt>();
|
||||||
|
|
||||||
for (const [i, stmt] of this.comp.stmts.entries()) {
|
for (const [i, stmt] of this.comp.stmts.entries()) {
|
||||||
@ -226,7 +233,7 @@ export class ComponentOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
moveSetStateToSource() {
|
moveSetStateToSource() {
|
||||||
const mut = new StmtsMutater(this.comp);
|
const mut = new StmtsMutater(this.comp, this.replacedStates);
|
||||||
|
|
||||||
for (const [baseIdx, stmt] of this.comp.stmts.entries()) {
|
for (const [baseIdx, stmt] of this.comp.stmts.entries()) {
|
||||||
const indices = this.indexMap();
|
const indices = this.indexMap();
|
||||||
@ -249,7 +256,7 @@ export class ComponentOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
collapseStates() {
|
collapseStates() {
|
||||||
const mut = new StmtsMutater(this.comp);
|
const mut = new StmtsMutater(this.comp, this.replacedStates);
|
||||||
|
|
||||||
const sourceStates = new MultiMap<Stmt, State>();
|
const sourceStates = new MultiMap<Stmt, State>();
|
||||||
for (const stmt of this.comp.stmts) {
|
for (const stmt of this.comp.stmts) {
|
||||||
@ -266,7 +273,7 @@ export class ComponentOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
eliminateUnusedStates() {
|
eliminateUnusedStates() {
|
||||||
const mut = new StmtsMutater(this.comp);
|
const mut = new StmtsMutater(this.comp, this.replacedStates);
|
||||||
|
|
||||||
const usedStates = new Set<State>();
|
const usedStates = new Set<State>();
|
||||||
for (const stmt of mut) {
|
for (const stmt of mut) {
|
||||||
@ -287,7 +294,7 @@ export class ComponentOptimizer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
eliminateRedundantSetState() {
|
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) {
|
for (let i = this.comp.stmts.length - 1; i > 0; --i) {
|
||||||
const [first, second] = this.comp.stmts.slice(i - 1, i + 1);
|
const [first, second] = this.comp.stmts.slice(i - 1, i + 1);
|
||||||
|
|||||||
49
editor/src/editor/serialize.ts
Normal file
49
editor/src/editor/serialize.ts
Normal 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];
|
||||||
@ -5,22 +5,25 @@ export class Sim {
|
|||||||
private comp: ir.Component,
|
private comp: ir.Component,
|
||||||
private inputs: boolean[],
|
private inputs: boolean[],
|
||||||
private outputs: boolean[],
|
private outputs: boolean[],
|
||||||
|
private state: Map<ir.State, boolean>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
simulate() {
|
simulate() {
|
||||||
const { comp, inputs, outputs } = this;
|
const { comp, inputs, outputs } = this;
|
||||||
|
|
||||||
const stmtIdcs = new Map(comp.stmts.map((stmt, i) => [stmt, i]));
|
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 regs = new Array<boolean>(comp.stmts.length).fill(false);
|
||||||
|
|
||||||
|
const stateDependents = new Map<ir.State, number>();
|
||||||
|
|
||||||
const operation = <Ops extends ir.Stmt[]>(
|
const operation = <Ops extends ir.Stmt[]>(
|
||||||
action: (...ops: boolean[]) => boolean,
|
action: (...ops: boolean[]) => boolean,
|
||||||
...ops: Ops
|
...ops: Ops
|
||||||
) => action(...ops.map((op) => regs[stmtIdcs.get(op)!]));
|
) => 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;
|
const k = stmt.kind;
|
||||||
switch (k.tag) {
|
switch (k.tag) {
|
||||||
case "Null":
|
case "Null":
|
||||||
@ -30,14 +33,29 @@ export class Sim {
|
|||||||
regs[i] = inputs[k.i];
|
regs[i] = inputs[k.i];
|
||||||
break;
|
break;
|
||||||
case "Output":
|
case "Output":
|
||||||
outputs[k.i] = regs[i];
|
outputs[k.i] = regs[stmtIdcs.get(k.src)!];
|
||||||
break;
|
break;
|
||||||
case "GetState":
|
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;
|
break;
|
||||||
case "SetState":
|
case "SetState": {
|
||||||
state.set(k.state, regs[i]);
|
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;
|
break;
|
||||||
|
}
|
||||||
case "Not":
|
case "Not":
|
||||||
regs[i] = operation((v) => !v, k.op);
|
regs[i] = operation((v) => !v, k.op);
|
||||||
break;
|
break;
|
||||||
@ -50,6 +68,12 @@ export class Sim {
|
|||||||
case "Component":
|
case "Component":
|
||||||
throw new Error("not implemented");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,6 @@
|
|||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
text-transform: capitalize;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
@ -61,7 +60,6 @@
|
|||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
text-transform: capitalize;
|
|
||||||
|
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user