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