move states into editor
This commit is contained in:
parent
9a1894c6b5
commit
ae14bc3724
@ -2,20 +2,21 @@ import { Board, 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";
|
||||||
import { ConnectingWire } from "./ConnectingWire";
|
import { ConnectingWire, type ConnectingWireKind } from "./ConnectingWire";
|
||||||
import { EventBus } from "./events";
|
import { EventBus } from "./events";
|
||||||
import { Mouse } from "./Mouse";
|
import { Mouse } from "./Mouse";
|
||||||
import { Renderer } from "./Renderer";
|
import { Renderer } from "./Renderer";
|
||||||
import * as states from "./states";
|
import { v2, type V2 } from "./V2";
|
||||||
import type { V2 } from "./V2";
|
|
||||||
import { ViewPos } from "./ViewPos";
|
import { ViewPos } from "./ViewPos";
|
||||||
|
import { type ComponentKind } from "./Board";
|
||||||
|
import type { EventUnsub } from "./events";
|
||||||
|
|
||||||
export class Editor {
|
export class Editor {
|
||||||
public events = new EventBus();
|
public events = new EventBus();
|
||||||
public viewpos = new ViewPos(this.events);
|
public viewpos = new ViewPos(this.events);
|
||||||
private renderNeeded = false;
|
private renderNeeded = false;
|
||||||
|
|
||||||
private state = new states.Normal(this) as states.State;
|
private state: State = new Normal(this);
|
||||||
|
|
||||||
public selectionBox: SelectionBox | null = null;
|
public selectionBox: SelectionBox | null = null;
|
||||||
private componentPlacer: ComponentPlacer | null = null;
|
private componentPlacer: ComponentPlacer | null = null;
|
||||||
@ -71,22 +72,22 @@ export class Editor {
|
|||||||
private onSelectTool(tool: string) {
|
private onSelectTool(tool: string) {
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
case "pan":
|
case "pan":
|
||||||
this.transitionTo(new states.Panning(this));
|
this.transitionTo(new Panning(this));
|
||||||
break;
|
break;
|
||||||
case "input":
|
case "input":
|
||||||
case "output":
|
case "output":
|
||||||
case "and":
|
case "and":
|
||||||
case "or":
|
case "or":
|
||||||
case "not":
|
case "not":
|
||||||
this.transitionTo(new states.Placing(this, tool));
|
this.transitionTo(new Placing(this, tool));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
this.transitionTo(new states.Normal(this));
|
this.transitionTo(new Normal(this));
|
||||||
}
|
}
|
||||||
this.events.send({ tag: "ShowSelectedTool", tool });
|
this.events.send({ tag: "ShowSelectedTool", tool });
|
||||||
}
|
}
|
||||||
|
|
||||||
transitionTo(newState: states.State) {
|
transitionTo(newState: State) {
|
||||||
this.state.leave();
|
this.state.leave();
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
// console.log(`Entering state ${newState.constructor.name}`);
|
// console.log(`Entering state ${newState.constructor.name}`);
|
||||||
@ -122,3 +123,403 @@ export class Editor {
|
|||||||
return ["select", "pan", "input", "output", "and", "or", "not"];
|
return ["select", "pan", "input", "output", "and", "or", "not"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
leave(): void;
|
||||||
|
enter(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Normal implements State {
|
||||||
|
private unsubscribe!: EventUnsub;
|
||||||
|
|
||||||
|
constructor(private cx: Editor) {}
|
||||||
|
|
||||||
|
enter(): void {
|
||||||
|
this.unsubscribe = this.cx.events.subscribe(
|
||||||
|
["MouseDownOffset", "MouseMoveOffset", "MouseDragBegin", "KeyDown"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseDownOffset":
|
||||||
|
this.onMouseDown(ev.pos);
|
||||||
|
break;
|
||||||
|
case "MouseMoveOffset":
|
||||||
|
this.cx.board.updateMouseHover(ev.pos);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMouseDown(pos: V2): void {
|
||||||
|
this.cx.board.handleMouseClick(pos, {
|
||||||
|
onInputPinClicked: (comp, i) => {
|
||||||
|
this.cx.connectingWire = new ConnectingWire(
|
||||||
|
{ tag: "InputPin", comp, i },
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
this.cx.transitionTo(new Wiring(this.cx));
|
||||||
|
},
|
||||||
|
onOutputPinClicked: (comp, i) => {
|
||||||
|
this.cx.connectingWire = new ConnectingWire(
|
||||||
|
{ tag: "OutputPin", comp, i },
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
this.cx.transitionTo(new Wiring(this.cx));
|
||||||
|
},
|
||||||
|
onComponentClicked: (comp) => {
|
||||||
|
this.cx.selection = new Selection();
|
||||||
|
this.cx.selection.addComponent(comp);
|
||||||
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
|
},
|
||||||
|
onJointClicked: (joint) => {
|
||||||
|
if (this.cx.keysPressed.has("Control")) {
|
||||||
|
this.cx.selection = new Selection();
|
||||||
|
this.cx.selection.addJoint(joint);
|
||||||
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
|
} else {
|
||||||
|
this.cx.connectingWire = new ConnectingWire(
|
||||||
|
{ tag: "Joint", joint },
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
this.cx.transitionTo(new Wiring(this.cx));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Panning implements State {
|
||||||
|
private unsubscribe!: EventUnsub;
|
||||||
|
|
||||||
|
constructor(private cx: Editor) {}
|
||||||
|
|
||||||
|
enter(): void {
|
||||||
|
this.unsubscribe = this.cx.events.subscribe(
|
||||||
|
["MouseDragBegin", "MouseDrag", "KeyDown", "KeyUp"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseDragBegin":
|
||||||
|
case "MouseDrag":
|
||||||
|
this.cx.viewpos.move(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" });
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Placing implements State {
|
||||||
|
private unsubscribe!: EventUnsub;
|
||||||
|
|
||||||
|
private compDef: ComponentKind;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private cx: Editor,
|
||||||
|
private tool: string,
|
||||||
|
) {
|
||||||
|
this.compDef = this.cx.componentRepo.get(this.tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
enter(): void {
|
||||||
|
this.unsubscribe = this.cx.events.subscribe(
|
||||||
|
["MouseDownOffset", "MouseMove", "KeyDown"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseDownOffset": {
|
||||||
|
const boardPos = 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.cx.removeComponentPlacer();
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Selecting implements State {
|
||||||
|
private unsubscribe!: EventUnsub;
|
||||||
|
|
||||||
|
private isMouseDown = false;
|
||||||
|
|
||||||
|
constructor(private cx: Editor) {}
|
||||||
|
|
||||||
|
enter(): void {
|
||||||
|
this.unsubscribe = this.cx.events.subscribe(
|
||||||
|
["MouseDownOffset", "MouseUp", "MouseMoveOffset", "KeyDown"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseDownOffset":
|
||||||
|
this.onMouseDown(ev.pos, ev.absPos);
|
||||||
|
break;
|
||||||
|
case "MouseUp":
|
||||||
|
this.isMouseDown = false;
|
||||||
|
break;
|
||||||
|
case "MouseMoveOffset": {
|
||||||
|
this.cx.board.updateMouseHover(ev.pos);
|
||||||
|
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, absPos: V2): void {
|
||||||
|
if (
|
||||||
|
this.cx.board.handleMouseClick(pos, {
|
||||||
|
onComponentClicked: (comp) => {
|
||||||
|
if (this.cx.keysPressed.has("Control")) {
|
||||||
|
this.cx.selection?.toggleComponent(comp);
|
||||||
|
} else if (!this.cx.selection?.isComponentSelected(comp)) {
|
||||||
|
this.cx.selection = new Selection();
|
||||||
|
this.cx.selection.addComponent(comp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onJointClicked: (joint) => {
|
||||||
|
if (this.cx.keysPressed.has("Control")) {
|
||||||
|
this.cx.selection?.toggleJoint(joint);
|
||||||
|
} else if (!this.cx.selection?.isJointSelected(joint)) {
|
||||||
|
this.cx.selection = new Selection();
|
||||||
|
this.cx.selection.addJoint(joint);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}) !== "handled"
|
||||||
|
) {
|
||||||
|
if (this.cx.keysPressed.has("Control")) {
|
||||||
|
this.cx.selectionBox = new SelectionBox(absPos);
|
||||||
|
this.cx.transitionTo(new SelectingBox(this.cx));
|
||||||
|
} else {
|
||||||
|
this.cx.selection = null;
|
||||||
|
this.cx.selectionBox = new SelectionBox(absPos);
|
||||||
|
this.cx.transitionTo(new SelectingBox(this.cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isMouseDown = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Moving implements State {
|
||||||
|
private unsubscribe!: EventUnsub;
|
||||||
|
|
||||||
|
constructor(private cx: Editor) {}
|
||||||
|
|
||||||
|
enter(): void {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
leave(): void {
|
||||||
|
this.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectingBox implements State {
|
||||||
|
private unsubscribe!: EventUnsub;
|
||||||
|
|
||||||
|
constructor(private cx: Editor) {}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
throw new Error("expected selectionBox to active");
|
||||||
|
}
|
||||||
|
const { pos, size } = this.cx.selectionBox.boardRect(this.cx.viewpos);
|
||||||
|
|
||||||
|
const components = this.cx.board.componentsInRect(pos, size);
|
||||||
|
const joints = this.cx.board.jointsInRect(pos, size);
|
||||||
|
|
||||||
|
if (components.length > 0 || joints.length > 0) {
|
||||||
|
this.cx.selection ??= new Selection();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const comp of components) {
|
||||||
|
this.cx.selection?.addComponent(comp);
|
||||||
|
}
|
||||||
|
for (const joint of joints) {
|
||||||
|
this.cx.selection?.addJoint(joint);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.cx.selection) {
|
||||||
|
this.cx.selectionBox = null;
|
||||||
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
|
} else {
|
||||||
|
this.cx.selectionBox = null;
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Wiring implements State {
|
||||||
|
private unsubscribe!: EventUnsub;
|
||||||
|
|
||||||
|
constructor(private cx: Editor) {}
|
||||||
|
|
||||||
|
enter(): void {
|
||||||
|
this.unsubscribe = this.cx.events.subscribe(
|
||||||
|
["MouseDownOffset", "MouseMoveOffset", "KeyDown"],
|
||||||
|
(ev) => {
|
||||||
|
switch (ev.tag) {
|
||||||
|
case "MouseDownOffset":
|
||||||
|
this.onMouseDown(ev.pos);
|
||||||
|
break;
|
||||||
|
case "MouseMoveOffset": {
|
||||||
|
if (!this.cx.connectingWire) {
|
||||||
|
throw new Error("expected connectingWire to be active");
|
||||||
|
}
|
||||||
|
this.cx.connectingWire.move(ev.pos);
|
||||||
|
this.cx.board.updateMouseHover(ev.pos);
|
||||||
|
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 (
|
||||||
|
this.cx.board.handleMouseClick(pos, {
|
||||||
|
onInputPinClicked: (comp, i) => {
|
||||||
|
this.cx.connectingWire!.connectToInput(this.cx.board, comp, i);
|
||||||
|
this.cx.connectingWire = null;
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
},
|
||||||
|
onOutputPinClicked: (comp, i) => {
|
||||||
|
this.cx.connectingWire!.connectToOutput(this.cx.board, comp, i);
|
||||||
|
this.cx.connectingWire = null;
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
},
|
||||||
|
onJointClicked: (joint) => {
|
||||||
|
this.cx.connectingWire!.connectToJoint(this.cx.board, joint);
|
||||||
|
this.cx.connectingWire = null;
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
},
|
||||||
|
}) !== "handled"
|
||||||
|
) {
|
||||||
|
const kind: ConnectingWireKind = {
|
||||||
|
tag: "Intermediary",
|
||||||
|
prev: this.cx.connectingWire!,
|
||||||
|
pos,
|
||||||
|
};
|
||||||
|
this.cx.connectingWire = new ConnectingWire(kind, pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,407 +0,0 @@
|
|||||||
import { Component, Joint, type ComponentKind } from "./Board";
|
|
||||||
import { Selection } from "./Selection";
|
|
||||||
import { ConnectingWire, type ConnectingWireKind } from "./ConnectingWire";
|
|
||||||
import { SelectionBox } from "./SelectionBox";
|
|
||||||
import type { Editor } from "./Editor";
|
|
||||||
import type { EventUnsub } from "./events";
|
|
||||||
import { v2, type V2 } from "./V2";
|
|
||||||
|
|
||||||
export interface State {
|
|
||||||
leave(): void;
|
|
||||||
enter(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Normal implements State {
|
|
||||||
private unsubscribe!: EventUnsub;
|
|
||||||
|
|
||||||
constructor(private cx: Editor) {}
|
|
||||||
|
|
||||||
enter(): void {
|
|
||||||
this.unsubscribe = this.cx.events.subscribe(
|
|
||||||
["MouseDownOffset", "MouseMoveOffset", "MouseDragBegin", "KeyDown"],
|
|
||||||
(ev) => {
|
|
||||||
switch (ev.tag) {
|
|
||||||
case "MouseDownOffset":
|
|
||||||
this.onMouseDown(ev.pos);
|
|
||||||
break;
|
|
||||||
case "MouseMoveOffset":
|
|
||||||
this.cx.board.updateMouseHover(ev.pos);
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
leave(): void {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onMouseDown(pos: V2): void {
|
|
||||||
this.cx.board.handleMouseClick(pos, {
|
|
||||||
onInputPinClicked: (comp, i) => {
|
|
||||||
this.cx.connectingWire = new ConnectingWire(
|
|
||||||
{ tag: "InputPin", comp, i },
|
|
||||||
pos,
|
|
||||||
);
|
|
||||||
this.cx.transitionTo(new Wiring(this.cx));
|
|
||||||
},
|
|
||||||
onOutputPinClicked: (comp, i) => {
|
|
||||||
this.cx.connectingWire = new ConnectingWire(
|
|
||||||
{ tag: "OutputPin", comp, i },
|
|
||||||
pos,
|
|
||||||
);
|
|
||||||
this.cx.transitionTo(new Wiring(this.cx));
|
|
||||||
},
|
|
||||||
onComponentClicked: (comp) => {
|
|
||||||
this.cx.selection = new Selection();
|
|
||||||
this.cx.selection.addComponent(comp);
|
|
||||||
this.cx.transitionTo(new Selecting(this.cx));
|
|
||||||
},
|
|
||||||
onJointClicked: (joint) => {
|
|
||||||
if (this.cx.keysPressed.has("Control")) {
|
|
||||||
this.cx.selection = new Selection();
|
|
||||||
this.cx.selection.addJoint(joint);
|
|
||||||
this.cx.transitionTo(new Selecting(this.cx));
|
|
||||||
} else {
|
|
||||||
this.cx.connectingWire = new ConnectingWire(
|
|
||||||
{ tag: "Joint", joint },
|
|
||||||
pos,
|
|
||||||
);
|
|
||||||
this.cx.transitionTo(new Wiring(this.cx));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Panning implements State {
|
|
||||||
private unsubscribe!: EventUnsub;
|
|
||||||
|
|
||||||
constructor(private cx: Editor) {}
|
|
||||||
|
|
||||||
enter(): void {
|
|
||||||
this.unsubscribe = this.cx.events.subscribe(
|
|
||||||
["MouseDragBegin", "MouseDrag", "KeyDown", "KeyUp"],
|
|
||||||
(ev) => {
|
|
||||||
switch (ev.tag) {
|
|
||||||
case "MouseDragBegin":
|
|
||||||
case "MouseDrag":
|
|
||||||
this.cx.viewpos.move(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" });
|
|
||||||
}
|
|
||||||
|
|
||||||
leave(): void {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Placing implements State {
|
|
||||||
private unsubscribe!: EventUnsub;
|
|
||||||
|
|
||||||
private compDef: ComponentKind;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private cx: Editor,
|
|
||||||
private tool: string,
|
|
||||||
) {
|
|
||||||
this.compDef = this.cx.componentRepo.get(this.tool);
|
|
||||||
}
|
|
||||||
|
|
||||||
enter(): void {
|
|
||||||
this.unsubscribe = this.cx.events.subscribe(
|
|
||||||
["MouseDownOffset", "MouseMove", "KeyDown"],
|
|
||||||
(ev) => {
|
|
||||||
switch (ev.tag) {
|
|
||||||
case "MouseDownOffset": {
|
|
||||||
const boardPos = 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
leave(): void {
|
|
||||||
this.cx.removeComponentPlacer();
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Selecting implements State {
|
|
||||||
private unsubscribe!: EventUnsub;
|
|
||||||
|
|
||||||
private isMouseDown = false;
|
|
||||||
|
|
||||||
constructor(private cx: Editor) {}
|
|
||||||
|
|
||||||
enter(): void {
|
|
||||||
this.unsubscribe = this.cx.events.subscribe(
|
|
||||||
["MouseDownOffset", "MouseUp", "MouseMoveOffset", "KeyDown"],
|
|
||||||
(ev) => {
|
|
||||||
switch (ev.tag) {
|
|
||||||
case "MouseDownOffset":
|
|
||||||
this.onMouseDown(ev.pos, ev.absPos);
|
|
||||||
break;
|
|
||||||
case "MouseUp":
|
|
||||||
this.isMouseDown = false;
|
|
||||||
break;
|
|
||||||
case "MouseMoveOffset": {
|
|
||||||
this.cx.board.updateMouseHover(ev.pos);
|
|
||||||
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, absPos: V2): void {
|
|
||||||
if (
|
|
||||||
this.cx.board.handleMouseClick(pos, {
|
|
||||||
onComponentClicked: (comp) => {
|
|
||||||
if (this.cx.keysPressed.has("Control")) {
|
|
||||||
this.cx.selection?.toggleComponent(comp);
|
|
||||||
} else if (!this.cx.selection?.isComponentSelected(comp)) {
|
|
||||||
this.cx.selection = new Selection();
|
|
||||||
this.cx.selection.addComponent(comp);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onJointClicked: (joint) => {
|
|
||||||
if (this.cx.keysPressed.has("Control")) {
|
|
||||||
this.cx.selection?.toggleJoint(joint);
|
|
||||||
} else if (!this.cx.selection?.isJointSelected(joint)) {
|
|
||||||
this.cx.selection = new Selection();
|
|
||||||
this.cx.selection.addJoint(joint);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}) !== "handled"
|
|
||||||
) {
|
|
||||||
if (this.cx.keysPressed.has("Control")) {
|
|
||||||
this.cx.selectionBox = new SelectionBox(absPos);
|
|
||||||
this.cx.transitionTo(new SelectingBox(this.cx));
|
|
||||||
} else {
|
|
||||||
this.cx.selection = null;
|
|
||||||
this.cx.selectionBox = new SelectionBox(absPos);
|
|
||||||
this.cx.transitionTo(new SelectingBox(this.cx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isMouseDown = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Moving implements State {
|
|
||||||
private unsubscribe!: EventUnsub;
|
|
||||||
|
|
||||||
constructor(private cx: Editor) {}
|
|
||||||
|
|
||||||
enter(): void {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
leave(): void {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SelectingBox implements State {
|
|
||||||
private unsubscribe!: EventUnsub;
|
|
||||||
|
|
||||||
constructor(private cx: Editor) {}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
throw new Error("expected selectionBox to active");
|
|
||||||
}
|
|
||||||
const { pos, size } = this.cx.selectionBox.boardRect(this.cx.viewpos);
|
|
||||||
|
|
||||||
const components = this.cx.board.componentsInRect(pos, size);
|
|
||||||
const joints = this.cx.board.jointsInRect(pos, size);
|
|
||||||
|
|
||||||
if (components.length > 0 || joints.length > 0) {
|
|
||||||
this.cx.selection ??= new Selection();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const comp of components) {
|
|
||||||
this.cx.selection?.addComponent(comp);
|
|
||||||
}
|
|
||||||
for (const joint of joints) {
|
|
||||||
this.cx.selection?.addJoint(joint);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.cx.selection) {
|
|
||||||
this.cx.selectionBox = null;
|
|
||||||
this.cx.transitionTo(new Selecting(this.cx));
|
|
||||||
} else {
|
|
||||||
this.cx.selectionBox = null;
|
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Wiring implements State {
|
|
||||||
private unsubscribe!: EventUnsub;
|
|
||||||
|
|
||||||
constructor(private cx: Editor) {}
|
|
||||||
|
|
||||||
enter(): void {
|
|
||||||
this.unsubscribe = this.cx.events.subscribe(
|
|
||||||
["MouseDownOffset", "MouseMoveOffset", "KeyDown"],
|
|
||||||
(ev) => {
|
|
||||||
switch (ev.tag) {
|
|
||||||
case "MouseDownOffset":
|
|
||||||
this.onMouseDown(ev.pos);
|
|
||||||
break;
|
|
||||||
case "MouseMoveOffset": {
|
|
||||||
if (!this.cx.connectingWire) {
|
|
||||||
throw new Error("expected connectingWire to be active");
|
|
||||||
}
|
|
||||||
this.cx.connectingWire.move(ev.pos);
|
|
||||||
this.cx.board.updateMouseHover(ev.pos);
|
|
||||||
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 (
|
|
||||||
this.cx.board.handleMouseClick(pos, {
|
|
||||||
onInputPinClicked: (comp, i) => {
|
|
||||||
this.cx.connectingWire!.connectToInput(this.cx.board, comp, i);
|
|
||||||
this.cx.connectingWire = null;
|
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
|
||||||
},
|
|
||||||
onOutputPinClicked: (comp, i) => {
|
|
||||||
this.cx.connectingWire!.connectToOutput(this.cx.board, comp, i);
|
|
||||||
this.cx.connectingWire = null;
|
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
|
||||||
},
|
|
||||||
onJointClicked: (joint) => {
|
|
||||||
this.cx.connectingWire!.connectToJoint(this.cx.board, joint);
|
|
||||||
this.cx.connectingWire = null;
|
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
|
||||||
},
|
|
||||||
}) !== "handled"
|
|
||||||
) {
|
|
||||||
const kind: ConnectingWireKind = {
|
|
||||||
tag: "Intermediary",
|
|
||||||
prev: this.cx.connectingWire!,
|
|
||||||
pos,
|
|
||||||
};
|
|
||||||
this.cx.connectingWire = new ConnectingWire(kind, pos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user