add board

This commit is contained in:
sfja 2026-05-12 03:22:24 +02:00
parent 93bd3c7167
commit 51934b7cb9
10 changed files with 174 additions and 26 deletions

View File

@ -1,6 +1,6 @@
import { useEffect, useRef, type ReactElement, type RefObject } from "react";
import { useEffect, type ReactElement, type RefObject } from "react";
import { type Editor } from "./editor/Editor";
import { V2 } from "./editor/Cx";
import { V2 } from "./editor/V2";
type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null> };
@ -31,7 +31,9 @@ function Canvas({ editor, canvasRef }: Props): ReactElement {
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}}
onMouseMove={(ev) => {
editor.mouseMove(V2(ev.movementX, ev.movementY));
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);
}}
onKeyDown={(ev) => {

View File

@ -0,0 +1,59 @@
import { rectsCollide, type V2 } from "./V2";
export class Board {
private components: Component[] = [];
canPlaceComponent(pos: V2, size: V2): boolean {
return !this.components.some((comp) =>
rectsCollide(comp.pos, comp.size, pos, size),
);
}
placeComponent(pos: V2, size: V2, label: string) {
this.components.push({ pos, size, label });
}
render(
canvas: HTMLCanvasElement,
c: CanvasRenderingContext2D,
offset: V2,
gridSize: Readonly<V2>,
) {
for (const comp of this.components) {
const {
pos: { x, y },
size: { x: w, y: h },
} = comp;
c.fillStyle = `#0088cc`;
c.fillRect(
x * gridSize.x + offset.x,
y * gridSize.y + offset.y,
w * gridSize.x,
h * gridSize.y,
);
c.strokeStyle = `#333333`;
c.lineWidth = 2;
c.strokeRect(
x * gridSize.x + offset.x,
y * gridSize.y + offset.y,
w * gridSize.x,
h * gridSize.y,
);
c.fillStyle = `#333333`;
c.font = "bold 16px monospace";
const textMetrix = c.measureText(comp.label);
c.fillText(
comp.label,
x * gridSize.x + offset.x + (w * gridSize.x) / 2 - textMetrix.width / 2,
y * gridSize.y + offset.y + 13 + (h * gridSize.y) / 2 - 16 / 2,
);
}
}
}
type Component = {
pos: V2;
size: V2;
label: string;
};

View File

@ -1,12 +1,20 @@
import { Board } from "./Board";
import type { State } from "./State";
import { Normal } from "./states/Normal";
import { V2 } from "./V2";
export class Cx {
private offset = V2(0, 0);
private renderNeeded = false;
private state = new Normal(this) as State;
private updateActions: (() => void)[] = [];
public gridSize = Object.freeze(V2(20, 20));
private selectionRect: SelectionRect | null = null;
private componentPlacer: ComponentPlacer | null = null;
public board = new Board();
render(canvas: HTMLCanvasElement) {
const c = canvas.getContext("2d")!;
@ -15,7 +23,7 @@ export class Cx {
c.fillStyle = "#666";
c.fillRect(0, 0, canvas.width, canvas.height);
const gridSize = { x: 20, y: 20 };
const gridSize = this.gridSize;
const dotSize = { x: 2, y: 2 };
c.fillStyle = "#111";
@ -30,6 +38,8 @@ export class Cx {
}
}
this.board.render(canvas, c, this.offset, gridSize);
if (this.selectionRect) {
const {
pos: { x, y },
@ -42,6 +52,17 @@ export class Cx {
c.lineWidth = 2;
c.strokeRect(x, y, w, h);
}
if (this.componentPlacer) {
const {
pos: { x, y },
size: { x: w, y: h },
} = this.componentPlacer;
c.strokeStyle = `#ffffff`;
c.lineWidth = 2;
c.strokeRect(x - (x % gridSize.x), y - (y % gridSize.y), w, h);
}
}
renderIfNeeded(canvas: HTMLCanvasElement) {
@ -57,8 +78,8 @@ export class Cx {
mouseUp(pos: V2) {
this.state.onMouseUp?.(pos);
}
mouseMove(deltaPos: V2) {
this.state.onMouseMove?.(deltaPos);
mouseMove(deltaPos: V2, pos: V2) {
this.state.onMouseMove?.(deltaPos, pos);
}
keyDown(key: string) {
this.state.onKeyDown?.(key);
@ -67,6 +88,12 @@ export class Cx {
this.state.onKeyUp?.(key);
}
selectTool(tool: Tool) {
// this is very much a hack so that other tools
// can be selected from any tool, without me
// having to add that in every state.
if (!(this.state instanceof Normal)) {
this.transitionTo(new Normal(this));
}
this.state.selectTool?.(tool);
}
selectedTool(): Tool | null {
@ -85,7 +112,9 @@ export class Cx {
}
transitionTo(newState: State) {
this.state.leaveState?.();
this.state = newState;
this.state.enterState?.();
this.notifyListeners();
}
@ -118,14 +147,42 @@ export class Cx {
this.renderNeeded = true;
}
}
}
export type V2 = { x: number; y: number };
export const V2 = (x: number, y: number): V2 => ({ x, y });
addComponentPlacer(pos: V2, size: V2) {
this.componentPlacer = { pos, size };
this.renderNeeded = true;
}
removeComponentPlacer() {
this.componentPlacer = null;
this.renderNeeded = true;
}
setComponentPlacerPos(pos: V2) {
if (this.componentPlacer) {
this.componentPlacer.pos = pos;
this.renderNeeded = true;
}
}
canvasPosToBoard(pos: V2): V2 {
const absX = pos.x - this.offset.x;
const absY = pos.y - this.offset.y;
return V2(
(absX - (absX % this.gridSize.x)) / this.gridSize.x,
(absY - (absY % this.gridSize.y)) / this.gridSize.y,
);
}
}
export type SelectionRect = {
pos: V2;
size: V2;
};
export type ComponentPlacer = {
pos: V2;
size: V2;
};
export type Tool = "select" | "pan" | "and";

View File

@ -1,4 +1,5 @@
import { Cx, V2, type Tool } from "./Cx";
import { Cx, type Tool } from "./Cx";
import { V2 } from "./V2";
export class Editor {
private cx = new Cx();
@ -19,8 +20,8 @@ export class Editor {
this.cx.mouseUp(pos);
}
mouseMove(deltaPos: V2) {
this.cx.mouseMove(deltaPos);
mouseMove(deltaPos: V2, pos: V2) {
this.cx.mouseMove(deltaPos, pos);
}
keyDown(key: string) {

View File

@ -1,9 +1,12 @@
import type { V2, Tool } from "./Cx";
import type { Tool } from "./Cx";
import type { V2 } from "./V2";
export interface State {
enterState?(): void;
leaveState?(): void;
onMouseDown?(pos: V2): void;
onMouseUp?(pos: V2): void;
onMouseMove?(deltaPos: V2): void;
onMouseMove?(deltaPos: V2, pos: V2): void;
onKeyDown?(key: string): void;
onKeyUp?(key: string): void;
selectTool?(tool: Tool): void;

11
editor/src/editor/V2.ts Normal file
View File

@ -0,0 +1,11 @@
export type V2 = { x: number; y: number };
export const V2 = (x: number, y: number): V2 => ({ x, y });
export function rectsCollide(
{ x: ax, y: ay }: V2,
{ x: aw, y: ah }: V2,
{ x: bx, y: by }: V2,
{ x: bw, y: bh }: V2,
): boolean {
return ax < bx + bw && ax + aw > bx && ay < by + bh && ay + ah > by;
}

View File

@ -1,4 +1,5 @@
import type { Cx, Tool, V2 } from "../Cx";
import type { Cx, Tool } from "../Cx";
import type { V2 } from "../V2";
import type { State } from "../State";
import { Panning } from "./Panning";
import { Placing } from "./Placing";

View File

@ -1,4 +1,5 @@
import type { Cx, Tool, V2 } from "../Cx";
import type { Cx, Tool } from "../Cx";
import type { V2 } from "../V2";
import type { State } from "../State";
import { Normal } from "./Normal";
@ -35,11 +36,6 @@ export class Panning implements State {
}
}
selectTool(tool: Tool): void {
this.cx.transitionTo(new Normal(this.cx));
this.cx.selectTool(tool);
}
selectedTool(): Tool | null {
return "pan";
}

View File

@ -1,4 +1,5 @@
import type { Cx, Tool, V2 } from "../Cx";
import { type Cx, type Tool } from "../Cx";
import { V2 } from "../V2";
import type { State } from "../State";
import { Normal } from "./Normal";
@ -8,9 +9,25 @@ export class Placing implements State {
private tool: Tool,
) {}
onMouseUp(pos: V2): void {
this.cx.transitionTo(new Normal(this.cx));
enterState(): void {
this.cx.addComponentPlacer(V2(0, 0), V2(20 * 4, 20 * 2));
}
leaveState(): void {
this.cx.removeComponentPlacer();
}
onMouseDown(pos: V2): void {
const boardPos = this.cx.canvasPosToBoard(pos);
if (this.cx.board.canPlaceComponent(boardPos, V2(4, 2))) {
console.log("place");
this.cx.board.placeComponent(boardPos, V2(4, 2), "AND");
this.cx.transitionTo(new Normal(this.cx));
}
}
onMouseMove(_deltaPos: V2, pos: V2): void {
this.cx.setComponentPlacerPos(pos);
}
onKeyDown(key: string): void {

View File

@ -1,4 +1,5 @@
import type { Cx, Tool, V2 } from "../Cx";
import type { Cx, Tool } from "../Cx";
import type { V2 } from "../V2";
import type { State } from "../State";
import { Normal } from "./Normal";