diff --git a/editor/src/editor/Board.ts b/editor/src/editor/Board.ts index 68e484c..1accdc8 100644 --- a/editor/src/editor/Board.ts +++ b/editor/src/editor/Board.ts @@ -1,3 +1,4 @@ +import type { Renderer } from "./Renderer"; import { pointInsideRect, rectsCollide, v2, V2 } from "./V2"; export class Board { @@ -16,81 +17,9 @@ export class Board { this.components.push({ def, pos }); } - render(_canvas: HTMLCanvasElement, c: CanvasRenderingContext2D, offset: V2) { + render(r: Renderer) { for (const comp of this.components) { - const { - def: { - size: { x: w, y: h }, - label, - inputs, - outputs, - }, - pos, - } = comp; - - const [x, y] = [pos.x + offset.x, pos.y + offset.y]; - - c.fillStyle = `#6abbde`; - c.fillRect(x, y, w, h); - c.strokeStyle = `#333333`; - c.lineWidth = 2; - c.strokeRect(x, y, w, h); - - c.fillStyle = `#333333`; - c.font = "bold 16px monospace"; - const textMetrix = c.measureText(label); - c.fillText( - label, - 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) { - 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 = `#eee`; - c.lineWidth = 2; - c.beginPath(); - c.arc(x + w, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2); - c.stroke(); - } - } - } + r.drawComponent(comp, this.hoveredOverInput, this.hoveredOverOutput); } } @@ -239,7 +168,7 @@ export class ComponentRepo { } } -type Component = { +export type Component = { def: ComponentDef; pos: V2; }; diff --git a/editor/src/editor/Cx.ts b/editor/src/editor/Cx.ts index 326a1a8..b48a7ce 100644 --- a/editor/src/editor/Cx.ts +++ b/editor/src/editor/Cx.ts @@ -1,6 +1,9 @@ import { Board, ComponentRepo } from "./Board"; +import { Renderer } from "./Renderer"; import type { State } from "./State"; import { Normal } from "./states/Normal"; +import { Panning } from "./states/Panning"; +import { Placing } from "./states/Placing"; import { v2, V2 } from "./V2"; export class Cx { @@ -9,59 +12,20 @@ export class Cx { private state = new Normal(this) as State; private updateActions: (() => void)[] = []; - private selectionRect: SelectionRect | null = null; + private selectionBox: SelectionBox | null = null; private componentPlacer: ComponentPlacer | null = null; public board = new Board(); public componentRepo = ComponentRepo.withDefaults(); render(canvas: HTMLCanvasElement) { - const c = canvas.getContext("2d")!; + const r = new Renderer(canvas, this.offset); - c.imageSmoothingEnabled = false; - c.fillStyle = "#666"; - c.fillRect(0, 0, canvas.width, canvas.height); - - const dotSize = { x: 2, y: 2 }; - const gridSize = v2(20, 20); - - 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, - ); - } - } - - this.board.render(canvas, c, this.offset); - - 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); - } - - if (this.componentPlacer) { - const { - pos: { x, y }, - size: { x: w, y: h }, - } = this.componentPlacer; - - c.strokeStyle = `#ffffff`; - c.lineWidth = 2; - c.strokeRect(x, y, w, h); - } + r.clear(); + r.drawGrid(); + this.board.render(r); + this.selectionBox?.render(r); + this.componentPlacer?.render(r); } renderIfNeeded(canvas: HTMLCanvasElement) { @@ -91,16 +55,20 @@ 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)); + switch (tool) { + case "pan": + this.transitionTo(new Panning(this)); + break; + case "input": + case "output": + case "and": + case "or": + case "not": + this.transitionTo(new Placing(this, tool)); + break; + default: + this.transitionTo(new Normal(this)); } - this.state.selectTool?.(tool); - } - selectedTool(): Tool | null { - return this.state.selectedTool?.() ?? null; } addUpdateAction(action: () => void): object { @@ -134,25 +102,25 @@ export class Cx { } addSelectionRect(pos: V2) { - this.selectionRect = { pos, size: v2(0, 0) }; + this.selectionBox = new SelectionBox(pos, v2(0, 0)); this.renderNeeded = true; } removeSelectionRect() { - this.selectionRect = null; + this.selectionBox = null; this.renderNeeded = true; } moveSelectionRect(deltaPos: V2) { - if (this.selectionRect) { - this.selectionRect.size.x += deltaPos.x; - this.selectionRect.size.y += deltaPos.y; + if (this.selectionBox) { + this.selectionBox.size.x += deltaPos.x; + this.selectionBox.size.y += deltaPos.y; this.renderNeeded = true; } } addComponentPlacer(pos: V2, size: V2) { - this.componentPlacer = { pos, size }; + this.componentPlacer = new ComponentPlacer(pos, size); this.renderNeeded = true; } @@ -175,14 +143,26 @@ export class Cx { } } -export type SelectionRect = { - pos: V2; - size: V2; -}; +export class SelectionBox { + constructor( + public pos: V2, + public size: V2, + ) {} -export type ComponentPlacer = { - pos: V2; - size: V2; -}; + render(r: Renderer) { + r.drawSelectionBox(this.pos, this.size); + } +} + +export class ComponentPlacer { + constructor( + public pos: V2, + public size: V2, + ) {} + + render(r: Renderer) { + r.drawComponentPlacer(this.pos, this.size); + } +} export type Tool = string; diff --git a/editor/src/editor/Renderer.ts b/editor/src/editor/Renderer.ts new file mode 100644 index 0000000..b75e75b --- /dev/null +++ b/editor/src/editor/Renderer.ts @@ -0,0 +1,134 @@ +import type { Component } from "./Board"; +import { v2, type V2 } from "./V2"; + +export class Renderer { + private c: CanvasRenderingContext2D; + + constructor( + private canvas: HTMLCanvasElement, + private offset: V2, + ) { + this.c = this.canvas.getContext("2d")!; + this.c.imageSmoothingEnabled = false; + } + + clear() { + const { canvas, c } = this; + c.fillStyle = "#666"; + c.fillRect(0, 0, canvas.width, canvas.height); + } + + drawGrid() { + const { canvas, c } = this; + + const dotSize = { x: 2, y: 2 }; + const gridSize = v2(20, 20); + + 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, + ); + } + } + } + + drawSelectionBox(pos: V2, size: V2) { + const { c } = this; + const { x, y } = pos; + const { x: w, y: h } = size; + c.fillStyle = `#ff880088`; + c.fillRect(x, y, w, h); + c.strokeStyle = `#ff8800`; + c.lineWidth = 2; + c.strokeRect(x, y, w, h); + } + + drawComponentPlacer(pos: V2, size: V2) { + const { c } = this; + const { x, y } = pos; + const { x: w, y: h } = size; + c.strokeStyle = `#ffffff`; + c.lineWidth = 2; + c.strokeRect(x, y, w, h); + } + + drawComponent( + comp: Component, + hoveredOverInput: [Component, number] | null, + hoveredOverOutput: [Component, number] | null, + ) { + const { c, offset } = this; + const { + def: { + size: { x: w, y: h }, + label, + inputs, + outputs, + }, + pos, + } = comp; + + const [x, y] = [pos.x + offset.x, pos.y + offset.y]; + + c.fillStyle = `#6abbde`; + c.fillRect(x, y, w, h); + c.strokeStyle = `#333333`; + c.lineWidth = 2; + c.strokeRect(x, y, w, h); + + c.fillStyle = `#333333`; + c.font = "bold 16px monospace"; + const textMetrix = c.measureText(label); + c.fillText( + label, + 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) { + 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 (hoveredOverInput?.[0] === comp && 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 (hoveredOverOutput?.[0] === comp && hoveredOverOutput[1] === i) { + c.strokeStyle = `#eee`; + c.lineWidth = 2; + c.beginPath(); + c.arc(x + w, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2); + c.stroke(); + } + } + } + } +} diff --git a/editor/src/editor/State.ts b/editor/src/editor/State.ts index a0ecd8b..724bf59 100644 --- a/editor/src/editor/State.ts +++ b/editor/src/editor/State.ts @@ -1,14 +1,13 @@ 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; selectedTool?(): Tool | null; } diff --git a/editor/src/editor/states/Normal.ts b/editor/src/editor/states/Normal.ts index 2d76395..cac5888 100644 --- a/editor/src/editor/states/Normal.ts +++ b/editor/src/editor/states/Normal.ts @@ -2,26 +2,11 @@ import type { Cx, Tool } from "../Cx"; import type { V2 } from "../V2"; import type { State } from "../State"; import { Panning } from "./Panning"; -import { Placing } from "./Placing"; -import { Selecting } from "./Selecting"; +import { SelectingBox } from "./SelectingBox"; 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 "input": - case "output": - case "and": - case "or": - case "not": - this.cx.transitionTo(new Placing(this.cx, tool)); - } - } - onMouseDown(pos: V2): void { if ( this.cx.board.handleMouseClick( @@ -34,7 +19,7 @@ export class Normal implements State { return; } else { this.cx.addSelectionRect(pos); - this.cx.transitionTo(new Selecting(this.cx)); + this.cx.transitionTo(new SelectingBox(this.cx)); } } diff --git a/editor/src/editor/states/Selecting.ts b/editor/src/editor/states/Selecting.ts index 0b4c181..7940ee0 100644 --- a/editor/src/editor/states/Selecting.ts +++ b/editor/src/editor/states/Selecting.ts @@ -1,21 +1,6 @@ -import type { Cx, Tool } from "../Cx"; -import type { V2 } from "../V2"; +import type { Cx } 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"; - } } diff --git a/editor/src/editor/states/SelectingBox.ts b/editor/src/editor/states/SelectingBox.ts new file mode 100644 index 0000000..5375539 --- /dev/null +++ b/editor/src/editor/states/SelectingBox.ts @@ -0,0 +1,21 @@ +import type { Cx, Tool } from "../Cx"; +import type { V2 } from "../V2"; +import type { State } from "../State"; +import { Normal } from "./Normal"; + +export class SelectingBox 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"; + } +}