visual editor works
This commit is contained in:
parent
6a5b051872
commit
4cadf3adbd
@ -2,7 +2,12 @@ import { useEffect, type ReactElement, type RefObject } from "react";
|
|||||||
import { type Editor } from "./editor/Editor";
|
import { type Editor } from "./editor/Editor";
|
||||||
import { v2 } from "./editor/V2";
|
import { v2 } from "./editor/V2";
|
||||||
|
|
||||||
type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null>, width: number, height: number };
|
type Props = {
|
||||||
|
editor: Editor;
|
||||||
|
canvasRef: RefObject<HTMLCanvasElement | null>;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
};
|
||||||
|
|
||||||
function Canvas({ editor, canvasRef, width, height }: Props): ReactElement {
|
function Canvas({ editor, canvasRef, width, height }: Props): ReactElement {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,9 +43,11 @@ function Canvas({ editor, canvasRef, width, height }: Props): ReactElement {
|
|||||||
}}
|
}}
|
||||||
onKeyDown={(ev) => {
|
onKeyDown={(ev) => {
|
||||||
editor.keyDown(ev.key);
|
editor.keyDown(ev.key);
|
||||||
|
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||||
}}
|
}}
|
||||||
onKeyUp={(ev) => {
|
onKeyUp={(ev) => {
|
||||||
editor.keyUp(ev.key);
|
editor.keyUp(ev.key);
|
||||||
|
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -48,7 +48,11 @@ export class Board {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const joint of this.joints) {
|
for (const joint of this.joints) {
|
||||||
|
if (selection?.isJointSelected(joint)) {
|
||||||
|
r.drawJointSelected(joint.pos);
|
||||||
|
} else {
|
||||||
r.drawJoint(joint.pos);
|
r.drawJoint(joint.pos);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hoveredOverJoint === joint) {
|
if (this.hoveredOverJoint === joint) {
|
||||||
r.drawJointHover(joint.pos);
|
r.drawJointHover(joint.pos);
|
||||||
@ -164,6 +168,9 @@ export class Board {
|
|||||||
rectsCollide(pos, size, comp.pos, comp.kind.size),
|
rectsCollide(pos, size, comp.pos, comp.kind.size),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
jointsInRect(pos: V2, size: V2): Joint[] {
|
||||||
|
return this.joints.filter((joint) => pointInsideRect(joint.pos, pos, size));
|
||||||
|
}
|
||||||
|
|
||||||
addJoint(pos: V2): Joint {
|
addJoint(pos: V2): Joint {
|
||||||
const t = new Joint(pos);
|
const t = new Joint(pos);
|
||||||
@ -175,6 +182,16 @@ export class Board {
|
|||||||
this.wires.push(wire);
|
this.wires.push(wire);
|
||||||
return wire;
|
return wire;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteSelection(selection: Selection) {
|
||||||
|
this.components = this.components.filter(
|
||||||
|
(comp) => !selection.isComponentSelected(comp),
|
||||||
|
);
|
||||||
|
this.joints = this.joints.filter(
|
||||||
|
(joint) => !selection.isJointSelected(joint),
|
||||||
|
);
|
||||||
|
this.wires = this.wires.filter((wire) => !wire.isSelected(selection));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComponentRepo {
|
export class ComponentRepo {
|
||||||
@ -296,6 +313,26 @@ export class Wire {
|
|||||||
return distance !== null && distance < 6;
|
return distance !== null && distance < 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSelected(selection: Selection): boolean {
|
||||||
|
return (
|
||||||
|
this.connectionIsSelected(this.begin, selection) ||
|
||||||
|
this.connectionIsSelected(this.end, selection)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private connectionIsSelected(
|
||||||
|
connection: WireConnection,
|
||||||
|
selection: Selection,
|
||||||
|
): boolean {
|
||||||
|
switch (connection.tag) {
|
||||||
|
case "InputPin":
|
||||||
|
case "OutputPin":
|
||||||
|
return selection.isComponentSelected(connection.comp);
|
||||||
|
case "Joint":
|
||||||
|
return selection.isJointSelected(connection.joint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
beginPos(): V2 {
|
beginPos(): V2 {
|
||||||
return this.connectionPos(this.begin);
|
return this.connectionPos(this.begin);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -175,11 +175,15 @@ export class ComponentPlacer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Selection {
|
export class Selection {
|
||||||
selectedComponents = new Set<Component>();
|
private selectedComponents = new Set<Component>();
|
||||||
|
private selectedJoints = new Set<Joint>();
|
||||||
|
|
||||||
addComponent(comp: Component) {
|
addComponent(comp: Component) {
|
||||||
this.selectedComponents.add(comp);
|
this.selectedComponents.add(comp);
|
||||||
}
|
}
|
||||||
|
addJoint(joint: Joint) {
|
||||||
|
this.selectedJoints.add(joint);
|
||||||
|
}
|
||||||
|
|
||||||
toggleComponent(comp: Component) {
|
toggleComponent(comp: Component) {
|
||||||
if (this.selectedComponents.has(comp)) {
|
if (this.selectedComponents.has(comp)) {
|
||||||
@ -188,10 +192,29 @@ export class Selection {
|
|||||||
this.selectedComponents.add(comp);
|
this.selectedComponents.add(comp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
toggleJoint(joint: Joint) {
|
||||||
|
if (this.selectedJoints.has(joint)) {
|
||||||
|
this.selectedJoints.delete(joint);
|
||||||
|
} else {
|
||||||
|
this.selectedJoints.add(joint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
isComponentSelected(comp: Component) {
|
isComponentSelected(comp: Component) {
|
||||||
return this.selectedComponents.has(comp);
|
return this.selectedComponents.has(comp);
|
||||||
}
|
}
|
||||||
|
isJointSelected(joint: Joint) {
|
||||||
|
return this.selectedJoints.has(joint);
|
||||||
|
}
|
||||||
|
|
||||||
|
move(deltaPos: V2) {
|
||||||
|
for (const comp of this.selectedComponents) {
|
||||||
|
comp.pos = comp.pos.add(deltaPos);
|
||||||
|
}
|
||||||
|
for (const joint of this.selectedJoints) {
|
||||||
|
joint.pos = joint.pos.add(deltaPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConnectingWire {
|
export class ConnectingWire {
|
||||||
@ -225,6 +248,10 @@ export class ConnectingWire {
|
|||||||
this.pushWire(board, { tag: "OutputPin", comp, i });
|
this.pushWire(board, { tag: "OutputPin", comp, i });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectToJoint(board: Board, joint: Joint) {
|
||||||
|
this.pushWire(board, { tag: "Joint", joint });
|
||||||
|
}
|
||||||
|
|
||||||
private pushWire(board: Board, end: WireConnection) {
|
private pushWire(board: Board, end: WireConnection) {
|
||||||
switch (this.kind.tag) {
|
switch (this.kind.tag) {
|
||||||
case "InputPin":
|
case "InputPin":
|
||||||
|
|||||||
@ -182,6 +182,18 @@ export class Renderer {
|
|||||||
c.fill();
|
c.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawJointSelected(pos: V2) {
|
||||||
|
const { c, offset } = this;
|
||||||
|
const { x, y } = pos.add(offset);
|
||||||
|
|
||||||
|
this.drawJoint(pos);
|
||||||
|
c.strokeStyle = `#ff8800`;
|
||||||
|
c.lineWidth = 1;
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(x, y, 5, 0, Math.PI * 2);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
drawJointHover(pos: V2) {
|
drawJointHover(pos: V2) {
|
||||||
const { c, offset } = this;
|
const { c, offset } = this;
|
||||||
const { x, y } = pos.add(offset);
|
const { x, y } = pos.add(offset);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { 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 { v2, type V2 } from "./V2";
|
import { v2, type V2 } from "./V2";
|
||||||
@ -43,11 +43,17 @@ export class Normal implements State {
|
|||||||
this.cx.transitionTo(new Selecting(this.cx));
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
},
|
},
|
||||||
onJointClicked: (joint) => {
|
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(
|
this.cx.connectingWire = new ConnectingWire(
|
||||||
{ tag: "Joint", joint },
|
{ tag: "Joint", joint },
|
||||||
pos.sub(this.cx.offset),
|
pos.sub(this.cx.offset),
|
||||||
);
|
);
|
||||||
this.cx.transitionTo(new Wiring(this.cx));
|
this.cx.transitionTo(new Wiring(this.cx));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}) !== "handled"
|
}) !== "handled"
|
||||||
) {
|
) {
|
||||||
@ -57,7 +63,7 @@ export class Normal implements State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove(_deltaPos: V2, pos: V2): void {
|
onMouseMove(_deltaPos: V2, pos: V2): void {
|
||||||
if (this.isMouseDown && this.dragStart.sub(pos).len() > 40) {
|
if (this.isMouseDown && this.dragStart.sub(pos).len() > 5) {
|
||||||
this.cx.selectionBox = new SelectionBox(
|
this.cx.selectionBox = new SelectionBox(
|
||||||
this.dragStart,
|
this.dragStart,
|
||||||
pos.sub(this.dragStart),
|
pos.sub(this.dragStart),
|
||||||
@ -160,6 +166,8 @@ export class Placing implements State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Selecting implements State {
|
export class Selecting implements State {
|
||||||
|
private isMouseDown = false;
|
||||||
|
|
||||||
constructor(private cx: Cx) {}
|
constructor(private cx: Cx) {}
|
||||||
|
|
||||||
onMouseDown(pos: V2): void {
|
onMouseDown(pos: V2): void {
|
||||||
@ -168,22 +176,67 @@ export class Selecting implements State {
|
|||||||
onComponentClicked: (comp) => {
|
onComponentClicked: (comp) => {
|
||||||
if (this.cx.keysPressed.has("Control")) {
|
if (this.cx.keysPressed.has("Control")) {
|
||||||
this.cx.selection?.toggleComponent(comp);
|
this.cx.selection?.toggleComponent(comp);
|
||||||
} else {
|
} else if (!this.cx.selection?.isComponentSelected(comp)) {
|
||||||
this.cx.selection = new Selection();
|
this.cx.selection = new Selection();
|
||||||
this.cx.selection.addComponent(comp);
|
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"
|
}) !== "handled"
|
||||||
) {
|
) {
|
||||||
if (this.cx.keysPressed.has("Control")) {
|
if (this.cx.keysPressed.has("Control")) {
|
||||||
this.cx.selectionBox = new SelectionBox(pos);
|
this.cx.selectionBox = new SelectionBox(pos);
|
||||||
this.cx.transitionTo(new SelectingBox(this.cx));
|
this.cx.transitionTo(new SelectingBox(this.cx));
|
||||||
} else {
|
} else {
|
||||||
|
this.cx.selection = null;
|
||||||
|
this.cx.selectionBox = new SelectionBox(pos);
|
||||||
|
this.cx.transitionTo(new SelectingBox(this.cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.selection = null;
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Moving implements State {
|
||||||
|
constructor(private cx: Cx) {}
|
||||||
|
|
||||||
|
onMouseUp(_pos: V2): void {
|
||||||
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseMove(deltaPos: V2, _pos: V2): void {
|
||||||
|
this.cx.selection?.move(deltaPos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SelectingBox implements State {
|
export class SelectingBox implements State {
|
||||||
@ -194,16 +247,24 @@ export class SelectingBox implements State {
|
|||||||
throw new Error("expected selectionBox to active");
|
throw new Error("expected selectionBox to active");
|
||||||
}
|
}
|
||||||
const { pos, size } = this.cx.selectionBox.normalized();
|
const { pos, size } = this.cx.selectionBox.normalized();
|
||||||
const selected = this.cx.board.componentsInRect(
|
|
||||||
|
const components = this.cx.board.componentsInRect(
|
||||||
pos.sub(this.cx.offset),
|
pos.sub(this.cx.offset),
|
||||||
size,
|
size,
|
||||||
);
|
);
|
||||||
if (selected.length > 0) {
|
const joints = this.cx.board.jointsInRect(pos.sub(this.cx.offset), size);
|
||||||
|
|
||||||
|
if (components.length > 0 || joints.length > 0) {
|
||||||
this.cx.selection ??= new Selection();
|
this.cx.selection ??= new Selection();
|
||||||
}
|
}
|
||||||
for (const comp of selected) {
|
|
||||||
|
for (const comp of components) {
|
||||||
this.cx.selection?.addComponent(comp);
|
this.cx.selection?.addComponent(comp);
|
||||||
}
|
}
|
||||||
|
for (const joint of joints) {
|
||||||
|
this.cx.selection?.addJoint(joint);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.cx.selection) {
|
if (this.cx.selection) {
|
||||||
this.cx.selectionBox = null;
|
this.cx.selectionBox = null;
|
||||||
this.cx.transitionTo(new Selecting(this.cx));
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
@ -234,7 +295,12 @@ export class Wiring implements State {
|
|||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
},
|
},
|
||||||
onOutputPinClicked: (comp, i) => {
|
onOutputPinClicked: (comp, i) => {
|
||||||
this.cx.connectingWire!.connectToInput(this.cx.board, 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.connectingWire = null;
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user