Compare commits

..

2 Commits

Author SHA1 Message Date
cc1ef05b95 add eventbus 2026-06-10 01:26:15 +02:00
90bbe2794f add sim 2026-06-10 01:26:07 +02:00
7 changed files with 741 additions and 373 deletions

716
editor/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,10 @@ function Canvas({ editor, canvasRef, width, height }: Props): ReactElement {
editor.mouseMove(deltaPos, pos);
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}}
onMouseLeave={(ev) => {
editor.mouseLeave();
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}}
onKeyDown={(ev) => {
editor.keyDown(ev.key);
editor.renderIfNeeded(ev.target as HTMLCanvasElement);

View File

@ -9,6 +9,9 @@ import { Renderer } from "./Renderer";
import * as states from "./states";
import { v2, V2 } from "./V2";
import * as ir from "./ir";
import { Sim } from "./sim";
import { EventBus } from "./events";
import { Mouse } from "./Mouse";
export type Tool = string;
@ -28,6 +31,9 @@ export class Cx {
public keysPressed = new Set<string>();
public eventBus = new EventBus();
public mouse = new Mouse(this.eventBus);
render(canvas: HTMLCanvasElement) {
const r = new Renderer(canvas, this.offset);
@ -102,7 +108,7 @@ export class Cx {
transitionTo(newState: states.State) {
this.state.leaveState?.();
this.state = newState;
console.log(`Entering state ${newState.constructor.name}`);
// console.log(`Entering state ${newState.constructor.name}`);
this.state.enterState?.();
this.notifyListeners();
}
@ -139,14 +145,14 @@ export class Cx {
}
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 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();
}
}

View File

@ -13,17 +13,24 @@ export class Editor {
}
mouseDown(pos: V2) {
this.cx.eventBus.send({ tag: "MouseDown", pos });
this.cx.mouseDown(pos);
}
mouseUp(pos: V2) {
this.cx.eventBus.send({ tag: "MouseUp", pos });
this.cx.mouseUp(pos);
}
mouseMove(deltaPos: V2, pos: V2) {
this.cx.eventBus.send({ tag: "MouseMove", pos, deltaPos });
this.cx.mouseMove(deltaPos, pos);
}
mouseLeave() {
this.cx.eventBus.send({ tag: "MouseLeave" });
}
keyDown(key: string) {
this.cx.keyDown(key);
}

229
editor/src/editor/Mouse.ts Normal file
View 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();
}
}

View 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
View 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");
}
}
}
}