upsert cx

This commit is contained in:
sfja 2026-06-10 05:30:49 +02:00
parent a0f0486590
commit 9a1894c6b5
8 changed files with 299 additions and 314 deletions

View File

@ -1,4 +1,4 @@
import type { Selection } from "./Cx"; import type { Selection } from "./Selection";
import type { Renderer } from "./Renderer"; import type { Renderer } from "./Renderer";
import { import {
lineSegmentPointDistance, lineSegmentPointDistance,

View File

@ -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);
}
}

View File

@ -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 };

View File

@ -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<string>();
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<Component>();
private selectedJoints = new Set<Joint>();
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 };

View File

@ -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 { 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 { export class Editor {
public events = new EventBus(); 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<string>();
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) { 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) { 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"]; return ["select", "pan", "input", "output", "and", "or", "not"];
} }
} }

View File

@ -0,0 +1,45 @@
import type { Component, Joint } from "./Board";
import type { V2 } from "./V2";
export class Selection {
private selectedComponents = new Set<Component>();
private selectedJoints = new Set<Joint>();
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);
}
}
}

View File

@ -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) };
}
}

View File

@ -1,6 +1,8 @@
import { Component, Joint, type ComponentKind } from "./Board"; import { Component, Joint, type ComponentKind } from "./Board";
import { ConnectingWire, Selection, type ConnectingWireKind } from "./Cx"; import { Selection } from "./Selection";
import { SelectionBox, type Cx, type Tool } from "./Cx"; import { ConnectingWire, type ConnectingWireKind } from "./ConnectingWire";
import { SelectionBox } from "./SelectionBox";
import type { Editor } from "./Editor";
import type { EventUnsub } from "./events"; import type { EventUnsub } from "./events";
import { v2, type V2 } from "./V2"; import { v2, type V2 } from "./V2";
@ -12,7 +14,7 @@ export interface State {
export class Normal implements State { export class Normal implements State {
private unsubscribe!: EventUnsub; private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Editor) {}
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
@ -90,7 +92,7 @@ export class Normal implements State {
export class Panning implements State { export class Panning implements State {
private unsubscribe!: EventUnsub; private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Editor) {}
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
@ -133,8 +135,8 @@ export class Placing implements State {
private compDef: ComponentKind; private compDef: ComponentKind;
constructor( constructor(
private cx: Cx, private cx: Editor,
private tool: Tool, private tool: string,
) { ) {
this.compDef = this.cx.componentRepo.get(this.tool); this.compDef = this.cx.componentRepo.get(this.tool);
} }
@ -180,7 +182,7 @@ export class Selecting implements State {
private isMouseDown = false; private isMouseDown = false;
constructor(private cx: Cx) {} constructor(private cx: Editor) {}
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
@ -258,7 +260,7 @@ export class Selecting implements State {
export class Moving implements State { export class Moving implements State {
private unsubscribe!: EventUnsub; private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Editor) {}
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
@ -284,7 +286,7 @@ export class Moving implements State {
export class SelectingBox implements State { export class SelectingBox implements State {
private unsubscribe!: EventUnsub; private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Editor) {}
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
@ -334,16 +336,12 @@ export class SelectingBox implements State {
this.cx.transitionTo(new Normal(this.cx)); this.cx.transitionTo(new Normal(this.cx));
} }
} }
selectedTool(): Tool | null {
return "select";
}
} }
export class Wiring implements State { export class Wiring implements State {
private unsubscribe!: EventUnsub; private unsubscribe!: EventUnsub;
constructor(private cx: Cx) {} constructor(private cx: Editor) {}
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(