extract offset

This commit is contained in:
sfja 2026-06-10 04:10:36 +02:00
parent 9def12cad9
commit 022e8b89f7
4 changed files with 106 additions and 101 deletions

View File

@ -12,11 +12,12 @@ import * as ir from "./ir";
import { Sim } from "./sim"; import { Sim } from "./sim";
import { EventBus } from "./events"; import { EventBus } from "./events";
import { Mouse } from "./Mouse"; import { Mouse } from "./Mouse";
import { ViewPos } from "./ViewPos";
export type Tool = string; export type Tool = string;
export class Cx { export class Cx {
public offset = v2(0, 0); public viewpos: ViewPos;
private renderNeeded = false; private renderNeeded = false;
private state = new states.Normal(this) as states.State; private state = new states.Normal(this) as states.State;
@ -34,6 +35,7 @@ export class Cx {
public mouse: Mouse; public mouse: Mouse;
constructor(public events: EventBus) { constructor(public events: EventBus) {
this.viewpos = new ViewPos(events);
this.mouse = new Mouse(this.events); this.mouse = new Mouse(this.events);
this.state.enter(); this.state.enter();
@ -57,7 +59,7 @@ export class Cx {
} }
render(canvas: HTMLCanvasElement) { render(canvas: HTMLCanvasElement) {
const r = new Renderer(canvas, this.offset); const r = new Renderer(canvas, this.viewpos.offset);
r.clear(); r.clear();
r.drawGrid(); r.drawGrid();
@ -95,15 +97,10 @@ export class Cx {
transitionTo(newState: states.State) { transitionTo(newState: states.State) {
this.state.leave(); this.state.leave();
this.state = newState; this.state = newState;
console.log(`Entering state ${newState.constructor.name}`); // console.log(`Entering state ${newState.constructor.name}`);
this.state.enter(); this.state.enter();
} }
moveOffset(deltaPos: V2) {
this.offset.x += deltaPos.x;
this.offset.y += deltaPos.y;
}
addComponentPlacer(pos: V2, size: V2) { addComponentPlacer(pos: V2, size: V2) {
this.componentPlacer = new ComponentPlacer(pos, size); this.componentPlacer = new ComponentPlacer(pos, size);
} }
@ -118,12 +115,6 @@ export class Cx {
} }
} }
canvasPosToBoard(pos: V2): V2 {
const absX = pos.x - this.offset.x;
const absY = pos.y - this.offset.y;
return v2(absX, absY);
}
runSimulation() { runSimulation() {
// const comp = this.board.toIr(); // const comp = this.board.toIr();
// console.log("Before optimizing"); // console.log("Before optimizing");
@ -150,14 +141,14 @@ export class SelectionBox {
this.size = this.size.add(deltaPos); this.size = this.size.add(deltaPos);
} }
normalized(): { pos: V2; size: V2 } { boardRect(viewpos: ViewPos): { pos: V2; size: V2 } {
const normalizedAxis = (p: number, s: number): [number, number] => const normalizedAxis = (p: number, s: number): [number, number] =>
s >= 0 ? [p, s] : [p + s, -s]; s >= 0 ? [p, s] : [p + s, -s];
const [x, w] = normalizedAxis(this.pos.x, this.size.x); const [x, w] = normalizedAxis(this.pos.x, this.size.x);
const [y, h] = normalizedAxis(this.pos.y, this.size.y); const [y, h] = normalizedAxis(this.pos.y, this.size.y);
return { pos: v2(x, y), size: v2(w, h) }; return { pos: v2(x, y).sub(viewpos.offset), size: v2(w, h) };
} }
} }

View File

@ -0,0 +1,34 @@
import type { EventBus } from "./events";
import { v2, type V2 } from "./V2";
export class ViewPos {
public offset = v2(0, 0);
constructor(private events: EventBus) {
this.events.subscribe(["MouseDown", "MouseMove"], (ev) => {
const absPos = ev.pos;
const pos = this.canvasToBoard(absPos);
switch (ev.tag) {
case "MouseDown":
this.events.send({ tag: "MouseDownOffset", pos, absPos });
break;
case "MouseMove":
this.events.send({
tag: "MouseMoveOffset",
pos,
deltaPos: ev.deltaPos,
});
break;
}
});
}
canvasToBoard(pos: V2): V2 {
return pos.sub(this.offset);
}
move(deltaPos: V2) {
this.offset.x += deltaPos.x;
this.offset.y += deltaPos.y;
}
}

View File

@ -15,7 +15,9 @@ export type Event =
deltaPos: V2; deltaPos: V2;
} }
| { tag: "KeyDown" | "KeyUp"; key: string } | { tag: "KeyDown" | "KeyUp"; key: string }
| { tag: "SelectTool" | "ShowSelectedTool"; tool: string }; | { tag: "SelectTool" | "ShowSelectedTool"; tool: string }
| { tag: "MouseDownOffset"; pos: V2; absPos: V2 }
| { tag: "MouseMoveOffset"; pos: V2; deltaPos: V2 };
export type EventOf<Tag extends Event["tag"]> = Event & { tag: Tag }; export type EventOf<Tag extends Event["tag"]> = Event & { tag: Tag };

View File

@ -7,7 +7,6 @@ import { v2, type V2 } from "./V2";
export interface State { export interface State {
leave(): void; leave(): void;
enter(): void; enter(): void;
selectedTool?(): Tool | null;
} }
export class Normal implements State { export class Normal implements State {
@ -17,14 +16,14 @@ export class Normal implements State {
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
["MouseDown", "MouseMove", "MouseDragBegin", "KeyDown"], ["MouseDownOffset", "MouseMoveOffset", "MouseDragBegin", "KeyDown"],
(ev) => { (ev) => {
switch (ev.tag) { switch (ev.tag) {
case "MouseDown": case "MouseDownOffset":
this._onMouseDown(ev.pos); this.onMouseDown(ev.pos);
break; break;
case "MouseMove": case "MouseMoveOffset":
this.cx.board.updateMouseHover(ev.pos.sub(this.cx.offset)); this.cx.board.updateMouseHover(ev.pos);
break; break;
case "MouseDragBegin": { case "MouseDragBegin": {
this.cx.selectionBox = new SelectionBox(ev.pos, ev.deltaPos); this.cx.selectionBox = new SelectionBox(ev.pos, ev.deltaPos);
@ -50,20 +49,19 @@ export class Normal implements State {
this.unsubscribe(); this.unsubscribe();
} }
private _onMouseDown(pos: V2): void { private onMouseDown(pos: V2): void {
if ( this.cx.board.handleMouseClick(pos, {
this.cx.board.handleMouseClick(pos.sub(this.cx.offset), {
onInputPinClicked: (comp, i) => { onInputPinClicked: (comp, i) => {
this.cx.connectingWire = new ConnectingWire( this.cx.connectingWire = new ConnectingWire(
{ tag: "InputPin", comp, i }, { tag: "InputPin", comp, i },
pos.sub(this.cx.offset), pos,
); );
this.cx.transitionTo(new Wiring(this.cx)); this.cx.transitionTo(new Wiring(this.cx));
}, },
onOutputPinClicked: (comp, i) => { onOutputPinClicked: (comp, i) => {
this.cx.connectingWire = new ConnectingWire( this.cx.connectingWire = new ConnectingWire(
{ tag: "OutputPin", comp, i }, { tag: "OutputPin", comp, i },
pos.sub(this.cx.offset), pos,
); );
this.cx.transitionTo(new Wiring(this.cx)); this.cx.transitionTo(new Wiring(this.cx));
}, },
@ -80,18 +78,12 @@ export class Normal implements State {
} else { } else {
this.cx.connectingWire = new ConnectingWire( this.cx.connectingWire = new ConnectingWire(
{ tag: "Joint", joint }, { tag: "Joint", joint },
pos.sub(this.cx.offset), pos,
); );
this.cx.transitionTo(new Wiring(this.cx)); this.cx.transitionTo(new Wiring(this.cx));
} }
}, },
}) !== "handled" });
) {
}
}
selectedTool(): Tool | null {
return "select";
} }
} }
@ -107,7 +99,7 @@ export class Panning implements State {
switch (ev.tag) { switch (ev.tag) {
case "MouseDragBegin": case "MouseDragBegin":
case "MouseDrag": case "MouseDrag":
this.cx.moveOffset(ev.deltaPos); this.cx.viewpos.move(ev.deltaPos);
break; break;
case "KeyDown": { case "KeyDown": {
if (ev.key === "Escape") { if (ev.key === "Escape") {
@ -133,10 +125,6 @@ export class Panning implements State {
leave(): void { leave(): void {
this.unsubscribe(); this.unsubscribe();
} }
selectedTool(): Tool | null {
return "pan";
}
} }
export class Placing implements State { export class Placing implements State {
@ -153,11 +141,11 @@ export class Placing implements State {
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
["MouseDown", "MouseMove", "KeyDown"], ["MouseDownOffset", "MouseMove", "KeyDown"],
(ev) => { (ev) => {
switch (ev.tag) { switch (ev.tag) {
case "MouseDown": { case "MouseDownOffset": {
const boardPos = this.cx.canvasPosToBoard(ev.pos); const boardPos = ev.pos;
if (this.cx.board.canPlaceComponent(this.compDef, boardPos)) { if (this.cx.board.canPlaceComponent(this.compDef, boardPos)) {
this.cx.board.placeComponent(this.compDef, boardPos); this.cx.board.placeComponent(this.compDef, boardPos);
this.cx.transitionTo(new Normal(this.cx)); this.cx.transitionTo(new Normal(this.cx));
@ -185,10 +173,6 @@ export class Placing implements State {
this.cx.removeComponentPlacer(); this.cx.removeComponentPlacer();
this.unsubscribe(); this.unsubscribe();
} }
selectedTool(): Tool | null {
return this.tool;
}
} }
export class Selecting implements State { export class Selecting implements State {
@ -200,17 +184,17 @@ export class Selecting implements State {
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
["MouseDown", "MouseUp", "MouseMove", "KeyDown"], ["MouseDownOffset", "MouseUp", "MouseMoveOffset", "KeyDown"],
(ev) => { (ev) => {
switch (ev.tag) { switch (ev.tag) {
case "MouseDown": case "MouseDownOffset":
this.onMouseDown(ev.pos); this.onMouseDown(ev.pos, ev.absPos);
break; break;
case "MouseUp": case "MouseUp":
this.isMouseDown = false; this.isMouseDown = false;
break; break;
case "MouseMove": { case "MouseMoveOffset": {
this.cx.board.updateMouseHover(ev.pos.sub(this.cx.offset)); this.cx.board.updateMouseHover(ev.pos);
if (this.isMouseDown) { if (this.isMouseDown) {
this.cx.transitionTo(new Moving(this.cx)); this.cx.transitionTo(new Moving(this.cx));
} }
@ -236,9 +220,9 @@ export class Selecting implements State {
this.unsubscribe(); this.unsubscribe();
} }
private onMouseDown(pos: V2): void { private onMouseDown(pos: V2, absPos: V2): void {
if ( if (
this.cx.board.handleMouseClick(pos.sub(this.cx.offset), { this.cx.board.handleMouseClick(pos, {
onComponentClicked: (comp) => { onComponentClicked: (comp) => {
if (this.cx.keysPressed.has("Control")) { if (this.cx.keysPressed.has("Control")) {
this.cx.selection?.toggleComponent(comp); this.cx.selection?.toggleComponent(comp);
@ -258,11 +242,11 @@ export class Selecting implements State {
}) !== "handled" }) !== "handled"
) { ) {
if (this.cx.keysPressed.has("Control")) { if (this.cx.keysPressed.has("Control")) {
this.cx.selectionBox = new SelectionBox(pos); this.cx.selectionBox = new SelectionBox(absPos);
this.cx.transitionTo(new SelectingBox(this.cx)); this.cx.transitionTo(new SelectingBox(this.cx));
} else { } else {
this.cx.selection = null; this.cx.selection = null;
this.cx.selectionBox = new SelectionBox(pos); this.cx.selectionBox = new SelectionBox(absPos);
this.cx.transitionTo(new SelectingBox(this.cx)); this.cx.transitionTo(new SelectingBox(this.cx));
} }
} }
@ -326,13 +310,10 @@ export class SelectingBox implements State {
if (!this.cx.selectionBox) { if (!this.cx.selectionBox) {
throw new Error("expected selectionBox to active"); throw new Error("expected selectionBox to active");
} }
const { pos, size } = this.cx.selectionBox.normalized(); const { pos, size } = this.cx.selectionBox.boardRect(this.cx.viewpos);
const components = this.cx.board.componentsInRect( const components = this.cx.board.componentsInRect(pos, size);
pos.sub(this.cx.offset), const joints = this.cx.board.jointsInRect(pos, size);
size,
);
const joints = this.cx.board.jointsInRect(pos.sub(this.cx.offset), size);
if (components.length > 0 || joints.length > 0) { if (components.length > 0 || joints.length > 0) {
this.cx.selection ??= new Selection(); this.cx.selection ??= new Selection();
@ -366,18 +347,18 @@ export class Wiring implements State {
enter(): void { enter(): void {
this.unsubscribe = this.cx.events.subscribe( this.unsubscribe = this.cx.events.subscribe(
["MouseDown", "MouseMove", "KeyDown"], ["MouseDownOffset", "MouseMoveOffset", "KeyDown"],
(ev) => { (ev) => {
switch (ev.tag) { switch (ev.tag) {
case "MouseDown": case "MouseDownOffset":
this.onMouseDown(ev.pos); this.onMouseDown(ev.pos);
break; break;
case "MouseMove": { case "MouseMoveOffset": {
if (!this.cx.connectingWire) { if (!this.cx.connectingWire) {
throw new Error("expected connectingWire to be active"); throw new Error("expected connectingWire to be active");
} }
this.cx.connectingWire.move(ev.pos.sub(this.cx.offset)); this.cx.connectingWire.move(ev.pos);
this.cx.board.updateMouseHover(ev.pos.sub(this.cx.offset)); this.cx.board.updateMouseHover(ev.pos);
break; break;
} }
case "KeyDown": { case "KeyDown": {
@ -399,7 +380,7 @@ export class Wiring implements State {
private onMouseDown(pos: V2): void { private onMouseDown(pos: V2): void {
if ( if (
this.cx.board.handleMouseClick(pos.sub(this.cx.offset), { this.cx.board.handleMouseClick(pos, {
onInputPinClicked: (comp, i) => { onInputPinClicked: (comp, i) => {
this.cx.connectingWire!.connectToInput(this.cx.board, comp, i); this.cx.connectingWire!.connectToInput(this.cx.board, comp, i);
this.cx.connectingWire = null; this.cx.connectingWire = null;
@ -420,12 +401,9 @@ export class Wiring implements State {
const kind: ConnectingWireKind = { const kind: ConnectingWireKind = {
tag: "Intermediary", tag: "Intermediary",
prev: this.cx.connectingWire!, prev: this.cx.connectingWire!,
pos: pos.sub(this.cx.offset), pos,
}; };
this.cx.connectingWire = new ConnectingWire( this.cx.connectingWire = new ConnectingWire(kind, pos);
kind,
pos.sub(this.cx.offset),
);
} }
} }
} }