diff --git a/editor/src/Canvas.tsx b/editor/src/Canvas.tsx index d5b68d7..50b6f42 100644 --- a/editor/src/Canvas.tsx +++ b/editor/src/Canvas.tsx @@ -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 }; @@ -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) => { diff --git a/editor/src/editor/Board.ts b/editor/src/editor/Board.ts new file mode 100644 index 0000000..5b77c2f --- /dev/null +++ b/editor/src/editor/Board.ts @@ -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, + ) { + 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; +}; diff --git a/editor/src/editor/Cx.ts b/editor/src/editor/Cx.ts index c741fad..44f9945 100644 --- a/editor/src/editor/Cx.ts +++ b/editor/src/editor/Cx.ts @@ -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"; diff --git a/editor/src/editor/Editor.ts b/editor/src/editor/Editor.ts index ca52810..b88f8cf 100644 --- a/editor/src/editor/Editor.ts +++ b/editor/src/editor/Editor.ts @@ -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) { diff --git a/editor/src/editor/State.ts b/editor/src/editor/State.ts index db72eeb..e1b9e35 100644 --- a/editor/src/editor/State.ts +++ b/editor/src/editor/State.ts @@ -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; diff --git a/editor/src/editor/V2.ts b/editor/src/editor/V2.ts new file mode 100644 index 0000000..19e7e84 --- /dev/null +++ b/editor/src/editor/V2.ts @@ -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; +} diff --git a/editor/src/editor/states/Normal.ts b/editor/src/editor/states/Normal.ts index 6e74155..ffcbc3b 100644 --- a/editor/src/editor/states/Normal.ts +++ b/editor/src/editor/states/Normal.ts @@ -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"; diff --git a/editor/src/editor/states/Panning.ts b/editor/src/editor/states/Panning.ts index 0dbab28..ebacb31 100644 --- a/editor/src/editor/states/Panning.ts +++ b/editor/src/editor/states/Panning.ts @@ -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"; } diff --git a/editor/src/editor/states/Placing.ts b/editor/src/editor/states/Placing.ts index a3f5203..8bb4b31 100644 --- a/editor/src/editor/states/Placing.ts +++ b/editor/src/editor/states/Placing.ts @@ -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)); - console.log("place"); + 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 { diff --git a/editor/src/editor/states/Selecting.ts b/editor/src/editor/states/Selecting.ts index 288c181..0b4c181 100644 --- a/editor/src/editor/states/Selecting.ts +++ b/editor/src/editor/states/Selecting.ts @@ -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";