Compare commits
2 Commits
54e82d4a8d
...
cc1ef05b95
| Author | SHA1 | Date | |
|---|---|---|---|
| cc1ef05b95 | |||
| 90bbe2794f |
716
editor/package-lock.json
generated
716
editor/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -41,6 +41,10 @@ function Canvas({ editor, canvasRef, width, height }: Props): ReactElement {
|
|||||||
editor.mouseMove(deltaPos, pos);
|
editor.mouseMove(deltaPos, pos);
|
||||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||||
}}
|
}}
|
||||||
|
onMouseLeave={(ev) => {
|
||||||
|
editor.mouseLeave();
|
||||||
|
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||||
|
}}
|
||||||
onKeyDown={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
editor.keyDown(ev.key);
|
editor.keyDown(ev.key);
|
||||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import { Renderer } from "./Renderer";
|
|||||||
import * as states from "./states";
|
import * as states from "./states";
|
||||||
import { v2, V2 } from "./V2";
|
import { v2, V2 } from "./V2";
|
||||||
import * as ir from "./ir";
|
import * as ir from "./ir";
|
||||||
|
import { Sim } from "./sim";
|
||||||
|
import { EventBus } from "./events";
|
||||||
|
import { Mouse } from "./Mouse";
|
||||||
|
|
||||||
export type Tool = string;
|
export type Tool = string;
|
||||||
|
|
||||||
@ -28,6 +31,9 @@ export class Cx {
|
|||||||
|
|
||||||
public keysPressed = new Set<string>();
|
public keysPressed = new Set<string>();
|
||||||
|
|
||||||
|
public eventBus = new EventBus();
|
||||||
|
public mouse = new Mouse(this.eventBus);
|
||||||
|
|
||||||
render(canvas: HTMLCanvasElement) {
|
render(canvas: HTMLCanvasElement) {
|
||||||
const r = new Renderer(canvas, this.offset);
|
const r = new Renderer(canvas, this.offset);
|
||||||
|
|
||||||
@ -102,7 +108,7 @@ export class Cx {
|
|||||||
transitionTo(newState: states.State) {
|
transitionTo(newState: states.State) {
|
||||||
this.state.leaveState?.();
|
this.state.leaveState?.();
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
console.log(`Entering state ${newState.constructor.name}`);
|
// console.log(`Entering state ${newState.constructor.name}`);
|
||||||
this.state.enterState?.();
|
this.state.enterState?.();
|
||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
}
|
}
|
||||||
@ -139,14 +145,14 @@ export class Cx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
runSimulation() {
|
runSimulation() {
|
||||||
const comp = this.board.toIr();
|
// const comp = this.board.toIr();
|
||||||
console.log("Before optimizing");
|
// console.log("Before optimizing");
|
||||||
console.log(...new ir.ComponentPrinter().stringifyToConsole(comp));
|
// console.log(...new ir.ComponentPrinter().stringifyToConsole(comp));
|
||||||
|
// new ir.ComponentOptimizer(comp).optimize();
|
||||||
new ir.ComponentOptimizer(comp).optimize();
|
// console.log("After optimizing");
|
||||||
|
// console.log(...new ir.ComponentPrinter().stringifyToConsole(comp));
|
||||||
console.log("After optimizing");
|
// const sim = new Sim(comp, [], []);
|
||||||
console.log(...new ir.ComponentPrinter().stringifyToConsole(comp));
|
// sim.simulate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,17 +13,24 @@ export class Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mouseDown(pos: V2) {
|
mouseDown(pos: V2) {
|
||||||
|
this.cx.eventBus.send({ tag: "MouseDown", pos });
|
||||||
this.cx.mouseDown(pos);
|
this.cx.mouseDown(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
mouseUp(pos: V2) {
|
mouseUp(pos: V2) {
|
||||||
|
this.cx.eventBus.send({ tag: "MouseUp", pos });
|
||||||
this.cx.mouseUp(pos);
|
this.cx.mouseUp(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
mouseMove(deltaPos: V2, pos: V2) {
|
mouseMove(deltaPos: V2, pos: V2) {
|
||||||
|
this.cx.eventBus.send({ tag: "MouseMove", pos, deltaPos });
|
||||||
this.cx.mouseMove(deltaPos, pos);
|
this.cx.mouseMove(deltaPos, pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mouseLeave() {
|
||||||
|
this.cx.eventBus.send({ tag: "MouseLeave" });
|
||||||
|
}
|
||||||
|
|
||||||
keyDown(key: string) {
|
keyDown(key: string) {
|
||||||
this.cx.keyDown(key);
|
this.cx.keyDown(key);
|
||||||
}
|
}
|
||||||
|
|||||||
229
editor/src/editor/Mouse.ts
Normal file
229
editor/src/editor/Mouse.ts
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import type { EventBus, EventUnsub } from "./events";
|
||||||
|
import { v2, type V2 } from "./V2";
|
||||||
|
|
||||||
|
const doubleClickDelay = 200;
|
||||||
|
|
||||||
|
export class Mouse {
|
||||||
|
private state: State;
|
||||||
|
|
||||||
|
constructor(public eventBus: EventBus) {
|
||||||
|
this.state = new Normal(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionTo(state: State) {
|
||||||
|
this.state.leave();
|
||||||
|
this.state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
leave(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Normal implements State {
|
||||||
|
private unsubscribe: EventUnsub;
|
||||||
|
|
||||||
|
constructor(private cx: Mouse) {
|
||||||
|
console.log("Mouse::Normal");
|
||||||
|
|
||||||
|
this.unsubscribe = cx.eventBus.subscribe(
|
||||||
|
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseDown":
|
||||||
|
this.cx.transitionTo(new FirstPress(this.cx, ev.pos));
|
||||||
|
break;
|
||||||
|
case "MouseUp":
|
||||||
|
break;
|
||||||
|
case "MouseMove":
|
||||||
|
break;
|
||||||
|
case "MouseLeave":
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("invalid state");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FirstPress implements State {
|
||||||
|
private unsubscribe: EventUnsub;
|
||||||
|
|
||||||
|
private time = Date.now();
|
||||||
|
private totalDelta = v2(0, 0);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cx: Mouse,
|
||||||
|
private pos: V2,
|
||||||
|
) {
|
||||||
|
console.log("Mouse::FirstPress");
|
||||||
|
|
||||||
|
this.unsubscribe = cx.eventBus.subscribe(
|
||||||
|
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseUp": {
|
||||||
|
if (Date.now() - this.time < doubleClickDelay) {
|
||||||
|
this.cx.transitionTo(new FirstRelease(this.cx, this.pos));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.cx.eventBus.send({ tag: "MouseClick", pos: ev.pos });
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "MouseMove": {
|
||||||
|
this.totalDelta = this.totalDelta.add(ev.deltaPos);
|
||||||
|
if (this.totalDelta.len() > 5) {
|
||||||
|
this.cx.eventBus.send({
|
||||||
|
tag: "MouseDragBegin",
|
||||||
|
pos: this.pos,
|
||||||
|
deltaPos: this.totalDelta,
|
||||||
|
});
|
||||||
|
this.cx.transitionTo(new Dragging(this.cx));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "MouseLeave":
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`invalid state, unexpected ${ev.tag}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FirstRelease implements State {
|
||||||
|
private unsubscribe: EventUnsub;
|
||||||
|
|
||||||
|
private timeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cx: Mouse,
|
||||||
|
private pos: V2,
|
||||||
|
) {
|
||||||
|
console.log("Mouse::FirstRelease");
|
||||||
|
|
||||||
|
this.unsubscribe = cx.eventBus.subscribe(
|
||||||
|
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseDown":
|
||||||
|
this.cx.transitionTo(new SecondPress(this.cx, this.pos));
|
||||||
|
break;
|
||||||
|
case "MouseUp":
|
||||||
|
break;
|
||||||
|
case "MouseMove":
|
||||||
|
break;
|
||||||
|
case "MouseLeave":
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("invalid state");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
}, doubleClickDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
clearTimeout(this.timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SecondPress implements State {
|
||||||
|
private unsubscribe: EventUnsub;
|
||||||
|
|
||||||
|
private totalDelta = v2(0, 0);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cx: Mouse,
|
||||||
|
private pos: V2,
|
||||||
|
) {
|
||||||
|
console.log("Mouse::SecondPress");
|
||||||
|
|
||||||
|
this.unsubscribe = cx.eventBus.subscribe(
|
||||||
|
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseUp":
|
||||||
|
this.cx.eventBus.send({ tag: "MouseDoubleClick", pos: this.pos });
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
break;
|
||||||
|
case "MouseMove":
|
||||||
|
this.totalDelta = this.totalDelta.add(ev.deltaPos);
|
||||||
|
if (this.totalDelta.len() > 5) {
|
||||||
|
this.cx.eventBus.send({
|
||||||
|
tag: "MouseDragBegin",
|
||||||
|
pos: this.pos,
|
||||||
|
deltaPos: this.totalDelta,
|
||||||
|
});
|
||||||
|
this.cx.transitionTo(new Dragging(this.cx));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "MouseLeave":
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("invalid state");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Dragging implements State {
|
||||||
|
private unsubscribe: EventUnsub;
|
||||||
|
|
||||||
|
constructor(private cx: Mouse) {
|
||||||
|
console.log("Mouse::Dragging");
|
||||||
|
|
||||||
|
this.unsubscribe = cx.eventBus.subscribe(
|
||||||
|
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseDown":
|
||||||
|
this.cx.transitionTo(new FirstPress(this.cx, ev.pos));
|
||||||
|
break;
|
||||||
|
case "MouseUp":
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
break;
|
||||||
|
case "MouseMove":
|
||||||
|
this.cx.eventBus.send({
|
||||||
|
tag: "MouseDrag",
|
||||||
|
pos: ev.pos,
|
||||||
|
deltaPos: ev.deltaPos,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "MouseLeave":
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("invalid state");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
79
editor/src/editor/events.ts
Normal file
79
editor/src/editor/events.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import type { V2 } from "./V2";
|
||||||
|
|
||||||
|
export type Event =
|
||||||
|
| { tag: "MouseDown" | "MouseUp"; pos: V2 }
|
||||||
|
| { tag: "MouseMove"; pos: V2; deltaPos: V2 }
|
||||||
|
| { tag: "MouseLeave" }
|
||||||
|
| { tag: "MouseClick" | "MouseDoubleClick"; pos: V2 }
|
||||||
|
| {
|
||||||
|
tag:
|
||||||
|
| "MouseDragBegin"
|
||||||
|
| "MouseDrag"
|
||||||
|
| "MouseDoubleDragBegin"
|
||||||
|
| "MouseDoubleDrag";
|
||||||
|
pos: V2;
|
||||||
|
deltaPos: V2;
|
||||||
|
}
|
||||||
|
| { tag: "MouseDrag" };
|
||||||
|
|
||||||
|
export type EventOf<Tag extends Event["tag"]> = Event & { tag: Tag };
|
||||||
|
|
||||||
|
export type EventUnsub = () => void;
|
||||||
|
export type EventAction = (event: Event) => void;
|
||||||
|
|
||||||
|
export class EventBus {
|
||||||
|
private eventActionsMap = new Map<Event["tag"], Set<EventAction>>();
|
||||||
|
private actionEventsMap = new Map<EventAction, Event["tag"][]>();
|
||||||
|
|
||||||
|
subscribe<Tags extends Event["tag"]>(
|
||||||
|
tags: Tags[],
|
||||||
|
action: (event: EventOf<Tags>) => void,
|
||||||
|
): EventUnsub {
|
||||||
|
this.addSubscriber<Tags>(tags, action as EventAction);
|
||||||
|
return () => {
|
||||||
|
this.removeSubscriber(action as EventAction);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private addSubscriber<Tags extends Event["tag"]>(
|
||||||
|
tags: Tags[],
|
||||||
|
action: EventAction,
|
||||||
|
) {
|
||||||
|
if (this.actionEventsMap.has(action)) {
|
||||||
|
throw new Error("action was added twice without cleanup");
|
||||||
|
}
|
||||||
|
this.actionEventsMap.set(action, tags);
|
||||||
|
|
||||||
|
for (const tag of tags) {
|
||||||
|
if (!this.eventActionsMap.has(tag)) {
|
||||||
|
this.eventActionsMap.set(tag, new Set());
|
||||||
|
}
|
||||||
|
this.eventActionsMap.get(tag)!.add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeSubscriber(action: EventAction) {
|
||||||
|
if (!this.actionEventsMap.has(action)) {
|
||||||
|
throw new Error("action was already cleaned up");
|
||||||
|
}
|
||||||
|
for (const tag of this.actionEventsMap.get(action)!) {
|
||||||
|
this.eventActionsMap.get(tag)?.delete(action);
|
||||||
|
}
|
||||||
|
this.actionEventsMap.delete(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
send(event: Event) {
|
||||||
|
const actionSet = this.eventActionsMap.get(event.tag);
|
||||||
|
if (!actionSet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const actions = [...actionSet];
|
||||||
|
for (const action of actions) {
|
||||||
|
if (!this.actionEventsMap.has(action)) {
|
||||||
|
// has been unsubscribed by prior action
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
action(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
55
editor/src/editor/sim.ts
Normal file
55
editor/src/editor/sim.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import * as ir from "./ir";
|
||||||
|
|
||||||
|
export class Sim {
|
||||||
|
constructor(
|
||||||
|
private comp: ir.Component,
|
||||||
|
private inputs: boolean[],
|
||||||
|
private outputs: 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 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()) {
|
||||||
|
const k = stmt.kind;
|
||||||
|
switch (k.tag) {
|
||||||
|
case "Null":
|
||||||
|
regs[i] = false;
|
||||||
|
break;
|
||||||
|
case "Input":
|
||||||
|
regs[i] = inputs[k.i];
|
||||||
|
break;
|
||||||
|
case "Output":
|
||||||
|
outputs[k.i] = regs[i];
|
||||||
|
break;
|
||||||
|
case "GetState":
|
||||||
|
regs[i] = state.get(k.state)!;
|
||||||
|
break;
|
||||||
|
case "SetState":
|
||||||
|
state.set(k.state, regs[i]);
|
||||||
|
break;
|
||||||
|
case "Not":
|
||||||
|
regs[i] = operation((v) => !v, k.op);
|
||||||
|
break;
|
||||||
|
case "And":
|
||||||
|
regs[i] = operation((a, b) => a && b, k.lhs, k.rhs);
|
||||||
|
break;
|
||||||
|
case "Or":
|
||||||
|
regs[i] = operation((a, b) => a || b, k.lhs, k.rhs);
|
||||||
|
break;
|
||||||
|
case "Component":
|
||||||
|
throw new Error("not implemented");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user