From 9a1894c6b543b5673781ee5a399e30bc109be164 Mon Sep 17 00:00:00 2001 From: sfja Date: Wed, 10 Jun 2026 05:30:49 +0200 Subject: [PATCH] upsert cx --- editor/src/editor/Board.ts | 2 +- editor/src/editor/ComponentPlacer.ts | 13 ++ editor/src/editor/ConnectingWire.ts | 90 ++++++++ editor/src/editor/Cx.ts | 294 --------------------------- editor/src/editor/Editor.ts | 115 ++++++++++- editor/src/editor/Selection.ts | 45 ++++ editor/src/editor/SelectionBox.ts | 28 +++ editor/src/editor/states.ts | 26 ++- 8 files changed, 299 insertions(+), 314 deletions(-) create mode 100644 editor/src/editor/ComponentPlacer.ts create mode 100644 editor/src/editor/ConnectingWire.ts delete mode 100644 editor/src/editor/Cx.ts create mode 100644 editor/src/editor/Selection.ts create mode 100644 editor/src/editor/SelectionBox.ts diff --git a/editor/src/editor/Board.ts b/editor/src/editor/Board.ts index f24b75d..8218fb5 100644 --- a/editor/src/editor/Board.ts +++ b/editor/src/editor/Board.ts @@ -1,4 +1,4 @@ -import type { Selection } from "./Cx"; +import type { Selection } from "./Selection"; import type { Renderer } from "./Renderer"; import { lineSegmentPointDistance, diff --git a/editor/src/editor/ComponentPlacer.ts b/editor/src/editor/ComponentPlacer.ts new file mode 100644 index 0000000..326eafe --- /dev/null +++ b/editor/src/editor/ComponentPlacer.ts @@ -0,0 +1,13 @@ +import type { Renderer } from "./Renderer"; +import type { V2 } from "./V2"; + +export class ComponentPlacer { + constructor( + public pos: V2, + public size: V2, + ) {} + + render(r: Renderer) { + r.drawComponentPlacer(this.pos, this.size); + } +} diff --git a/editor/src/editor/ConnectingWire.ts b/editor/src/editor/ConnectingWire.ts new file mode 100644 index 0000000..7bda407 --- /dev/null +++ b/editor/src/editor/ConnectingWire.ts @@ -0,0 +1,90 @@ +import type { Board, Component, Joint, WireConnection } from "./Board"; +import type { Renderer } from "./Renderer"; +import { V2, v2 } from "./V2"; + +export class ConnectingWire { + constructor( + public kind: ConnectingWireKind, + public pos: V2, + ) {} + + render(r: Renderer) { + switch (this.kind.tag) { + case "InputPin": + case "OutputPin": + break; + case "Intermediary": + this.kind.prev.render(r); + r.drawConnectingWirePoint(this.beginPos()); + break; + } + r.drawConnectingWire(this.beginPos(), this.pos); + } + + move(pos: V2) { + this.pos = pos; + } + + connectToInput(board: Board, comp: Component, i: number) { + this.pushWire(board, { tag: "InputPin", comp, i }); + } + + connectToOutput(board: Board, comp: Component, i: number) { + this.pushWire(board, { tag: "OutputPin", comp, i }); + } + + connectToJoint(board: Board, joint: Joint) { + this.pushWire(board, { tag: "Joint", joint }); + } + + private pushWire(board: Board, end: WireConnection) { + switch (this.kind.tag) { + case "InputPin": + case "OutputPin": { + const { tag, comp, i } = this.kind; + board.addWire({ tag, comp, i }, end); + break; + } + case "Intermediary": { + const joint = board.addJoint(this.kind.pos); + board.addWire({ tag: "Joint", joint }, end); + this.kind.prev.pushWire(board, { tag: "Joint", joint }); + break; + } + case "Joint": { + const joint = this.kind.joint; + board.addWire({ tag: "Joint", joint }, end); + break; + } + default: + this.kind satisfies never; + } + } + + private beginPos(): V2 { + switch (this.kind.tag) { + case "InputPin": + return v2( + this.kind.comp.pos.x, + this.kind.comp.pos.y + + this.kind.comp.kind.inputPinOffsets()[this.kind.i], + ); + case "OutputPin": + return v2( + this.kind.comp.pos.x + this.kind.comp.kind.size.x, + this.kind.comp.pos.y + + this.kind.comp.kind.outputPinOffsets()[this.kind.i], + ); + case "Intermediary": + return this.kind.pos; + case "Joint": + return this.kind.joint.pos; + } + } +} + +export type ConnectingWireKind = + | { tag: "InputPin"; comp: Component; i: number } + | { tag: "OutputPin"; comp: Component; i: number } + | { tag: "Intermediary"; prev: ConnectingWire; pos: V2 } + | { tag: "Joint"; joint: Joint }; diff --git a/editor/src/editor/Cx.ts b/editor/src/editor/Cx.ts deleted file mode 100644 index e46489e..0000000 --- a/editor/src/editor/Cx.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { - Board, - ComponentRepo, - Joint, - type Component, - type WireConnection, -} from "./Board"; -import { Renderer } from "./Renderer"; -import * as states from "./states"; -import { v2, V2 } from "./V2"; -import * as ir from "./ir"; -import { Sim } from "./sim"; -import { EventBus } from "./events"; -import { Mouse } from "./Mouse"; -import { ViewPos } from "./ViewPos"; - -export type Tool = string; - -export class Cx { - public viewpos: ViewPos; - private renderNeeded = false; - - private state = new states.Normal(this) as states.State; - - public selectionBox: SelectionBox | null = null; - private componentPlacer: ComponentPlacer | null = null; - public selection: Selection | null = null; - public connectingWire: ConnectingWire | null = null; - - public board = new Board(); - public componentRepo = ComponentRepo.withDefaults(); - - public keysPressed = new Set(); - - public mouse: Mouse; - - constructor(public events: EventBus) { - this.viewpos = new ViewPos(events); - this.mouse = new Mouse(this.events); - - this.state.enter(); - - this.events.subscribe( - ["MouseDown", "MouseUp", "MouseMove", "KeyDown", "KeyUp", "SelectTool"], - (ev) => { - switch (ev.tag) { - case "KeyDown": - this.keysPressed.add(ev.key); - break; - case "KeyUp": - this.keysPressed.delete(ev.key); - break; - case "SelectTool": - this.onSelectTool(ev.tool); - } - this.renderNeeded = true; - }, - ); - } - - render(canvas: HTMLCanvasElement) { - const r = new Renderer(canvas, this.viewpos.offset); - - r.clear(); - r.drawGrid(); - this.board.render(r, this.selection); - this.selectionBox?.render(r); - this.componentPlacer?.render(r); - this.connectingWire?.render(r); - } - - renderIfNeeded(canvas: HTMLCanvasElement) { - if (this.renderNeeded) { - this.render(canvas); - this.renderNeeded = false; - } - } - - private onSelectTool(tool: Tool) { - switch (tool) { - case "pan": - this.transitionTo(new states.Panning(this)); - break; - case "input": - case "output": - case "and": - case "or": - case "not": - this.transitionTo(new states.Placing(this, tool)); - break; - default: - this.transitionTo(new states.Normal(this)); - } - this.events.send({ tag: "ShowSelectedTool", tool }); - } - - transitionTo(newState: states.State) { - this.state.leave(); - this.state = newState; - // console.log(`Entering state ${newState.constructor.name}`); - this.state.enter(); - } - - addComponentPlacer(pos: V2, size: V2) { - this.componentPlacer = new ComponentPlacer(pos, size); - } - - removeComponentPlacer() { - this.componentPlacer = null; - } - - setComponentPlacerPos(pos: V2) { - if (this.componentPlacer) { - this.componentPlacer.pos = pos; - } - } - - runSimulation() { - // const comp = this.board.toIr(); - // console.log("Before optimizing"); - // console.log(...new ir.ComponentPrinter().stringifyToConsole(comp)); - // new ir.ComponentOptimizer(comp).optimize(); - // console.log("After optimizing"); - // console.log(...new ir.ComponentPrinter().stringifyToConsole(comp)); - // const sim = new Sim(comp, [], []); - // sim.simulate(); - } -} - -export class SelectionBox { - constructor( - public pos: V2, - public size = v2(0, 0), - ) {} - - render(r: Renderer) { - r.drawSelectionBox(this.pos, this.size); - } - - move(deltaPos: V2) { - this.size = this.size.add(deltaPos); - } - - boardRect(viewpos: ViewPos): { pos: V2; size: V2 } { - const normalizedAxis = (p: number, s: number): [number, number] => - s >= 0 ? [p, s] : [p + s, -s]; - - const [x, w] = normalizedAxis(this.pos.x, this.size.x); - const [y, h] = normalizedAxis(this.pos.y, this.size.y); - - return { pos: v2(x, y).sub(viewpos.offset), size: v2(w, h) }; - } -} - -export class ComponentPlacer { - constructor( - public pos: V2, - public size: V2, - ) {} - - render(r: Renderer) { - r.drawComponentPlacer(this.pos, this.size); - } -} - -export class Selection { - private selectedComponents = new Set(); - private selectedJoints = new Set(); - - addComponent(comp: Component) { - this.selectedComponents.add(comp); - } - addJoint(joint: Joint) { - this.selectedJoints.add(joint); - } - - toggleComponent(comp: Component) { - if (this.selectedComponents.has(comp)) { - this.selectedComponents.delete(comp); - } else { - this.selectedComponents.add(comp); - } - } - toggleJoint(joint: Joint) { - if (this.selectedJoints.has(joint)) { - this.selectedJoints.delete(joint); - } else { - this.selectedJoints.add(joint); - } - } - - isComponentSelected(comp: Component) { - return this.selectedComponents.has(comp); - } - isJointSelected(joint: Joint) { - return this.selectedJoints.has(joint); - } - - move(deltaPos: V2) { - for (const comp of this.selectedComponents) { - comp.pos = comp.pos.add(deltaPos); - } - for (const joint of this.selectedJoints) { - joint.pos = joint.pos.add(deltaPos); - } - } -} - -export class ConnectingWire { - constructor( - public kind: ConnectingWireKind, - public pos: V2, - ) {} - - render(r: Renderer) { - switch (this.kind.tag) { - case "InputPin": - case "OutputPin": - break; - case "Intermediary": - this.kind.prev.render(r); - r.drawConnectingWirePoint(this.beginPos()); - break; - } - r.drawConnectingWire(this.beginPos(), this.pos); - } - - move(pos: V2) { - this.pos = pos; - } - - connectToInput(board: Board, comp: Component, i: number) { - this.pushWire(board, { tag: "InputPin", comp, i }); - } - - connectToOutput(board: Board, comp: Component, i: number) { - this.pushWire(board, { tag: "OutputPin", comp, i }); - } - - connectToJoint(board: Board, joint: Joint) { - this.pushWire(board, { tag: "Joint", joint }); - } - - private pushWire(board: Board, end: WireConnection) { - switch (this.kind.tag) { - case "InputPin": - case "OutputPin": { - const { tag, comp, i } = this.kind; - board.addWire({ tag, comp, i }, end); - break; - } - case "Intermediary": { - const joint = board.addJoint(this.kind.pos); - board.addWire({ tag: "Joint", joint }, end); - this.kind.prev.pushWire(board, { tag: "Joint", joint }); - break; - } - case "Joint": { - const joint = this.kind.joint; - board.addWire({ tag: "Joint", joint }, end); - break; - } - default: - this.kind satisfies never; - } - } - - private beginPos(): V2 { - switch (this.kind.tag) { - case "InputPin": - return v2( - this.kind.comp.pos.x, - this.kind.comp.pos.y + - this.kind.comp.kind.inputPinOffsets()[this.kind.i], - ); - case "OutputPin": - return v2( - this.kind.comp.pos.x + this.kind.comp.kind.size.x, - this.kind.comp.pos.y + - this.kind.comp.kind.outputPinOffsets()[this.kind.i], - ); - case "Intermediary": - return this.kind.pos; - case "Joint": - return this.kind.joint.pos; - } - } -} - -export type ConnectingWireKind = - | { tag: "InputPin"; comp: Component; i: number } - | { tag: "OutputPin"; comp: Component; i: number } - | { tag: "Intermediary"; prev: ConnectingWire; pos: V2 } - | { tag: "Joint"; joint: Joint }; diff --git a/editor/src/editor/Editor.ts b/editor/src/editor/Editor.ts index a119f4e..5f7b505 100644 --- a/editor/src/editor/Editor.ts +++ b/editor/src/editor/Editor.ts @@ -1,19 +1,124 @@ -import { Cx, type Tool } from "./Cx"; +import { Board, ComponentRepo } from "./Board"; +import { SelectionBox } from "./SelectionBox"; +import { ComponentPlacer } from "./ComponentPlacer"; +import { Selection } from "./Selection"; +import { ConnectingWire } from "./ConnectingWire"; import { EventBus } from "./events"; +import { Mouse } from "./Mouse"; +import { Renderer } from "./Renderer"; +import * as states from "./states"; +import type { V2 } from "./V2"; +import { ViewPos } from "./ViewPos"; export class Editor { public events = new EventBus(); - private cx = new Cx(this.events); + public viewpos = new ViewPos(this.events); + private renderNeeded = false; + + private state = new states.Normal(this) as states.State; + + public selectionBox: SelectionBox | null = null; + private componentPlacer: ComponentPlacer | null = null; + public selection: Selection | null = null; + public connectingWire: ConnectingWire | null = null; + + public board = new Board(); + public componentRepo = ComponentRepo.withDefaults(); + + public keysPressed = new Set(); + + public mouse = new Mouse(this.events); + + constructor() { + this.state.enter(); + + this.events.subscribe( + ["MouseDown", "MouseUp", "MouseMove", "KeyDown", "KeyUp", "SelectTool"], + (ev) => { + switch (ev.tag) { + case "KeyDown": + this.keysPressed.add(ev.key); + break; + case "KeyUp": + this.keysPressed.delete(ev.key); + break; + case "SelectTool": + this.onSelectTool(ev.tool); + } + this.renderNeeded = true; + }, + ); + } render(canvas: HTMLCanvasElement) { - this.cx.render(canvas); + const r = new Renderer(canvas, this.viewpos.offset); + + r.clear(); + r.drawGrid(); + this.board.render(r, this.selection); + this.selectionBox?.render(r); + this.componentPlacer?.render(r); + this.connectingWire?.render(r); } renderIfNeeded(canvas: HTMLCanvasElement) { - this.cx.renderIfNeeded(canvas); + if (this.renderNeeded) { + this.render(canvas); + this.renderNeeded = false; + } } - tools(): Tool[] { + private onSelectTool(tool: string) { + switch (tool) { + case "pan": + this.transitionTo(new states.Panning(this)); + break; + case "input": + case "output": + case "and": + case "or": + case "not": + this.transitionTo(new states.Placing(this, tool)); + break; + default: + this.transitionTo(new states.Normal(this)); + } + this.events.send({ tag: "ShowSelectedTool", tool }); + } + + transitionTo(newState: states.State) { + this.state.leave(); + this.state = newState; + // console.log(`Entering state ${newState.constructor.name}`); + this.state.enter(); + } + + addComponentPlacer(pos: V2, size: V2) { + this.componentPlacer = new ComponentPlacer(pos, size); + } + + removeComponentPlacer() { + this.componentPlacer = null; + } + + setComponentPlacerPos(pos: V2) { + if (this.componentPlacer) { + this.componentPlacer.pos = pos; + } + } + + runSimulation() { + // const comp = this.board.toIr(); + // console.log("Before optimizing"); + // console.log(...new ir.ComponentPrinter().stringifyToConsole(comp)); + // new ir.ComponentOptimizer(comp).optimize(); + // console.log("After optimizing"); + // console.log(...new ir.ComponentPrinter().stringifyToConsole(comp)); + // const sim = new Sim(comp, [], []); + // sim.simulate(); + } + + tools(): string[] { return ["select", "pan", "input", "output", "and", "or", "not"]; } } diff --git a/editor/src/editor/Selection.ts b/editor/src/editor/Selection.ts new file mode 100644 index 0000000..c4039fc --- /dev/null +++ b/editor/src/editor/Selection.ts @@ -0,0 +1,45 @@ +import type { Component, Joint } from "./Board"; +import type { V2 } from "./V2"; + +export class Selection { + private selectedComponents = new Set(); + private selectedJoints = new Set(); + + addComponent(comp: Component) { + this.selectedComponents.add(comp); + } + addJoint(joint: Joint) { + this.selectedJoints.add(joint); + } + + toggleComponent(comp: Component) { + if (this.selectedComponents.has(comp)) { + this.selectedComponents.delete(comp); + } else { + this.selectedComponents.add(comp); + } + } + toggleJoint(joint: Joint) { + if (this.selectedJoints.has(joint)) { + this.selectedJoints.delete(joint); + } else { + this.selectedJoints.add(joint); + } + } + + isComponentSelected(comp: Component) { + return this.selectedComponents.has(comp); + } + isJointSelected(joint: Joint) { + return this.selectedJoints.has(joint); + } + + move(deltaPos: V2) { + for (const comp of this.selectedComponents) { + comp.pos = comp.pos.add(deltaPos); + } + for (const joint of this.selectedJoints) { + joint.pos = joint.pos.add(deltaPos); + } + } +} diff --git a/editor/src/editor/SelectionBox.ts b/editor/src/editor/SelectionBox.ts new file mode 100644 index 0000000..242cf61 --- /dev/null +++ b/editor/src/editor/SelectionBox.ts @@ -0,0 +1,28 @@ +import type { Renderer } from "./Renderer"; +import { V2, v2 } from "./V2"; +import type { ViewPos } from "./ViewPos"; + +export class SelectionBox { + constructor( + public pos: V2, + public size = v2(0, 0), + ) {} + + render(r: Renderer) { + r.drawSelectionBox(this.pos, this.size); + } + + move(deltaPos: V2) { + this.size = this.size.add(deltaPos); + } + + boardRect(viewpos: ViewPos): { pos: V2; size: V2 } { + const normalizedAxis = (p: number, s: number): [number, number] => + s >= 0 ? [p, s] : [p + s, -s]; + + const [x, w] = normalizedAxis(this.pos.x, this.size.x); + const [y, h] = normalizedAxis(this.pos.y, this.size.y); + + return { pos: v2(x, y).sub(viewpos.offset), size: v2(w, h) }; + } +} diff --git a/editor/src/editor/states.ts b/editor/src/editor/states.ts index dbaf3a4..6fd75f0 100644 --- a/editor/src/editor/states.ts +++ b/editor/src/editor/states.ts @@ -1,6 +1,8 @@ import { Component, Joint, type ComponentKind } from "./Board"; -import { ConnectingWire, Selection, type ConnectingWireKind } from "./Cx"; -import { SelectionBox, type Cx, type Tool } from "./Cx"; +import { Selection } from "./Selection"; +import { ConnectingWire, type ConnectingWireKind } from "./ConnectingWire"; +import { SelectionBox } from "./SelectionBox"; +import type { Editor } from "./Editor"; import type { EventUnsub } from "./events"; import { v2, type V2 } from "./V2"; @@ -12,7 +14,7 @@ export interface State { export class Normal implements State { private unsubscribe!: EventUnsub; - constructor(private cx: Cx) {} + constructor(private cx: Editor) {} enter(): void { this.unsubscribe = this.cx.events.subscribe( @@ -90,7 +92,7 @@ export class Normal implements State { export class Panning implements State { private unsubscribe!: EventUnsub; - constructor(private cx: Cx) {} + constructor(private cx: Editor) {} enter(): void { this.unsubscribe = this.cx.events.subscribe( @@ -133,8 +135,8 @@ export class Placing implements State { private compDef: ComponentKind; constructor( - private cx: Cx, - private tool: Tool, + private cx: Editor, + private tool: string, ) { this.compDef = this.cx.componentRepo.get(this.tool); } @@ -180,7 +182,7 @@ export class Selecting implements State { private isMouseDown = false; - constructor(private cx: Cx) {} + constructor(private cx: Editor) {} enter(): void { this.unsubscribe = this.cx.events.subscribe( @@ -258,7 +260,7 @@ export class Selecting implements State { export class Moving implements State { private unsubscribe!: EventUnsub; - constructor(private cx: Cx) {} + constructor(private cx: Editor) {} enter(): void { this.unsubscribe = this.cx.events.subscribe( @@ -284,7 +286,7 @@ export class Moving implements State { export class SelectingBox implements State { private unsubscribe!: EventUnsub; - constructor(private cx: Cx) {} + constructor(private cx: Editor) {} enter(): void { this.unsubscribe = this.cx.events.subscribe( @@ -334,16 +336,12 @@ export class SelectingBox implements State { this.cx.transitionTo(new Normal(this.cx)); } } - - selectedTool(): Tool | null { - return "select"; - } } export class Wiring implements State { private unsubscribe!: EventUnsub; - constructor(private cx: Cx) {} + constructor(private cx: Editor) {} enter(): void { this.unsubscribe = this.cx.events.subscribe(