From 93bd3c7167a9b04b5dc3d2ae9271b95f58301dc4 Mon Sep 17 00:00:00 2001 From: sfja Date: Mon, 11 May 2026 23:43:43 +0200 Subject: [PATCH] extract files --- editor/src/App.tsx | 2 +- editor/src/Canvas.tsx | 3 +- editor/src/Editor.ts | 303 -------------------------- editor/src/Toolbar.tsx | 2 +- editor/src/editor/Cx.ts | 131 +++++++++++ editor/src/editor/Editor.ts | 53 +++++ editor/src/editor/State.ts | 11 + editor/src/editor/states/Normal.ts | 35 +++ editor/src/editor/states/Panning.ts | 46 ++++ editor/src/editor/states/Placing.ts | 26 +++ editor/src/editor/states/Selecting.ts | 20 ++ 11 files changed, 326 insertions(+), 306 deletions(-) delete mode 100644 editor/src/Editor.ts create mode 100644 editor/src/editor/Cx.ts create mode 100644 editor/src/editor/Editor.ts create mode 100644 editor/src/editor/State.ts create mode 100644 editor/src/editor/states/Normal.ts create mode 100644 editor/src/editor/states/Panning.ts create mode 100644 editor/src/editor/states/Placing.ts create mode 100644 editor/src/editor/states/Selecting.ts diff --git a/editor/src/App.tsx b/editor/src/App.tsx index b65f642..4c1ab4d 100644 --- a/editor/src/App.tsx +++ b/editor/src/App.tsx @@ -1,7 +1,7 @@ import { useRef, useState, type ReactElement } from "react"; import "./style.css"; import Canvas from "./Canvas"; -import { Editor } from "./Editor"; +import { Editor } from "./editor/Editor"; import Toolbar from "./Toolbar"; function App(): ReactElement { diff --git a/editor/src/Canvas.tsx b/editor/src/Canvas.tsx index 336a039..d5b68d7 100644 --- a/editor/src/Canvas.tsx +++ b/editor/src/Canvas.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef, type ReactElement, type RefObject } from "react"; -import { V2, type Editor } from "./Editor"; +import { type Editor } from "./editor/Editor"; +import { V2 } from "./editor/Cx"; type Props = { editor: Editor; canvasRef: RefObject }; diff --git a/editor/src/Editor.ts b/editor/src/Editor.ts deleted file mode 100644 index 98849e5..0000000 --- a/editor/src/Editor.ts +++ /dev/null @@ -1,303 +0,0 @@ -export type V2 = { x: number; y: number }; -export const V2 = (x: number, y: number): V2 => ({ x, y }); - -export class Editor { - private cx = new Cx(); - - render(canvas: HTMLCanvasElement) { - this.cx.render(canvas); - } - - renderIfNeeded(canvas: HTMLCanvasElement) { - this.cx.renderIfNeeded(canvas); - } - - mouseDown(pos: V2) { - this.cx.mouseDown(pos); - } - - mouseUp(pos: V2) { - this.cx.mouseUp(pos); - } - - mouseMove(deltaPos: V2) { - this.cx.mouseMove(deltaPos); - } - - keyDown(key: string) { - this.cx.keyDown(key); - } - - keyUp(key: string) { - this.cx.keyUp(key); - } - - selectTool(tool: Tool) { - this.cx.selectTool(tool); - } - - selectedTool(): Tool | null { - return this.cx.selectedTool(); - } - - tools(): Tool[] { - return ["select", "pan", "and"]; - } - - addUpdateAction(action: () => void): object { - return this.cx.addUpdateAction(action); - } - - removeUpdateAction(actionId: object) { - this.cx.removeUpdateAction(actionId); - } -} - -class Cx { - private offset = V2(0, 0); - private renderNeeded = false; - private state = new Normal(this) as State; - private updateActions: (() => void)[] = []; - private selectionRect: SelectionRect | null = null; - - render(canvas: HTMLCanvasElement) { - const c = canvas.getContext("2d")!; - - c.imageSmoothingEnabled = false; - c.fillStyle = "#666"; - c.fillRect(0, 0, canvas.width, canvas.height); - - const gridSize = { x: 20, y: 20 }; - const dotSize = { x: 2, y: 2 }; - - c.fillStyle = "#111"; - for (let y = 0; y < canvas.width / gridSize.x + 1; ++y) { - for (let x = 0; x < canvas.height / gridSize.y + 1; ++x) { - c.fillRect( - (this.offset.x % gridSize.x) + x * gridSize.x - dotSize.x / 2, - (this.offset.y % gridSize.y) + y * gridSize.y - dotSize.y / 2, - dotSize.x, - dotSize.y, - ); - } - } - - if (this.selectionRect) { - const { - pos: { x, y }, - size: { x: w, y: h }, - } = this.selectionRect; - - c.fillStyle = `#ff880088`; - c.fillRect(x, y, w, h); - c.strokeStyle = `#ff8800`; - c.lineWidth = 2; - c.strokeRect(x, y, w, h); - } - } - - renderIfNeeded(canvas: HTMLCanvasElement) { - if (this.renderNeeded) { - this.render(canvas); - this.renderNeeded = false; - } - } - - mouseDown(pos: V2) { - this.state.onMouseDown?.(pos); - } - mouseUp(pos: V2) { - this.state.onMouseUp?.(pos); - } - mouseMove(deltaPos: V2) { - this.state.onMouseMove?.(deltaPos); - } - keyDown(key: string) { - this.state.onKeyDown?.(key); - } - keyUp(key: string) { - this.state.onKeyUp?.(key); - } - selectTool(tool: Tool) { - this.state.selectTool?.(tool); - } - selectedTool(): Tool | null { - return this.state.selectedTool?.() ?? null; - } - - addUpdateAction(action: () => void): object { - this.updateActions.push(action); - return action; - } - - removeUpdateAction(actionId: object) { - this.updateActions = this.updateActions.filter( - (action) => action !== actionId, - ); - } - - transitionTo(newState: State) { - this.state = newState; - this.notifyListeners(); - } - - notifyListeners() { - for (const action of this.updateActions) { - action(); - } - } - - moveOffset(deltaPos: V2) { - this.offset.x += deltaPos.x; - this.offset.y += deltaPos.y; - this.renderNeeded = true; - } - - addSelectionRect(pos: V2) { - this.selectionRect = { pos, size: V2(0, 0) }; - this.renderNeeded = true; - } - - removeSelectionRect() { - this.selectionRect = null; - this.renderNeeded = true; - } - - moveSelectionRect(deltaPos: V2) { - if (this.selectionRect) { - this.selectionRect.size.x += deltaPos.x; - this.selectionRect.size.y += deltaPos.y; - this.renderNeeded = true; - } - } -} - -interface State { - onMouseDown?(pos: V2): void; - onMouseUp?(pos: V2): void; - onMouseMove?(deltaPos: V2): void; - onKeyDown?(key: string): void; - onKeyUp?(key: string): void; - selectTool?(tool: Tool): void; - selectedTool?(): Tool | null; -} - -class Normal implements State { - constructor(private cx: Cx) {} - - selectTool(tool: Tool): void { - switch (tool) { - case "pan": - this.cx.transitionTo(new Panning(this.cx)); - break; - case "and": - this.cx.transitionTo(new Placing(this.cx, "and")); - } - } - - onMouseDown(pos: V2): void { - this.cx.addSelectionRect(pos); - this.cx.transitionTo(new Selecting(this.cx)); - } - - onKeyDown(key: string): void { - if (key === "Shift") { - this.cx.transitionTo(new Panning(this.cx)); - return; - } - } - - selectedTool(): Tool | null { - return "select"; - } -} - -class Panning implements State { - private dragging = false; - - constructor(private cx: Cx) {} - - onMouseDown(_pos: V2): void { - this.dragging = true; - } - - onMouseUp(_pos: V2): void { - this.dragging = false; - } - - onMouseMove(deltaPos: V2): void { - if (this.dragging) { - this.cx.moveOffset(deltaPos); - } - } - - onKeyDown(key: string): void { - if (key === "Escape") { - this.cx.transitionTo(new Normal(this.cx)); - return; - } - } - - onKeyUp(key: string): void { - if (key === "Shift") { - this.cx.transitionTo(new Normal(this.cx)); - return; - } - } - - selectTool(tool: Tool): void { - this.cx.transitionTo(new Normal(this.cx)); - this.cx.selectTool(tool); - } - - selectedTool(): Tool | null { - return "pan"; - } -} - -type SelectionRect = { - pos: V2; - size: V2; -}; - -class Selecting implements State { - constructor(private cx: Cx) {} - - onMouseUp(_pos: V2): void { - this.cx.removeSelectionRect(); - this.cx.transitionTo(new Normal(this.cx)); - } - - onMouseMove(deltaPos: V2): void { - this.cx.moveSelectionRect(deltaPos); - } - - selectedTool(): Tool | null { - return "select"; - } -} - -class Placing implements State { - constructor( - private cx: Cx, - private tool: Tool, - ) {} - - onMouseUp(pos: V2): void { - this.cx.transitionTo(new Normal(this.cx)); - console.log("place"); - } - - onKeyDown(key: string): void { - if (key === "Escape") { - this.cx.transitionTo(new Normal(this.cx)); - return; - } - } - - selectedTool(): Tool | null { - return this.tool; - } -} - -type Tool = "select" | "pan" | "and"; diff --git a/editor/src/Toolbar.tsx b/editor/src/Toolbar.tsx index 86164a5..3930f9a 100644 --- a/editor/src/Toolbar.tsx +++ b/editor/src/Toolbar.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, type ReactElement, type RefObject } from "react"; -import type { Editor } from "./Editor"; +import type { Editor } from "./editor/Editor"; type Props = { editor: Editor; canvasRef: RefObject }; diff --git a/editor/src/editor/Cx.ts b/editor/src/editor/Cx.ts new file mode 100644 index 0000000..c741fad --- /dev/null +++ b/editor/src/editor/Cx.ts @@ -0,0 +1,131 @@ +import type { State } from "./State"; +import { Normal } from "./states/Normal"; + +export class Cx { + private offset = V2(0, 0); + private renderNeeded = false; + private state = new Normal(this) as State; + private updateActions: (() => void)[] = []; + private selectionRect: SelectionRect | null = null; + + render(canvas: HTMLCanvasElement) { + const c = canvas.getContext("2d")!; + + c.imageSmoothingEnabled = false; + c.fillStyle = "#666"; + c.fillRect(0, 0, canvas.width, canvas.height); + + const gridSize = { x: 20, y: 20 }; + const dotSize = { x: 2, y: 2 }; + + c.fillStyle = "#111"; + for (let y = 0; y < canvas.width / gridSize.x + 1; ++y) { + for (let x = 0; x < canvas.height / gridSize.y + 1; ++x) { + c.fillRect( + (this.offset.x % gridSize.x) + x * gridSize.x - dotSize.x / 2, + (this.offset.y % gridSize.y) + y * gridSize.y - dotSize.y / 2, + dotSize.x, + dotSize.y, + ); + } + } + + if (this.selectionRect) { + const { + pos: { x, y }, + size: { x: w, y: h }, + } = this.selectionRect; + + c.fillStyle = `#ff880088`; + c.fillRect(x, y, w, h); + c.strokeStyle = `#ff8800`; + c.lineWidth = 2; + c.strokeRect(x, y, w, h); + } + } + + renderIfNeeded(canvas: HTMLCanvasElement) { + if (this.renderNeeded) { + this.render(canvas); + this.renderNeeded = false; + } + } + + mouseDown(pos: V2) { + this.state.onMouseDown?.(pos); + } + mouseUp(pos: V2) { + this.state.onMouseUp?.(pos); + } + mouseMove(deltaPos: V2) { + this.state.onMouseMove?.(deltaPos); + } + keyDown(key: string) { + this.state.onKeyDown?.(key); + } + keyUp(key: string) { + this.state.onKeyUp?.(key); + } + selectTool(tool: Tool) { + this.state.selectTool?.(tool); + } + selectedTool(): Tool | null { + return this.state.selectedTool?.() ?? null; + } + + addUpdateAction(action: () => void): object { + this.updateActions.push(action); + return action; + } + + removeUpdateAction(actionId: object) { + this.updateActions = this.updateActions.filter( + (action) => action !== actionId, + ); + } + + transitionTo(newState: State) { + this.state = newState; + this.notifyListeners(); + } + + notifyListeners() { + for (const action of this.updateActions) { + action(); + } + } + + moveOffset(deltaPos: V2) { + this.offset.x += deltaPos.x; + this.offset.y += deltaPos.y; + this.renderNeeded = true; + } + + addSelectionRect(pos: V2) { + this.selectionRect = { pos, size: V2(0, 0) }; + this.renderNeeded = true; + } + + removeSelectionRect() { + this.selectionRect = null; + this.renderNeeded = true; + } + + moveSelectionRect(deltaPos: V2) { + if (this.selectionRect) { + this.selectionRect.size.x += deltaPos.x; + this.selectionRect.size.y += deltaPos.y; + this.renderNeeded = true; + } + } +} + +export type V2 = { x: number; y: number }; +export const V2 = (x: number, y: number): V2 => ({ x, y }); + +export type SelectionRect = { + pos: V2; + size: V2; +}; + +export type Tool = "select" | "pan" | "and"; diff --git a/editor/src/editor/Editor.ts b/editor/src/editor/Editor.ts new file mode 100644 index 0000000..ca52810 --- /dev/null +++ b/editor/src/editor/Editor.ts @@ -0,0 +1,53 @@ +import { Cx, V2, type Tool } from "./Cx"; + +export class Editor { + private cx = new Cx(); + + render(canvas: HTMLCanvasElement) { + this.cx.render(canvas); + } + + renderIfNeeded(canvas: HTMLCanvasElement) { + this.cx.renderIfNeeded(canvas); + } + + mouseDown(pos: V2) { + this.cx.mouseDown(pos); + } + + mouseUp(pos: V2) { + this.cx.mouseUp(pos); + } + + mouseMove(deltaPos: V2) { + this.cx.mouseMove(deltaPos); + } + + keyDown(key: string) { + this.cx.keyDown(key); + } + + keyUp(key: string) { + this.cx.keyUp(key); + } + + selectTool(tool: Tool) { + this.cx.selectTool(tool); + } + + selectedTool(): Tool | null { + return this.cx.selectedTool(); + } + + tools(): Tool[] { + return ["select", "pan", "and"]; + } + + addUpdateAction(action: () => void): object { + return this.cx.addUpdateAction(action); + } + + removeUpdateAction(actionId: object) { + this.cx.removeUpdateAction(actionId); + } +} diff --git a/editor/src/editor/State.ts b/editor/src/editor/State.ts new file mode 100644 index 0000000..db72eeb --- /dev/null +++ b/editor/src/editor/State.ts @@ -0,0 +1,11 @@ +import type { V2, Tool } from "./Cx"; + +export interface State { + onMouseDown?(pos: V2): void; + onMouseUp?(pos: V2): void; + onMouseMove?(deltaPos: V2): void; + onKeyDown?(key: string): void; + onKeyUp?(key: string): void; + selectTool?(tool: Tool): void; + selectedTool?(): Tool | null; +} diff --git a/editor/src/editor/states/Normal.ts b/editor/src/editor/states/Normal.ts new file mode 100644 index 0000000..6e74155 --- /dev/null +++ b/editor/src/editor/states/Normal.ts @@ -0,0 +1,35 @@ +import type { Cx, Tool, V2 } from "../Cx"; +import type { State } from "../State"; +import { Panning } from "./Panning"; +import { Placing } from "./Placing"; +import { Selecting } from "./Selecting"; + +export class Normal implements State { + constructor(private cx: Cx) {} + + selectTool(tool: Tool): void { + switch (tool) { + case "pan": + this.cx.transitionTo(new Panning(this.cx)); + break; + case "and": + this.cx.transitionTo(new Placing(this.cx, "and")); + } + } + + onMouseDown(pos: V2): void { + this.cx.addSelectionRect(pos); + this.cx.transitionTo(new Selecting(this.cx)); + } + + onKeyDown(key: string): void { + if (key === "Shift") { + this.cx.transitionTo(new Panning(this.cx)); + return; + } + } + + selectedTool(): Tool | null { + return "select"; + } +} diff --git a/editor/src/editor/states/Panning.ts b/editor/src/editor/states/Panning.ts new file mode 100644 index 0000000..0dbab28 --- /dev/null +++ b/editor/src/editor/states/Panning.ts @@ -0,0 +1,46 @@ +import type { Cx, Tool, V2 } from "../Cx"; +import type { State } from "../State"; +import { Normal } from "./Normal"; + +export class Panning implements State { + private dragging = false; + + constructor(private cx: Cx) {} + + onMouseDown(_pos: V2): void { + this.dragging = true; + } + + onMouseUp(_pos: V2): void { + this.dragging = false; + } + + onMouseMove(deltaPos: V2): void { + if (this.dragging) { + this.cx.moveOffset(deltaPos); + } + } + + onKeyDown(key: string): void { + if (key === "Escape") { + this.cx.transitionTo(new Normal(this.cx)); + return; + } + } + + onKeyUp(key: string): void { + if (key === "Shift") { + this.cx.transitionTo(new Normal(this.cx)); + return; + } + } + + 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 new file mode 100644 index 0000000..a3f5203 --- /dev/null +++ b/editor/src/editor/states/Placing.ts @@ -0,0 +1,26 @@ +import type { Cx, Tool, V2 } from "../Cx"; +import type { State } from "../State"; +import { Normal } from "./Normal"; + +export class Placing implements State { + constructor( + private cx: Cx, + private tool: Tool, + ) {} + + onMouseUp(pos: V2): void { + this.cx.transitionTo(new Normal(this.cx)); + console.log("place"); + } + + onKeyDown(key: string): void { + if (key === "Escape") { + this.cx.transitionTo(new Normal(this.cx)); + return; + } + } + + selectedTool(): Tool | null { + return this.tool; + } +} diff --git a/editor/src/editor/states/Selecting.ts b/editor/src/editor/states/Selecting.ts new file mode 100644 index 0000000..288c181 --- /dev/null +++ b/editor/src/editor/states/Selecting.ts @@ -0,0 +1,20 @@ +import type { Cx, Tool, V2 } from "../Cx"; +import type { State } from "../State"; +import { Normal } from "./Normal"; + +export class Selecting implements State { + constructor(private cx: Cx) {} + + onMouseUp(_pos: V2): void { + this.cx.removeSelectionRect(); + this.cx.transitionTo(new Normal(this.cx)); + } + + onMouseMove(deltaPos: V2): void { + this.cx.moveSelectionRect(deltaPos); + } + + selectedTool(): Tool | null { + return "select"; + } +}