refactor to use new events

This commit is contained in:
sfja 2026-06-10 02:54:59 +02:00
parent cc1ef05b95
commit 9def12cad9
8 changed files with 279 additions and 256 deletions

View File

@ -27,30 +27,30 @@ function Canvas({ editor, canvasRef, width, height }: Props): ReactElement {
tabIndex={0} tabIndex={0}
onMouseDown={(ev) => { onMouseDown={(ev) => {
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY); const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
editor.mouseDown(pos); editor.events.send({ tag: "MouseDown", pos });
editor.renderIfNeeded(ev.target as HTMLCanvasElement); editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}} }}
onMouseUp={(ev) => { onMouseUp={(ev) => {
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY); const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
editor.mouseUp(pos); editor.events.send({ tag: "MouseUp", pos });
editor.renderIfNeeded(ev.target as HTMLCanvasElement); editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}} }}
onMouseMove={(ev) => { onMouseMove={(ev) => {
const deltaPos = v2(ev.movementX, ev.movementY); const deltaPos = v2(ev.movementX, ev.movementY);
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY); const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
editor.mouseMove(deltaPos, pos); editor.events.send({ tag: "MouseMove", pos, deltaPos });
editor.renderIfNeeded(ev.target as HTMLCanvasElement); editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}} }}
onMouseLeave={(ev) => { onMouseLeave={(ev) => {
editor.mouseLeave(); editor.events.send({ tag: "MouseLeave" });
editor.renderIfNeeded(ev.target as HTMLCanvasElement); editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}} }}
onKeyDown={(ev) => { onKeyDown={(ev) => {
editor.keyDown(ev.key); editor.events.send({ tag: "KeyDown", key: ev.key });
editor.renderIfNeeded(ev.target as HTMLCanvasElement); editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}} }}
onKeyUp={(ev) => { onKeyUp={(ev) => {
editor.keyUp(ev.key); editor.events.send({ tag: "KeyUp", key: ev.key });
editor.renderIfNeeded(ev.target as HTMLCanvasElement); editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}} }}
/> />

View File

@ -3,28 +3,24 @@ import type { Editor } from "./editor/Editor";
type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null> }; type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null> };
function useUpdate(): [number, () => void] {
const [value, setValue] = useState(0);
return [value, () => setValue(value + 1)] as const;
}
function Toolbar({ editor, canvasRef }: Props): ReactElement { function Toolbar({ editor, canvasRef }: Props): ReactElement {
const [uid, update] = useUpdate(); const [selectedTool, setSelectedTool] = useState("select");
useEffect(() => { useEffect(() =>
const handle = editor.addUpdateAction(() => update()); editor.events.subscribe(["ShowSelectedTool"], (ev) => {
return () => editor.removeUpdateAction(handle); setSelectedTool(ev.tool);
}); }),
);
return ( return (
<> <>
<div className="Toolbar"> <div className="Toolbar">
{editor.tools().map((tool, key) => ( {editor.tools().map((tool, key) => (
<button <button
key={`${uid}${key}`} key={`${key}`}
className={editor.selectedTool() === tool ? "active" : ""} className={selectedTool === tool ? "active" : ""}
onClick={() => { onClick={() => {
editor.selectTool(tool); editor.events.send({ tag: "SelectTool", tool });
canvasRef.current?.focus(); canvasRef.current?.focus();
}} }}
> >

View File

@ -18,8 +18,8 @@ export type Tool = string;
export class Cx { export class Cx {
public offset = v2(0, 0); public offset = v2(0, 0);
private renderNeeded = false; private renderNeeded = false;
private state = new states.Normal(this) as states.State; private state = new states.Normal(this) as states.State;
private updateActions: (() => void)[] = [];
public selectionBox: SelectionBox | null = null; public selectionBox: SelectionBox | null = null;
private componentPlacer: ComponentPlacer | null = null; private componentPlacer: ComponentPlacer | null = null;
@ -31,8 +31,30 @@ export class Cx {
public keysPressed = new Set<string>(); public keysPressed = new Set<string>();
public eventBus = new EventBus(); public mouse: Mouse;
public mouse = new Mouse(this.eventBus);
constructor(public events: EventBus) {
this.mouse = new Mouse(this.events);
this.state.enter();
this.events.subscribe(
["MouseDown", "MouseUp", "MouseMove", "KeyDown", "KeyUp", "SelectTool"],
(ev) => {
switch (ev.tag) {
case "KeyDown":
this.keysPressed.add(ev.key);
break;
case "KeyUp":
this.keysPressed.delete(ev.key);
break;
case "SelectTool":
this.onSelectTool(ev.tool);
}
this.renderNeeded = true;
},
);
}
render(canvas: HTMLCanvasElement) { render(canvas: HTMLCanvasElement) {
const r = new Renderer(canvas, this.offset); const r = new Renderer(canvas, this.offset);
@ -52,29 +74,7 @@ export class Cx {
} }
} }
mouseDown(pos: V2) { private onSelectTool(tool: Tool) {
this.state.onMouseDown?.(pos);
this.renderNeeded = true;
}
mouseUp(pos: V2) {
this.state.onMouseUp?.(pos);
this.renderNeeded = true;
}
mouseMove(deltaPos: V2, pos: V2) {
this.state.onMouseMove?.(deltaPos, pos);
this.renderNeeded = true;
}
keyDown(key: string) {
this.keysPressed.add(key);
this.state.onKeyDown?.(key);
this.renderNeeded = true;
}
keyUp(key: string) {
this.keysPressed.delete(key);
this.state.onKeyUp?.(key);
this.renderNeeded = true;
}
selectTool(tool: Tool) {
switch (tool) { switch (tool) {
case "pan": case "pan":
this.transitionTo(new states.Panning(this)); this.transitionTo(new states.Panning(this));
@ -89,34 +89,14 @@ export class Cx {
default: default:
this.transitionTo(new states.Normal(this)); this.transitionTo(new states.Normal(this));
} }
} this.events.send({ tag: "ShowSelectedTool", tool });
selectedTool(): Tool {
return this.state.selectedTool?.() || "select";
}
addUpdateAction(action: () => void): object {
this.updateActions.push(action);
return action;
}
removeUpdateAction(actionId: object) {
this.updateActions = this.updateActions.filter(
(action) => action !== actionId,
);
} }
transitionTo(newState: states.State) { transitionTo(newState: states.State) {
this.state.leaveState?.(); this.state.leave();
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.enter();
this.notifyListeners();
}
notifyListeners() {
for (const action of this.updateActions) {
action();
}
} }
moveOffset(deltaPos: V2) { moveOffset(deltaPos: V2) {

View File

@ -1,8 +1,10 @@
import { Cx, type Tool } from "./Cx"; import { Cx, type Tool } from "./Cx";
import { EventBus } from "./events";
import { V2 } from "./V2"; import { V2 } from "./V2";
export class Editor { export class Editor {
private cx = new Cx(); public events = new EventBus();
private cx = new Cx(this.events);
render(canvas: HTMLCanvasElement) { render(canvas: HTMLCanvasElement) {
this.cx.render(canvas); this.cx.render(canvas);
@ -12,50 +14,7 @@ export class Editor {
this.cx.renderIfNeeded(canvas); this.cx.renderIfNeeded(canvas);
} }
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);
}
keyUp(key: string) {
this.cx.keyUp(key);
}
selectTool(tool: Tool) {
this.cx.selectTool(tool);
}
selectedTool(): Tool | null {
return this.cx.selectedTool();
}
tools(): Tool[] { tools(): Tool[] {
return ["select", "pan", "input", "output", "and", "or", "not"]; return ["select", "pan", "input", "output", "and", "or", "not"];
} }
addUpdateAction(action: () => void): object {
return this.cx.addUpdateAction(action);
}
removeUpdateAction(actionId: object) {
this.cx.removeUpdateAction(actionId);
}
} }

View File

@ -24,8 +24,6 @@ class Normal implements State {
private unsubscribe: EventUnsub; private unsubscribe: EventUnsub;
constructor(private cx: Mouse) { constructor(private cx: Mouse) {
console.log("Mouse::Normal");
this.unsubscribe = cx.eventBus.subscribe( this.unsubscribe = cx.eventBus.subscribe(
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"], ["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
(ev) => { (ev) => {
@ -61,8 +59,6 @@ class FirstPress implements State {
private cx: Mouse, private cx: Mouse,
private pos: V2, private pos: V2,
) { ) {
console.log("Mouse::FirstPress");
this.unsubscribe = cx.eventBus.subscribe( this.unsubscribe = cx.eventBus.subscribe(
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"], ["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
(ev) => { (ev) => {
@ -112,8 +108,6 @@ class FirstRelease implements State {
private cx: Mouse, private cx: Mouse,
private pos: V2, private pos: V2,
) { ) {
console.log("Mouse::FirstRelease");
this.unsubscribe = cx.eventBus.subscribe( this.unsubscribe = cx.eventBus.subscribe(
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"], ["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
(ev) => { (ev) => {
@ -121,14 +115,13 @@ class FirstRelease implements State {
case "MouseDown": case "MouseDown":
this.cx.transitionTo(new SecondPress(this.cx, this.pos)); this.cx.transitionTo(new SecondPress(this.cx, this.pos));
break; break;
case "MouseUp":
break;
case "MouseMove": case "MouseMove":
break; break;
case "MouseLeave": case "MouseLeave":
this.cx.transitionTo(new Normal(this.cx));
break; break;
default: default:
throw new Error("invalid state"); throw new Error(`unexpected event ${ev.tag}`);
} }
}, },
); );
@ -153,8 +146,6 @@ class SecondPress implements State {
private cx: Mouse, private cx: Mouse,
private pos: V2, private pos: V2,
) { ) {
console.log("Mouse::SecondPress");
this.unsubscribe = cx.eventBus.subscribe( this.unsubscribe = cx.eventBus.subscribe(
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"], ["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
(ev) => { (ev) => {
@ -179,7 +170,7 @@ class SecondPress implements State {
this.cx.transitionTo(new Normal(this.cx)); this.cx.transitionTo(new Normal(this.cx));
break; break;
default: default:
throw new Error("invalid state"); throw new Error(`unexpected event ${ev.tag}`);
} }
}, },
); );
@ -194,8 +185,6 @@ class Dragging implements State {
private unsubscribe: EventUnsub; private unsubscribe: EventUnsub;
constructor(private cx: Mouse) { constructor(private cx: Mouse) {
console.log("Mouse::Dragging");
this.unsubscribe = cx.eventBus.subscribe( this.unsubscribe = cx.eventBus.subscribe(
["MouseDown", "MouseUp", "MouseMove", "MouseLeave"], ["MouseDown", "MouseUp", "MouseMove", "MouseLeave"],
(ev) => { (ev) => {
@ -217,7 +206,7 @@ class Dragging implements State {
this.cx.transitionTo(new Normal(this.cx)); this.cx.transitionTo(new Normal(this.cx));
break; break;
default: default:
throw new Error("invalid state"); throw new Error(`unexpected event ${ev.tag}`);
} }
}, },
); );

View File

@ -24,6 +24,10 @@ export class V2 {
abs(): V2 { abs(): V2 {
return new V2(Math.abs(this.x), Math.abs(this.y)); return new V2(Math.abs(this.x), Math.abs(this.y));
} }
toString(): string {
return `V2(${this.x}, ${this.y})`;
}
} }
export const v2 = (x: number, y: number): V2 => new V2(x, y); export const v2 = (x: number, y: number): V2 => new V2(x, y);

View File

@ -14,7 +14,8 @@ export type Event =
pos: V2; pos: V2;
deltaPos: V2; deltaPos: V2;
} }
| { tag: "MouseDrag" }; | { tag: "KeyDown" | "KeyUp"; key: string }
| { tag: "SelectTool" | "ShowSelectedTool"; tool: string };
export type EventOf<Tag extends Event["tag"]> = Event & { tag: Tag }; export type EventOf<Tag extends Event["tag"]> = Event & { tag: Tag };

View File

@ -1,30 +1,56 @@
import { Component, Joint, type ComponentKind } from "./Board"; import { Component, Joint, type ComponentKind } from "./Board";
import { ConnectingWire, Selection, type ConnectingWireKind } from "./Cx"; import { ConnectingWire, Selection, type ConnectingWireKind } from "./Cx";
import { SelectionBox, type Cx, type Tool } from "./Cx"; import { SelectionBox, type Cx, type Tool } from "./Cx";
import type { EventUnsub } from "./events";
import { v2, type V2 } from "./V2"; import { v2, type V2 } from "./V2";
export interface State { export interface State {
enterState?(): void; leave(): void;
leaveState?(): void; enter(): void;
onMouseDown?(pos: V2): void;
onMouseUp?(pos: V2): void;
onMouseMove?(deltaPos: V2, pos: V2): void;
onKeyDown?(key: string): void;
onKeyUp?(key: string): void;
selectedTool?(): Tool | null; selectedTool?(): Tool | null;
} }
export class Normal implements State { export class Normal implements State {
private dragStart = v2(0, 0); private unsubscribe!: EventUnsub;
private isMouseDown = false;
constructor(private cx: Cx) {} constructor(private cx: Cx) {}
enterState(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe(
["MouseDown", "MouseMove", "MouseDragBegin", "KeyDown"],
(ev) => {
switch (ev.tag) {
case "MouseDown":
this._onMouseDown(ev.pos);
break;
case "MouseMove":
this.cx.board.updateMouseHover(ev.pos.sub(this.cx.offset));
break;
case "MouseDragBegin": {
this.cx.selectionBox = new SelectionBox(ev.pos, ev.deltaPos);
this.cx.transitionTo(new SelectingBox(this.cx));
break;
}
case "KeyDown": {
if (ev.key === "Shift") {
this.cx.transitionTo(new Panning(this.cx));
return;
}
break;
}
}
},
);
this.cx.events.send({ tag: "ShowSelectedTool", tool: "select" });
this.cx.runSimulation(); this.cx.runSimulation();
} }
onMouseDown(pos: V2): void { leave(): void {
this.unsubscribe();
}
private _onMouseDown(pos: V2): void {
if ( if (
this.cx.board.handleMouseClick(pos.sub(this.cx.offset), { this.cx.board.handleMouseClick(pos.sub(this.cx.offset), {
onInputPinClicked: (comp, i) => { onInputPinClicked: (comp, i) => {
@ -61,26 +87,6 @@ export class Normal implements State {
}, },
}) !== "handled" }) !== "handled"
) { ) {
this.isMouseDown = true;
this.dragStart = pos;
}
}
onMouseMove(_deltaPos: V2, pos: V2): void {
if (this.isMouseDown && this.dragStart.sub(pos).len() > 5) {
this.cx.selectionBox = new SelectionBox(
this.dragStart,
pos.sub(this.dragStart),
);
this.cx.transitionTo(new SelectingBox(this.cx));
}
this.cx.board.updateMouseHover(pos.sub(this.cx.offset));
}
onKeyDown(key: string): void {
if (key === "Shift") {
this.cx.transitionTo(new Panning(this.cx));
return;
} }
} }
@ -90,36 +96,42 @@ export class Normal implements State {
} }
export class Panning implements State { export class Panning implements State {
private dragging = false; private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Cx) {}
onMouseDown(_pos: V2): void { enter(): void {
this.dragging = true; this.unsubscribe = this.cx.events.subscribe(
["MouseDragBegin", "MouseDrag", "KeyDown", "KeyUp"],
(ev) => {
switch (ev.tag) {
case "MouseDragBegin":
case "MouseDrag":
this.cx.moveOffset(ev.deltaPos);
break;
case "KeyDown": {
if (ev.key === "Escape") {
this.cx.transitionTo(new Normal(this.cx));
break;
}
break;
}
case "KeyUp": {
if (ev.key === "Shift") {
this.cx.transitionTo(new Normal(this.cx));
break;
}
break;
}
}
},
);
this.cx.events.send({ tag: "ShowSelectedTool", tool: "pan" });
} }
onMouseUp(_pos: V2): void { leave(): void {
this.dragging = false; this.unsubscribe();
}
onMouseMove(deltaPos: V2): void {
if (this.dragging) {
this.cx.moveOffset(deltaPos);
}
}
onKeyDown(key: string): void {
if (key === "Escape") {
this.cx.transitionTo(new Normal(this.cx));
return;
}
}
onKeyUp(key: string): void {
if (key === "Shift") {
this.cx.transitionTo(new Normal(this.cx));
return;
}
} }
selectedTool(): Tool | null { selectedTool(): Tool | null {
@ -128,6 +140,8 @@ export class Panning implements State {
} }
export class Placing implements State { export class Placing implements State {
private unsubscribe!: EventUnsub;
private compDef: ComponentKind; private compDef: ComponentKind;
constructor( constructor(
@ -137,31 +151,39 @@ export class Placing implements State {
this.compDef = this.cx.componentRepo.get(this.tool); this.compDef = this.cx.componentRepo.get(this.tool);
} }
enterState(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe(
["MouseDown", "MouseMove", "KeyDown"],
(ev) => {
switch (ev.tag) {
case "MouseDown": {
const boardPos = this.cx.canvasPosToBoard(ev.pos);
if (this.cx.board.canPlaceComponent(this.compDef, boardPos)) {
this.cx.board.placeComponent(this.compDef, boardPos);
this.cx.transitionTo(new Normal(this.cx));
}
break;
}
case "MouseMove":
this.cx.setComponentPlacerPos(ev.pos);
break;
case "KeyDown": {
if (ev.key === "Escape") {
this.cx.transitionTo(new Normal(this.cx));
break;
}
break;
}
}
},
);
this.cx.addComponentPlacer(v2(0, 0), this.compDef.size); this.cx.addComponentPlacer(v2(0, 0), this.compDef.size);
} }
leaveState(): void { leave(): void {
this.cx.removeComponentPlacer(); this.cx.removeComponentPlacer();
} this.unsubscribe();
onMouseDown(pos: V2): void {
const boardPos = this.cx.canvasPosToBoard(pos);
if (this.cx.board.canPlaceComponent(this.compDef, boardPos)) {
this.cx.board.placeComponent(this.compDef, boardPos);
this.cx.transitionTo(new Normal(this.cx));
}
}
onMouseMove(_deltaPos: V2, pos: V2): void {
this.cx.setComponentPlacerPos(pos);
}
onKeyDown(key: string): void {
if (key === "Escape") {
this.cx.transitionTo(new Normal(this.cx));
return;
}
} }
selectedTool(): Tool | null { selectedTool(): Tool | null {
@ -170,11 +192,51 @@ export class Placing implements State {
} }
export class Selecting implements State { export class Selecting implements State {
private unsubscribe!: EventUnsub;
private isMouseDown = false; private isMouseDown = false;
constructor(private cx: Cx) {} constructor(private cx: Cx) {}
onMouseDown(pos: V2): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe(
["MouseDown", "MouseUp", "MouseMove", "KeyDown"],
(ev) => {
switch (ev.tag) {
case "MouseDown":
this.onMouseDown(ev.pos);
break;
case "MouseUp":
this.isMouseDown = false;
break;
case "MouseMove": {
this.cx.board.updateMouseHover(ev.pos.sub(this.cx.offset));
if (this.isMouseDown) {
this.cx.transitionTo(new Moving(this.cx));
}
break;
}
case "KeyDown": {
if (ev.key === "Delete") {
if (!this.cx.selection) {
throw new Error("expected selection");
}
this.cx.board.deleteSelection(this.cx.selection);
this.cx.selection = null;
this.cx.transitionTo(new Normal(this.cx));
}
break;
}
}
},
);
}
leave(): void {
this.unsubscribe();
}
private onMouseDown(pos: V2): void {
if ( if (
this.cx.board.handleMouseClick(pos.sub(this.cx.offset), { this.cx.board.handleMouseClick(pos.sub(this.cx.offset), {
onComponentClicked: (comp) => { onComponentClicked: (comp) => {
@ -207,46 +269,60 @@ export class Selecting implements State {
this.isMouseDown = true; this.isMouseDown = true;
} }
onMouseUp(_pos: V2): void {
this.isMouseDown = false;
}
onMouseMove(_deltaPos: V2, pos: V2): void {
this.cx.board.updateMouseHover(pos.sub(this.cx.offset));
if (this.isMouseDown) {
this.cx.transitionTo(new Moving(this.cx));
}
}
onKeyDown(key: string): void {
if (key === "Delete") {
if (!this.cx.selection) {
throw new Error("expected selection");
}
this.cx.board.deleteSelection(this.cx.selection);
this.cx.selection = null;
this.cx.transitionTo(new Normal(this.cx));
}
}
} }
export class Moving implements State { export class Moving implements State {
private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Cx) {}
onMouseUp(_pos: V2): void { enter(): void {
this.cx.transitionTo(new Selecting(this.cx)); this.unsubscribe = this.cx.events.subscribe(
["MouseUp", "MouseMove"],
(ev) => {
switch (ev.tag) {
case "MouseUp":
this.cx.transitionTo(new Selecting(this.cx));
break;
case "MouseMove":
this.cx.selection?.move(ev.deltaPos);
break;
}
},
);
} }
onMouseMove(deltaPos: V2, _pos: V2): void { leave(): void {
this.cx.selection?.move(deltaPos); this.unsubscribe();
} }
} }
export class SelectingBox implements State { export class SelectingBox implements State {
private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Cx) {}
onMouseUp(_pos: V2): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe(
["MouseUp", "MouseMove"],
(ev) => {
switch (ev.tag) {
case "MouseUp":
this.onMouseUp(ev.pos);
break;
case "MouseMove":
this.cx.selectionBox?.move(ev.deltaPos);
break;
}
},
);
}
leave(): void {
this.unsubscribe();
}
private onMouseUp(_pos: V2): void {
if (!this.cx.selectionBox) { if (!this.cx.selectionBox) {
throw new Error("expected selectionBox to active"); throw new Error("expected selectionBox to active");
} }
@ -278,19 +354,50 @@ export class SelectingBox implements State {
} }
} }
onMouseMove(deltaPos: V2): void {
this.cx.selectionBox?.move(deltaPos);
}
selectedTool(): Tool | null { selectedTool(): Tool | null {
return "select"; return "select";
} }
} }
export class Wiring implements State { export class Wiring implements State {
private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Cx) {}
onMouseDown(pos: V2): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe(
["MouseDown", "MouseMove", "KeyDown"],
(ev) => {
switch (ev.tag) {
case "MouseDown":
this.onMouseDown(ev.pos);
break;
case "MouseMove": {
if (!this.cx.connectingWire) {
throw new Error("expected connectingWire to be active");
}
this.cx.connectingWire.move(ev.pos.sub(this.cx.offset));
this.cx.board.updateMouseHover(ev.pos.sub(this.cx.offset));
break;
}
case "KeyDown": {
if (ev.key === "Escape") {
this.cx.transitionTo(new Normal(this.cx));
this.cx.connectingWire = null;
return;
}
break;
}
}
},
);
}
leave(): void {
this.unsubscribe();
}
private onMouseDown(pos: V2): void {
if ( if (
this.cx.board.handleMouseClick(pos.sub(this.cx.offset), { this.cx.board.handleMouseClick(pos.sub(this.cx.offset), {
onInputPinClicked: (comp, i) => { onInputPinClicked: (comp, i) => {
@ -315,23 +422,10 @@ export class Wiring implements State {
prev: this.cx.connectingWire!, prev: this.cx.connectingWire!,
pos: pos.sub(this.cx.offset), pos: pos.sub(this.cx.offset),
}; };
this.cx.connectingWire = new ConnectingWire(kind, pos); this.cx.connectingWire = new ConnectingWire(
} kind,
} pos.sub(this.cx.offset),
);
onMouseMove(_deltaPos: V2, pos: V2): void {
if (!this.cx.connectingWire) {
throw new Error("expected connectingWire to be active");
}
this.cx.connectingWire.move(pos.sub(this.cx.offset));
this.cx.board.updateMouseHover(pos.sub(this.cx.offset));
}
onKeyDown(key: string): void {
if (key === "Escape") {
this.cx.transitionTo(new Normal(this.cx));
this.cx.connectingWire = null;
return;
} }
} }
} }