hover pins
This commit is contained in:
parent
ed36f21bb2
commit
60e589e689
@ -1,6 +1,6 @@
|
||||
import { useEffect, type ReactElement, type RefObject } from "react";
|
||||
import { type Editor } from "./editor/Editor";
|
||||
import { V2 } from "./editor/V2";
|
||||
import { v2 } from "./editor/V2";
|
||||
|
||||
type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null> };
|
||||
|
||||
@ -21,18 +21,18 @@ function Canvas({ editor, canvasRef }: Props): ReactElement {
|
||||
style={{ width: 1000, height: 1000, backgroundColor: "black" }}
|
||||
tabIndex={0}
|
||||
onMouseDown={(ev) => {
|
||||
const pos = V2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
editor.mouseDown(pos);
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
onMouseUp={(ev) => {
|
||||
const pos = V2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
editor.mouseUp(pos);
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
onMouseMove={(ev) => {
|
||||
const deltaPos = V2(ev.movementX, ev.movementY);
|
||||
const pos = V2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
const deltaPos = v2(ev.movementX, ev.movementY);
|
||||
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
editor.mouseMove(deltaPos, pos);
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { rectsCollide, V2 } from "./V2";
|
||||
import { pointInsideRect, rectsCollide, v2, V2 } from "./V2";
|
||||
|
||||
export class Board {
|
||||
private components: Component[] = [];
|
||||
|
||||
private hoveredOverInput: [Component, number] | null = null;
|
||||
private hoveredOverOutput: [Component, number] | null = null;
|
||||
|
||||
canPlaceComponent(def: ComponentDef, pos: V2): boolean {
|
||||
return !this.components.some((comp) =>
|
||||
rectsCollide(comp.pos, comp.def.size, pos, def.size),
|
||||
@ -22,26 +25,114 @@ export class Board {
|
||||
inputs,
|
||||
outputs,
|
||||
},
|
||||
pos: { x, y },
|
||||
pos,
|
||||
} = comp;
|
||||
|
||||
const [x, y] = [pos.x + offset.x, pos.y + offset.y];
|
||||
|
||||
c.fillStyle = `#6abbde`;
|
||||
c.fillRect(x + offset.x, y + offset.y, w, h);
|
||||
c.fillRect(x, y, w, h);
|
||||
c.strokeStyle = `#333333`;
|
||||
c.lineWidth = 2;
|
||||
c.strokeRect(x + offset.x, y + offset.y, w, h);
|
||||
c.strokeRect(x, y, w, h);
|
||||
|
||||
c.fillStyle = `#333333`;
|
||||
c.font = "bold 16px monospace";
|
||||
const textMetrix = c.measureText(label);
|
||||
c.fillText(
|
||||
label,
|
||||
x + offset.x + w / 2 - textMetrix.width / 2,
|
||||
y + offset.y + 13 + h / 2 - 16 / 2,
|
||||
x + w / 2 - textMetrix.width / 2,
|
||||
y + 13 + h / 2 - 16 / 2,
|
||||
);
|
||||
|
||||
const pinSpace = h / (inputs.length + 1);
|
||||
for (let i = 0; i < inputs.length; ++i) {}
|
||||
{
|
||||
const pinSpace = h / (inputs.length + 1);
|
||||
for (let i = 0; i < inputs.length; ++i) {
|
||||
if (inputs[i] !== null) {
|
||||
throw new Error("pin text not implemented");
|
||||
}
|
||||
c.fillStyle = `#333333`;
|
||||
c.beginPath();
|
||||
c.arc(x, y + (i + 1) * pinSpace, 4, 0, Math.PI * 2);
|
||||
c.fill();
|
||||
|
||||
if (
|
||||
this.hoveredOverInput?.[0] === comp &&
|
||||
this.hoveredOverInput[1] === i
|
||||
) {
|
||||
c.strokeStyle = `#bbbbbb`;
|
||||
c.lineWidth = 2;
|
||||
c.beginPath();
|
||||
c.arc(x, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2);
|
||||
c.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const pinSpace = h / (outputs.length + 1);
|
||||
for (let i = 0; i < outputs.length; ++i) {
|
||||
if (outputs[i] !== null) {
|
||||
throw new Error("pin text not implemented");
|
||||
}
|
||||
c.fillStyle = `#333333`;
|
||||
c.beginPath();
|
||||
c.arc(x + w, y + (i + 1) * pinSpace, 4, 0, Math.PI * 2);
|
||||
c.fill();
|
||||
|
||||
if (
|
||||
this.hoveredOverOutput?.[0] === comp &&
|
||||
this.hoveredOverOutput[1] === i
|
||||
) {
|
||||
c.strokeStyle = `#bbbbbb`;
|
||||
c.lineWidth = 2;
|
||||
c.beginPath();
|
||||
c.arc(x + w, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2);
|
||||
c.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateMouseHover(pos: V2) {
|
||||
this.hoveredOverInput = null;
|
||||
this.hoveredOverOutput = null;
|
||||
|
||||
for (const comp of this.components) {
|
||||
const {
|
||||
pos: { x, y },
|
||||
def: {
|
||||
size: { x: w, y: h },
|
||||
inputs,
|
||||
outputs,
|
||||
},
|
||||
} = comp;
|
||||
|
||||
if (
|
||||
!pointInsideRect(
|
||||
pos,
|
||||
comp.pos.sub(v2(5, 5)),
|
||||
comp.def.size.add(v2(10, 10)),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
{
|
||||
const pinSpace = h / (inputs.length + 1);
|
||||
for (let i = 0; i < inputs.length; ++i) {
|
||||
if (v2(x, y + (i + 1) * pinSpace).distance(pos) < 5) {
|
||||
this.hoveredOverInput = [comp, i];
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
const pinSpace = h / (outputs.length + 1);
|
||||
for (let i = 0; i < outputs.length; ++i) {
|
||||
if (v2(x + w, y + (i + 1) * pinSpace).distance(pos) < 5) {
|
||||
this.hoveredOverOutput = [comp, i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,18 +143,36 @@ export class ComponentRepo {
|
||||
static withDefaults(): ComponentRepo {
|
||||
const repo = new ComponentRepo();
|
||||
|
||||
repo.add("input", {
|
||||
label: "input",
|
||||
size: v2(80, 40),
|
||||
inputs: [],
|
||||
outputs: [null],
|
||||
});
|
||||
repo.add("output", {
|
||||
label: "output",
|
||||
size: v2(80, 40),
|
||||
inputs: [null],
|
||||
outputs: [],
|
||||
});
|
||||
repo.add("and", {
|
||||
label: "and",
|
||||
size: V2(80, 40),
|
||||
size: v2(80, 40),
|
||||
inputs: [null, null],
|
||||
outputs: [null],
|
||||
});
|
||||
repo.add("or", {
|
||||
label: "or",
|
||||
size: V2(80, 40),
|
||||
size: v2(80, 40),
|
||||
inputs: [null, null],
|
||||
outputs: [null],
|
||||
});
|
||||
repo.add("not", {
|
||||
label: "not",
|
||||
size: v2(80, 40),
|
||||
inputs: [null],
|
||||
outputs: [null],
|
||||
});
|
||||
|
||||
return repo;
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { Board, ComponentRepo } from "./Board";
|
||||
import type { State } from "./State";
|
||||
import { Normal } from "./states/Normal";
|
||||
import { V2 } from "./V2";
|
||||
import { v2, V2 } from "./V2";
|
||||
|
||||
export class Cx {
|
||||
private offset = V2(0, 0);
|
||||
public offset = v2(0, 0);
|
||||
private renderNeeded = false;
|
||||
private state = new Normal(this) as State;
|
||||
private updateActions: (() => void)[] = [];
|
||||
@ -23,7 +23,7 @@ export class Cx {
|
||||
c.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
const dotSize = { x: 2, y: 2 };
|
||||
const gridSize = V2(20, 20);
|
||||
const gridSize = v2(20, 20);
|
||||
|
||||
c.fillStyle = "#111";
|
||||
for (let y = 0; y < canvas.width / gridSize.x + 1; ++y) {
|
||||
@ -71,6 +71,10 @@ export class Cx {
|
||||
}
|
||||
}
|
||||
|
||||
setRenderNeeded() {
|
||||
this.renderNeeded = true;
|
||||
}
|
||||
|
||||
mouseDown(pos: V2) {
|
||||
this.state.onMouseDown?.(pos);
|
||||
}
|
||||
@ -130,7 +134,7 @@ export class Cx {
|
||||
}
|
||||
|
||||
addSelectionRect(pos: V2) {
|
||||
this.selectionRect = { pos, size: V2(0, 0) };
|
||||
this.selectionRect = { pos, size: v2(0, 0) };
|
||||
this.renderNeeded = true;
|
||||
}
|
||||
|
||||
@ -167,7 +171,7 @@ export class Cx {
|
||||
canvasPosToBoard(pos: V2): V2 {
|
||||
const absX = pos.x - this.offset.x;
|
||||
const absY = pos.y - this.offset.y;
|
||||
return V2(absX, absY);
|
||||
return v2(absX, absY);
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,4 +185,4 @@ export type ComponentPlacer = {
|
||||
size: V2;
|
||||
};
|
||||
|
||||
export type Tool = "select" | "pan" | "and" | "or";
|
||||
export type Tool = string;
|
||||
|
||||
@ -41,7 +41,7 @@ export class Editor {
|
||||
}
|
||||
|
||||
tools(): Tool[] {
|
||||
return ["select", "pan", "and", "or"];
|
||||
return ["select", "pan", "input", "output", "and", "or", "not"];
|
||||
}
|
||||
|
||||
addUpdateAction(action: () => void): object {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import type { Tool } from "./Cx";
|
||||
import type { V2 } from "./V2";
|
||||
import type { V2_ } from "./V2";
|
||||
|
||||
export interface State {
|
||||
enterState?(): void;
|
||||
leaveState?(): void;
|
||||
onMouseDown?(pos: V2): void;
|
||||
onMouseUp?(pos: V2): void;
|
||||
onMouseMove?(deltaPos: V2, pos: V2): void;
|
||||
onMouseDown?(pos: V2_): void;
|
||||
onMouseUp?(pos: V2_): void;
|
||||
onMouseMove?(deltaPos: V2_, pos: V2_): void;
|
||||
onKeyDown?(key: string): void;
|
||||
onKeyUp?(key: string): void;
|
||||
selectTool?(tool: Tool): void;
|
||||
|
||||
@ -1,5 +1,29 @@
|
||||
export type V2 = { x: number; y: number };
|
||||
export const V2 = (x: number, y: number): V2 => ({ x, y });
|
||||
export class V2 {
|
||||
constructor(
|
||||
public x: number,
|
||||
public y: number,
|
||||
) {}
|
||||
|
||||
add(other: V2): V2 {
|
||||
return new V2(this.x + other.x, this.y + other.y);
|
||||
}
|
||||
sub(other: V2): V2 {
|
||||
return new V2(this.x - other.x, this.y - other.y);
|
||||
}
|
||||
rsub(other: V2): V2 {
|
||||
return new V2(other.x - this.x, other.y - this.y);
|
||||
}
|
||||
|
||||
len(): number {
|
||||
return Math.sqrt(this.x ** 2 + this.y ** 2);
|
||||
}
|
||||
|
||||
distance(other: V2) {
|
||||
return this.rsub(other).len();
|
||||
}
|
||||
}
|
||||
|
||||
export const v2 = (x: number, y: number): V2 => new V2(x, y);
|
||||
|
||||
export function rectsCollide(
|
||||
{ x: ax, y: ay }: V2,
|
||||
@ -9,3 +33,11 @@ export function rectsCollide(
|
||||
): boolean {
|
||||
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
|
||||
}
|
||||
|
||||
export function pointInsideRect(
|
||||
{ x: ax, y: ay }: V2,
|
||||
{ x: bx, y: by }: V2,
|
||||
{ x: bw, y: bh }: V2,
|
||||
): boolean {
|
||||
return ax < bx + bw && ax > bx && ay < by + bh && ay > by;
|
||||
}
|
||||
|
||||
@ -13,8 +13,11 @@ export class Normal implements State {
|
||||
case "pan":
|
||||
this.cx.transitionTo(new Panning(this.cx));
|
||||
break;
|
||||
case "input":
|
||||
case "output":
|
||||
case "and":
|
||||
case "or":
|
||||
case "not":
|
||||
this.cx.transitionTo(new Placing(this.cx, tool));
|
||||
}
|
||||
}
|
||||
@ -24,6 +27,11 @@ export class Normal implements State {
|
||||
this.cx.transitionTo(new Selecting(this.cx));
|
||||
}
|
||||
|
||||
onMouseMove(_deltaPos: V2, pos: V2): void {
|
||||
this.cx.board.updateMouseHover(pos.sub(this.cx.offset));
|
||||
this.cx.setRenderNeeded();
|
||||
}
|
||||
|
||||
onKeyDown(key: string): void {
|
||||
if (key === "Shift") {
|
||||
this.cx.transitionTo(new Panning(this.cx));
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Cx, Tool } from "../Cx";
|
||||
import type { V2 } from "../V2";
|
||||
import type { V2_ } from "../V2";
|
||||
import type { State } from "../State";
|
||||
import { Normal } from "./Normal";
|
||||
|
||||
@ -8,15 +8,15 @@ export class Panning implements State {
|
||||
|
||||
constructor(private cx: Cx) {}
|
||||
|
||||
onMouseDown(_pos: V2): void {
|
||||
onMouseDown(_pos: V2_): void {
|
||||
this.dragging = true;
|
||||
}
|
||||
|
||||
onMouseUp(_pos: V2): void {
|
||||
onMouseUp(_pos: V2_): void {
|
||||
this.dragging = false;
|
||||
}
|
||||
|
||||
onMouseMove(deltaPos: V2): void {
|
||||
onMouseMove(deltaPos: V2_): void {
|
||||
if (this.dragging) {
|
||||
this.cx.moveOffset(deltaPos);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { type Cx, type Tool } from "../Cx";
|
||||
import { V2 } from "../V2";
|
||||
import { V2, v2 } from "../V2";
|
||||
import type { State } from "../State";
|
||||
import { Normal } from "./Normal";
|
||||
import type { ComponentDef } from "../Board";
|
||||
@ -15,7 +15,7 @@ export class Placing implements State {
|
||||
}
|
||||
|
||||
enterState(): void {
|
||||
this.cx.addComponentPlacer(V2(0, 0), this.compDef.size);
|
||||
this.cx.addComponentPlacer(v2(0, 0), this.compDef.size);
|
||||
}
|
||||
|
||||
leaveState(): void {
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import type { Cx, Tool } from "../Cx";
|
||||
import type { V2 } from "../V2";
|
||||
import type { V2_ } from "../V2";
|
||||
import type { State } from "../State";
|
||||
import { Normal } from "./Normal";
|
||||
|
||||
export class Selecting implements State {
|
||||
constructor(private cx: Cx) {}
|
||||
|
||||
onMouseUp(_pos: V2): void {
|
||||
onMouseUp(_pos: V2_): void {
|
||||
this.cx.removeSelectionRect();
|
||||
this.cx.transitionTo(new Normal(this.cx));
|
||||
}
|
||||
|
||||
onMouseMove(deltaPos: V2): void {
|
||||
onMouseMove(deltaPos: V2_): void {
|
||||
this.cx.moveSelectionRect(deltaPos);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user