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 { 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 {
|
||||
useEffect(() => {
|
||||
@ -38,9 +43,11 @@ function Canvas({ editor, canvasRef, width, height }: Props): ReactElement {
|
||||
}}
|
||||
onKeyDown={(ev) => {
|
||||
editor.keyDown(ev.key);
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
onKeyUp={(ev) => {
|
||||
editor.keyUp(ev.key);
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -48,7 +48,11 @@ export class Board {
|
||||
}
|
||||
|
||||
for (const joint of this.joints) {
|
||||
if (selection?.isJointSelected(joint)) {
|
||||
r.drawJointSelected(joint.pos);
|
||||
} else {
|
||||
r.drawJoint(joint.pos);
|
||||
}
|
||||
|
||||
if (this.hoveredOverJoint === joint) {
|
||||
r.drawJointHover(joint.pos);
|
||||
@ -164,6 +168,9 @@ export class Board {
|
||||
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 {
|
||||
const t = new Joint(pos);
|
||||
@ -175,6 +182,16 @@ export class Board {
|
||||
this.wires.push(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 {
|
||||
@ -296,6 +313,26 @@ export class Wire {
|
||||
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 {
|
||||
return this.connectionPos(this.begin);
|
||||
}
|
||||
|
||||
@ -175,11 +175,15 @@ export class ComponentPlacer {
|
||||
}
|
||||
|
||||
export class Selection {
|
||||
selectedComponents = new Set<Component>();
|
||||
private selectedComponents = new Set<Component>();
|
||||
private selectedJoints = new Set<Joint>();
|
||||
|
||||
addComponent(comp: Component) {
|
||||
this.selectedComponents.add(comp);
|
||||
}
|
||||
addJoint(joint: Joint) {
|
||||
this.selectedJoints.add(joint);
|
||||
}
|
||||
|
||||
toggleComponent(comp: Component) {
|
||||
if (this.selectedComponents.has(comp)) {
|
||||
@ -188,10 +192,29 @@ export class Selection {
|
||||
this.selectedComponents.add(comp);
|
||||
}
|
||||
}
|
||||
toggleJoint(joint: Joint) {
|
||||
if (this.selectedJoints.has(joint)) {
|
||||
this.selectedJoints.delete(joint);
|
||||
} else {
|
||||
this.selectedJoints.add(joint);
|
||||
}
|
||||
}
|
||||
|
||||
isComponentSelected(comp: Component) {
|
||||
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 {
|
||||
@ -225,6 +248,10 @@ export class ConnectingWire {
|
||||
this.pushWire(board, { tag: "OutputPin", comp, i });
|
||||
}
|
||||
|
||||
connectToJoint(board: Board, joint: Joint) {
|
||||
this.pushWire(board, { tag: "Joint", joint });
|
||||
}
|
||||
|
||||
private pushWire(board: Board, end: WireConnection) {
|
||||
switch (this.kind.tag) {
|
||||
case "InputPin":
|
||||
|
||||
@ -182,6 +182,18 @@ export class Renderer {
|
||||
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) {
|
||||
const { c, offset } = this;
|
||||
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 { SelectionBox, type Cx, type Tool } from "./Cx";
|
||||
import { v2, type V2 } from "./V2";
|
||||
@ -43,11 +43,17 @@ export class Normal implements State {
|
||||
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.sub(this.cx.offset),
|
||||
);
|
||||
this.cx.transitionTo(new Wiring(this.cx));
|
||||
}
|
||||
},
|
||||
}) !== "handled"
|
||||
) {
|
||||
@ -57,7 +63,7 @@ export class Normal implements State {
|
||||
}
|
||||
|
||||
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.dragStart,
|
||||
pos.sub(this.dragStart),
|
||||
@ -160,6 +166,8 @@ export class Placing implements State {
|
||||
}
|
||||
|
||||
export class Selecting implements State {
|
||||
private isMouseDown = false;
|
||||
|
||||
constructor(private cx: Cx) {}
|
||||
|
||||
onMouseDown(pos: V2): void {
|
||||
@ -168,22 +176,67 @@ export class Selecting implements State {
|
||||
onComponentClicked: (comp) => {
|
||||
if (this.cx.keysPressed.has("Control")) {
|
||||
this.cx.selection?.toggleComponent(comp);
|
||||
} else {
|
||||
} 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(pos);
|
||||
this.cx.transitionTo(new SelectingBox(this.cx));
|
||||
} 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.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 {
|
||||
@ -194,16 +247,24 @@ export class SelectingBox implements State {
|
||||
throw new Error("expected selectionBox to active");
|
||||
}
|
||||
const { pos, size } = this.cx.selectionBox.normalized();
|
||||
const selected = this.cx.board.componentsInRect(
|
||||
|
||||
const components = this.cx.board.componentsInRect(
|
||||
pos.sub(this.cx.offset),
|
||||
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();
|
||||
}
|
||||
for (const comp of selected) {
|
||||
|
||||
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));
|
||||
@ -234,7 +295,12 @@ export class Wiring implements State {
|
||||
this.cx.transitionTo(new Normal(this.cx));
|
||||
},
|
||||
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.transitionTo(new Normal(this.cx));
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user