selecting and wiring
This commit is contained in:
parent
2a172f687c
commit
0ec608ee54
@ -11,12 +11,18 @@ export default defineConfig([
|
|||||||
files: ['**/*.{ts,tsx}'],
|
files: ['**/*.{ts,tsx}'],
|
||||||
extends: [
|
extends: [
|
||||||
js.configs.recommended,
|
js.configs.recommended,
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommendedTypeChecked,
|
||||||
reactHooks.configs.flat.recommended,
|
reactHooks.configs.flat.recommended,
|
||||||
reactRefresh.configs.vite,
|
reactRefresh.configs.vite,
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": "off"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|||||||
@ -1,25 +1,61 @@
|
|||||||
|
import type { Selection } from "./Cx";
|
||||||
import type { Renderer } from "./Renderer";
|
import type { Renderer } from "./Renderer";
|
||||||
import { pointInsideRect, rectsCollide, v2, V2 } from "./V2";
|
import { pointInsideRect, rectsCollide, v2, V2 } from "./V2";
|
||||||
|
|
||||||
export class Board {
|
export class Board {
|
||||||
private components: Component[] = [];
|
private components: PlacedComponent[] = [];
|
||||||
|
|
||||||
private hoveredOverInput: [Component, number] | null = null;
|
private hoveredOverInput: [PlacedComponent, number] | null = null;
|
||||||
private hoveredOverOutput: [Component, number] | null = null;
|
private hoveredOverOutput: [PlacedComponent, number] | null = null;
|
||||||
|
|
||||||
canPlaceComponent(def: ComponentDef, pos: V2): boolean {
|
constructor() {}
|
||||||
|
|
||||||
|
canPlaceComponent(kind: ComponentKind, pos: V2): boolean {
|
||||||
return !this.components.some((comp) =>
|
return !this.components.some((comp) =>
|
||||||
rectsCollide(comp.pos, comp.def.size, pos, def.size),
|
rectsCollide(comp.pos, comp.kind.size, pos, kind.size),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
placeComponent(def: ComponentDef, pos: V2) {
|
placeComponent(kind: ComponentKind, pos: V2) {
|
||||||
this.components.push({ def, pos });
|
this.components.push({ kind: kind, pos });
|
||||||
}
|
}
|
||||||
|
|
||||||
render(r: Renderer) {
|
render(r: Renderer, selection: Selection | null) {
|
||||||
for (const comp of this.components) {
|
for (const comp of this.components) {
|
||||||
r.drawComponent(comp, this.hoveredOverInput, this.hoveredOverOutput);
|
const { pos, kind } = comp;
|
||||||
|
if (selection?.isComponentSelected(comp)) {
|
||||||
|
r.drawComponentBodySelected(pos, kind);
|
||||||
|
} else {
|
||||||
|
r.drawComponentBody(pos, kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { i, pinOffset } of kind.inputPinIter()) {
|
||||||
|
if (kind.inputs[i] !== null) {
|
||||||
|
throw new Error("pin text not implemented");
|
||||||
|
}
|
||||||
|
r.drawComponentInputPin(pos, pinOffset);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.hoveredOverInput?.[0] === comp &&
|
||||||
|
this.hoveredOverInput[1] === i
|
||||||
|
) {
|
||||||
|
r.drawComponentInputPinHover(pos, pinOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { i, pinOffset } of kind.outputPinIter()) {
|
||||||
|
if (kind.outputs[i] !== null) {
|
||||||
|
throw new Error("pin text not implemented");
|
||||||
|
}
|
||||||
|
r.drawComponentOutputPin(pos, kind, pinOffset);
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.hoveredOverOutput?.[0] === comp &&
|
||||||
|
this.hoveredOverOutput[1] === i
|
||||||
|
) {
|
||||||
|
r.drawComponentOutputPinHover(pos, kind, pinOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,10 +66,8 @@ export class Board {
|
|||||||
for (const comp of this.components) {
|
for (const comp of this.components) {
|
||||||
const {
|
const {
|
||||||
pos: { x, y },
|
pos: { x, y },
|
||||||
def: {
|
kind: {
|
||||||
size: { x: w, y: h },
|
size: { x: w },
|
||||||
inputs,
|
|
||||||
outputs,
|
|
||||||
},
|
},
|
||||||
} = comp;
|
} = comp;
|
||||||
|
|
||||||
@ -41,43 +75,35 @@ export class Board {
|
|||||||
!pointInsideRect(
|
!pointInsideRect(
|
||||||
pos,
|
pos,
|
||||||
comp.pos.sub(v2(5, 5)),
|
comp.pos.sub(v2(5, 5)),
|
||||||
comp.def.size.add(v2(10, 10)),
|
comp.kind.size.add(v2(10, 10)),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
{
|
for (const { i, pinOffset } of comp.kind.inputPinIter()) {
|
||||||
const pinSpace = h / (inputs.length + 1);
|
if (v2(x, y + pinOffset).distance(pos) < 6) {
|
||||||
for (let i = 0; i < inputs.length; ++i) {
|
|
||||||
if (v2(x, y + (i + 1) * pinSpace).distance(pos) < 5) {
|
|
||||||
this.hoveredOverInput = [comp, i];
|
this.hoveredOverInput = [comp, i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (const { i, pinOffset } of comp.kind.outputPinIter()) {
|
||||||
{
|
if (v2(x + w, y + pinOffset).distance(pos) < 6) {
|
||||||
const pinSpace = h / (outputs.length + 1);
|
|
||||||
for (let i = 0; i < outputs.length; ++i) {
|
|
||||||
if (v2(x + w, y + (i + 1) * pinSpace).distance(pos) < 5) {
|
|
||||||
this.hoveredOverOutput = [comp, i];
|
this.hoveredOverOutput = [comp, i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleMouseClick(
|
handleMouseClick(
|
||||||
pos: V2,
|
pos: V2,
|
||||||
inputPinClicked: (comp: Component, i: number) => void,
|
inputPinClicked: (comp: PlacedComponent, i: number) => void,
|
||||||
outputPinClicked: (comp: Component, i: number) => void,
|
outputPinClicked: (comp: PlacedComponent, i: number) => void,
|
||||||
componentClicked: (comp: Component) => void,
|
componentClicked: (comp: PlacedComponent) => void,
|
||||||
): "handled" | "not handled" {
|
): "handled" | "not handled" {
|
||||||
for (const comp of this.components) {
|
for (const comp of this.components) {
|
||||||
const {
|
const {
|
||||||
pos: { x, y },
|
pos: { x, y },
|
||||||
def: {
|
kind: {
|
||||||
size: { x: w, y: h },
|
size: { x: w },
|
||||||
inputs,
|
|
||||||
outputs,
|
|
||||||
},
|
},
|
||||||
} = comp;
|
} = comp;
|
||||||
|
|
||||||
@ -85,97 +111,118 @@ export class Board {
|
|||||||
!pointInsideRect(
|
!pointInsideRect(
|
||||||
pos,
|
pos,
|
||||||
comp.pos.sub(v2(5, 5)),
|
comp.pos.sub(v2(5, 5)),
|
||||||
comp.def.size.add(v2(10, 10)),
|
comp.kind.size.add(v2(10, 10)),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
{
|
for (const { i, pinOffset } of comp.kind.inputPinIter()) {
|
||||||
const pinSpace = h / (inputs.length + 1);
|
if (v2(x, y + pinOffset).distance(pos) < 6) {
|
||||||
for (let i = 0; i < inputs.length; ++i) {
|
|
||||||
if (v2(x, y + (i + 1) * pinSpace).distance(pos) < 5) {
|
|
||||||
inputPinClicked(comp, i);
|
inputPinClicked(comp, i);
|
||||||
return "handled";
|
return "handled";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (const { i, pinOffset } of comp.kind.outputPinIter()) {
|
||||||
{
|
if (v2(x + w, y + pinOffset).distance(pos) < 6) {
|
||||||
const pinSpace = h / (outputs.length + 1);
|
|
||||||
for (let i = 0; i < outputs.length; ++i) {
|
|
||||||
if (v2(x + w, y + (i + 1) * pinSpace).distance(pos) < 5) {
|
|
||||||
outputPinClicked(comp, i);
|
outputPinClicked(comp, i);
|
||||||
return "handled";
|
return "handled";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
componentClicked(comp);
|
componentClicked(comp);
|
||||||
return "handled";
|
return "handled";
|
||||||
}
|
}
|
||||||
return "not handled";
|
return "not handled";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentsInRect(pos: V2, size: V2): PlacedComponent[] {
|
||||||
|
return this.components.filter((comp) =>
|
||||||
|
rectsCollide(pos, size, comp.pos, comp.kind.size),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComponentRepo {
|
export class ComponentRepo {
|
||||||
private defs = new Map<string, ComponentDef>();
|
private defs = new Map<string, ComponentKind>();
|
||||||
|
|
||||||
static withDefaults(): ComponentRepo {
|
static withDefaults(): ComponentRepo {
|
||||||
const repo = new ComponentRepo();
|
const repo = new ComponentRepo();
|
||||||
|
|
||||||
repo.add("input", {
|
for (const { label, size, inputs, outputs } of defaultDefs) {
|
||||||
label: "input",
|
repo.add(label, new ComponentKind(size, label, inputs, outputs));
|
||||||
size: v2(80, 40),
|
}
|
||||||
inputs: [],
|
|
||||||
outputs: [null],
|
|
||||||
});
|
|
||||||
repo.add("output", {
|
|
||||||
label: "output",
|
|
||||||
size: v2(80, 40),
|
|
||||||
inputs: [null],
|
|
||||||
outputs: [],
|
|
||||||
});
|
|
||||||
repo.add("and", {
|
|
||||||
label: "and",
|
|
||||||
size: v2(80, 40),
|
|
||||||
inputs: [null, null],
|
|
||||||
outputs: [null],
|
|
||||||
});
|
|
||||||
repo.add("or", {
|
|
||||||
label: "or",
|
|
||||||
size: v2(80, 40),
|
|
||||||
inputs: [null, null],
|
|
||||||
outputs: [null],
|
|
||||||
});
|
|
||||||
repo.add("not", {
|
|
||||||
label: "not",
|
|
||||||
size: v2(80, 40),
|
|
||||||
inputs: [null],
|
|
||||||
outputs: [null],
|
|
||||||
});
|
|
||||||
|
|
||||||
return repo;
|
return repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
add(ident: string, def: ComponentDef) {
|
add(ident: string, kind: ComponentKind) {
|
||||||
this.defs.set(ident, def);
|
this.defs.set(ident, kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(ident: string): ComponentDef {
|
get(ident: string): ComponentKind {
|
||||||
const def = this.defs.get(ident);
|
const kind = this.defs.get(ident);
|
||||||
if (!def) {
|
if (!kind) {
|
||||||
throw new Error("should be defined");
|
throw new Error("should be defined");
|
||||||
}
|
}
|
||||||
return def;
|
return kind;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Component = {
|
export type PlacedComponent = {
|
||||||
def: ComponentDef;
|
kind: ComponentKind;
|
||||||
pos: V2;
|
pos: V2;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ComponentDef = {
|
export class ComponentKind {
|
||||||
size: V2;
|
constructor(
|
||||||
label: string;
|
public size: V2,
|
||||||
inputs: (string | null)[];
|
public label: string,
|
||||||
outputs: (string | null)[];
|
public inputs: (string | null)[],
|
||||||
};
|
public outputs: (string | null)[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
inputPinIter(): { i: number; pinOffset: number }[] {
|
||||||
|
return this.inputs.map((_, i) => ({
|
||||||
|
i,
|
||||||
|
pinOffset: ((i + 1) * this.size.y) / (this.inputs.length + 1),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
outputPinIter(): { i: number; pinOffset: number }[] {
|
||||||
|
return this.outputs.map((_, i) => ({
|
||||||
|
i,
|
||||||
|
pinOffset: ((i + 1) * this.size.y) / (this.outputs.length + 1),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultDefs = [
|
||||||
|
{
|
||||||
|
label: "input",
|
||||||
|
size: v2(80, 40),
|
||||||
|
inputs: [],
|
||||||
|
outputs: [null],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "output",
|
||||||
|
size: v2(80, 40),
|
||||||
|
inputs: [null],
|
||||||
|
outputs: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "and",
|
||||||
|
size: v2(80, 40),
|
||||||
|
inputs: [null, null],
|
||||||
|
outputs: [null],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "or",
|
||||||
|
size: v2(80, 40),
|
||||||
|
inputs: [null, null],
|
||||||
|
outputs: [null],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "not",
|
||||||
|
size: v2(80, 40),
|
||||||
|
inputs: [null],
|
||||||
|
outputs: [null],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@ -1,28 +1,35 @@
|
|||||||
import { Board, ComponentRepo } from "./Board";
|
import { Board, ComponentRepo, type PlacedComponent } from "./Board";
|
||||||
import { Renderer } from "./Renderer";
|
import { Renderer } from "./Renderer";
|
||||||
import * as states from "./states";
|
import * as states from "./states";
|
||||||
import { v2, V2 } from "./V2";
|
import { v2, V2 } from "./V2";
|
||||||
|
|
||||||
|
export type Tool = string;
|
||||||
|
|
||||||
export class Cx {
|
export class Cx {
|
||||||
public offset = v2(0, 0);
|
public offset = v2(0, 0);
|
||||||
private renderNeeded = false;
|
private renderNeeded = false;
|
||||||
private state = new states.Normal(this) as states.State;
|
private state = new states.Normal(this) as states.State;
|
||||||
private updateActions: (() => void)[] = [];
|
private updateActions: (() => void)[] = [];
|
||||||
|
|
||||||
private selectionBox: SelectionBox | null = null;
|
public selectionBox: SelectionBox | null = null;
|
||||||
private componentPlacer: ComponentPlacer | null = null;
|
private componentPlacer: ComponentPlacer | null = null;
|
||||||
|
public selection: Selection | null = null;
|
||||||
|
public connectingWire: ConnectingWire | null = null;
|
||||||
|
|
||||||
public board = new Board();
|
public board = new Board();
|
||||||
public componentRepo = ComponentRepo.withDefaults();
|
public componentRepo = ComponentRepo.withDefaults();
|
||||||
|
|
||||||
|
public keysPressed = new Set<string>();
|
||||||
|
|
||||||
render(canvas: HTMLCanvasElement) {
|
render(canvas: HTMLCanvasElement) {
|
||||||
const r = new Renderer(canvas, this.offset);
|
const r = new Renderer(canvas, this.offset);
|
||||||
|
|
||||||
r.clear();
|
r.clear();
|
||||||
r.drawGrid();
|
r.drawGrid();
|
||||||
this.board.render(r);
|
this.board.render(r, this.selection);
|
||||||
this.selectionBox?.render(r);
|
this.selectionBox?.render(r);
|
||||||
this.componentPlacer?.render(r);
|
this.componentPlacer?.render(r);
|
||||||
|
this.connectingWire?.render(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderIfNeeded(canvas: HTMLCanvasElement) {
|
renderIfNeeded(canvas: HTMLCanvasElement) {
|
||||||
@ -32,24 +39,27 @@ export class Cx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setRenderNeeded() {
|
|
||||||
this.renderNeeded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
mouseDown(pos: V2) {
|
mouseDown(pos: V2) {
|
||||||
this.state.onMouseDown?.(pos);
|
this.state.onMouseDown?.(pos);
|
||||||
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
mouseUp(pos: V2) {
|
mouseUp(pos: V2) {
|
||||||
this.state.onMouseUp?.(pos);
|
this.state.onMouseUp?.(pos);
|
||||||
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
mouseMove(deltaPos: V2, pos: V2) {
|
mouseMove(deltaPos: V2, pos: V2) {
|
||||||
this.state.onMouseMove?.(deltaPos, pos);
|
this.state.onMouseMove?.(deltaPos, pos);
|
||||||
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
keyDown(key: string) {
|
keyDown(key: string) {
|
||||||
|
this.keysPressed.add(key);
|
||||||
this.state.onKeyDown?.(key);
|
this.state.onKeyDown?.(key);
|
||||||
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
keyUp(key: string) {
|
keyUp(key: string) {
|
||||||
|
this.keysPressed.delete(key);
|
||||||
this.state.onKeyUp?.(key);
|
this.state.onKeyUp?.(key);
|
||||||
|
this.renderNeeded = true;
|
||||||
}
|
}
|
||||||
selectTool(tool: Tool) {
|
selectTool(tool: Tool) {
|
||||||
switch (tool) {
|
switch (tool) {
|
||||||
@ -67,6 +77,9 @@ export class Cx {
|
|||||||
this.transitionTo(new states.Normal(this));
|
this.transitionTo(new states.Normal(this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
selectedTool(): Tool {
|
||||||
|
return this.state.selectedTool?.() || "select";
|
||||||
|
}
|
||||||
|
|
||||||
addUpdateAction(action: () => void): object {
|
addUpdateAction(action: () => void): object {
|
||||||
this.updateActions.push(action);
|
this.updateActions.push(action);
|
||||||
@ -82,6 +95,7 @@ export class Cx {
|
|||||||
transitionTo(newState: states.State) {
|
transitionTo(newState: states.State) {
|
||||||
this.state.leaveState?.();
|
this.state.leaveState?.();
|
||||||
this.state = newState;
|
this.state = newState;
|
||||||
|
console.log(`Entering state ${newState.constructor.name}`);
|
||||||
this.state.enterState?.();
|
this.state.enterState?.();
|
||||||
this.notifyListeners();
|
this.notifyListeners();
|
||||||
}
|
}
|
||||||
@ -95,41 +109,19 @@ export class Cx {
|
|||||||
moveOffset(deltaPos: V2) {
|
moveOffset(deltaPos: V2) {
|
||||||
this.offset.x += deltaPos.x;
|
this.offset.x += deltaPos.x;
|
||||||
this.offset.y += deltaPos.y;
|
this.offset.y += deltaPos.y;
|
||||||
this.renderNeeded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
addSelectionRect(pos: V2) {
|
|
||||||
this.selectionBox = new SelectionBox(pos, v2(0, 0));
|
|
||||||
this.renderNeeded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeSelectionRect() {
|
|
||||||
this.selectionBox = null;
|
|
||||||
this.renderNeeded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
moveSelectionRect(deltaPos: V2) {
|
|
||||||
if (this.selectionBox) {
|
|
||||||
this.selectionBox.size.x += deltaPos.x;
|
|
||||||
this.selectionBox.size.y += deltaPos.y;
|
|
||||||
this.renderNeeded = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addComponentPlacer(pos: V2, size: V2) {
|
addComponentPlacer(pos: V2, size: V2) {
|
||||||
this.componentPlacer = new ComponentPlacer(pos, size);
|
this.componentPlacer = new ComponentPlacer(pos, size);
|
||||||
this.renderNeeded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeComponentPlacer() {
|
removeComponentPlacer() {
|
||||||
this.componentPlacer = null;
|
this.componentPlacer = null;
|
||||||
this.renderNeeded = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setComponentPlacerPos(pos: V2) {
|
setComponentPlacerPos(pos: V2) {
|
||||||
if (this.componentPlacer) {
|
if (this.componentPlacer) {
|
||||||
this.componentPlacer.pos = pos;
|
this.componentPlacer.pos = pos;
|
||||||
this.renderNeeded = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,14 +133,27 @@ export class Cx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SelectionBox {
|
export class SelectionBox {
|
||||||
constructor(
|
public size = v2(0, 0);
|
||||||
public pos: V2,
|
|
||||||
public size: V2,
|
constructor(public pos: V2) {}
|
||||||
) {}
|
|
||||||
|
|
||||||
render(r: Renderer) {
|
render(r: Renderer) {
|
||||||
r.drawSelectionBox(this.pos, this.size);
|
r.drawSelectionBox(this.pos, this.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
move(deltaPos: V2) {
|
||||||
|
this.size = this.size.add(deltaPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalized(): { 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), size: v2(w, h) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ComponentPlacer {
|
export class ComponentPlacer {
|
||||||
@ -162,4 +167,62 @@ export class ComponentPlacer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tool = string;
|
export class Selection {
|
||||||
|
selectedComponents = new Set<PlacedComponent>();
|
||||||
|
|
||||||
|
addComponent(comp: PlacedComponent) {
|
||||||
|
this.selectedComponents.add(comp);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleComponent(comp: PlacedComponent) {
|
||||||
|
if (this.selectedComponents.has(comp)) {
|
||||||
|
this.selectedComponents.delete(comp);
|
||||||
|
} else {
|
||||||
|
this.selectedComponents.add(comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isComponentSelected(comp: PlacedComponent) {
|
||||||
|
return this.selectedComponents.has(comp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConnectingWire {
|
||||||
|
constructor(
|
||||||
|
public kind: ConnectingWireKind,
|
||||||
|
public pos: V2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
render(r: Renderer) {
|
||||||
|
switch (this.kind.tag) {
|
||||||
|
case "InputPin":
|
||||||
|
case "OutputPin":
|
||||||
|
}
|
||||||
|
r.drawConnectingWire(this.beginPos(), this.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
move(pos: V2) {
|
||||||
|
this.pos = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.inputPinIter()[this.kind.i].pinOffset,
|
||||||
|
);
|
||||||
|
case "OutputPin":
|
||||||
|
return v2(
|
||||||
|
this.kind.comp.pos.x + this.kind.comp.kind.size.x,
|
||||||
|
this.kind.comp.pos.y +
|
||||||
|
this.kind.comp.kind.outputPinIter()[this.kind.i].pinOffset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectingWireKind =
|
||||||
|
| { tag: "InputPin"; comp: PlacedComponent; i: number }
|
||||||
|
| { tag: "OutputPin"; comp: PlacedComponent; i: number };
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { Component } from "./Board";
|
import type { ComponentKind } from "./Board";
|
||||||
import { v2, type V2 } from "./V2";
|
import { v2, type V2 } from "./V2";
|
||||||
|
|
||||||
export class Renderer {
|
export class Renderer {
|
||||||
@ -57,23 +57,10 @@ export class Renderer {
|
|||||||
c.strokeRect(x, y, w, h);
|
c.strokeRect(x, y, w, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawComponent(
|
drawComponentBody(pos: V2, kind: ComponentKind) {
|
||||||
comp: Component,
|
|
||||||
hoveredOverInput: [Component, number] | null,
|
|
||||||
hoveredOverOutput: [Component, number] | null,
|
|
||||||
) {
|
|
||||||
const { c, offset } = this;
|
const { c, offset } = this;
|
||||||
const {
|
const { x, y } = pos.add(offset);
|
||||||
def: {
|
const { x: w, y: h } = kind.size;
|
||||||
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.fillStyle = `#6abbde`;
|
||||||
c.fillRect(x, y, w, h);
|
c.fillRect(x, y, w, h);
|
||||||
@ -83,52 +70,92 @@ export class Renderer {
|
|||||||
|
|
||||||
c.fillStyle = `#333333`;
|
c.fillStyle = `#333333`;
|
||||||
c.font = "bold 16px monospace";
|
c.font = "bold 16px monospace";
|
||||||
const textMetrix = c.measureText(label);
|
const textMetrix = c.measureText(kind.label);
|
||||||
c.fillText(
|
c.fillText(
|
||||||
label,
|
kind.label,
|
||||||
x + w / 2 - textMetrix.width / 2,
|
x + w / 2 - textMetrix.width / 2,
|
||||||
y + 13 + h / 2 - 16 / 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`;
|
drawComponentBodySelected(pos: V2, kind: ComponentKind) {
|
||||||
c.beginPath();
|
const { c, offset } = this;
|
||||||
c.arc(x, y + (i + 1) * pinSpace, 4, 0, Math.PI * 2);
|
const { x, y } = pos.add(offset);
|
||||||
c.fill();
|
const { x: w, y: h } = kind.size;
|
||||||
|
|
||||||
if (hoveredOverInput?.[0] === comp && hoveredOverInput[1] === i) {
|
c.fillStyle = `#6abbde`;
|
||||||
c.strokeStyle = `#bbbbbb`;
|
c.fillRect(x, y, w, h);
|
||||||
|
c.strokeStyle = `#ff8800`;
|
||||||
c.lineWidth = 2;
|
c.lineWidth = 2;
|
||||||
c.beginPath();
|
c.strokeRect(x, y, w, h);
|
||||||
c.arc(x, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2);
|
|
||||||
c.stroke();
|
c.fillStyle = `#333333`;
|
||||||
}
|
c.font = "bold 16px monospace";
|
||||||
}
|
const textMetrix = c.measureText(kind.label);
|
||||||
}
|
c.fillText(
|
||||||
{
|
kind.label,
|
||||||
const pinSpace = h / (outputs.length + 1);
|
x + w / 2 - textMetrix.width / 2,
|
||||||
for (let i = 0; i < outputs.length; ++i) {
|
y + 13 + h / 2 - 16 / 2,
|
||||||
if (outputs[i] !== null) {
|
);
|
||||||
throw new Error("pin text not implemented");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawComponentInputPin(pos: V2, pinOffset: number) {
|
||||||
|
const { c, offset } = this;
|
||||||
|
const { x, y } = pos.add(offset);
|
||||||
|
|
||||||
c.fillStyle = `#333333`;
|
c.fillStyle = `#333333`;
|
||||||
c.beginPath();
|
c.beginPath();
|
||||||
c.arc(x + w, y + (i + 1) * pinSpace, 4, 0, Math.PI * 2);
|
c.arc(x, y + pinOffset, 4, 0, Math.PI * 2);
|
||||||
c.fill();
|
c.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
drawComponentInputPinHover(pos: V2, pinOffset: number) {
|
||||||
|
const { c, offset } = this;
|
||||||
|
const { x, y } = pos.add(offset);
|
||||||
|
|
||||||
if (hoveredOverOutput?.[0] === comp && hoveredOverOutput[1] === i) {
|
|
||||||
c.strokeStyle = `#eee`;
|
c.strokeStyle = `#eee`;
|
||||||
c.lineWidth = 2;
|
c.lineWidth = 2;
|
||||||
c.beginPath();
|
c.beginPath();
|
||||||
c.arc(x + w, y + (i + 1) * pinSpace, 5, 0, Math.PI * 2);
|
c.arc(x, y + pinOffset, 5, 0, Math.PI * 2);
|
||||||
c.stroke();
|
c.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawComponentOutputPin(pos: V2, kind: ComponentKind, pinOffset: number) {
|
||||||
|
const { c, offset } = this;
|
||||||
|
const {
|
||||||
|
size: { x: w },
|
||||||
|
} = kind;
|
||||||
|
const { x, y } = pos.add(offset);
|
||||||
|
|
||||||
|
c.fillStyle = `#333333`;
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(x + w, y + pinOffset, 4, 0, Math.PI * 2);
|
||||||
|
c.fill();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawComponentOutputPinHover(pos: V2, kind: ComponentKind, pinOffset: number) {
|
||||||
|
const { c, offset } = this;
|
||||||
|
const {
|
||||||
|
size: { x: w },
|
||||||
|
} = kind;
|
||||||
|
const { x, y } = pos.add(offset);
|
||||||
|
|
||||||
|
c.strokeStyle = `#eee`;
|
||||||
|
c.lineWidth = 2;
|
||||||
|
c.beginPath();
|
||||||
|
c.arc(x + w, y + pinOffset, 5, 0, Math.PI * 2);
|
||||||
|
c.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawConnectingWire(begin: V2, end: V2) {
|
||||||
|
const { c, offset } = this;
|
||||||
|
const { x: x0, y: y0 } = begin.add(offset);
|
||||||
|
const { x: x1, y: y1 } = end.add(offset);
|
||||||
|
|
||||||
|
c.strokeStyle = `#333333`;
|
||||||
|
c.lineWidth = 3;
|
||||||
|
c.beginPath();
|
||||||
|
c.moveTo(x0, y0);
|
||||||
|
c.lineTo(x1, y1);
|
||||||
|
c.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import type { ComponentDef } from "./Board";
|
import { type ComponentKind } from "./Board";
|
||||||
import type { Cx, Tool } from "./Cx";
|
import { ConnectingWire, Selection } from "./Cx";
|
||||||
|
import { SelectionBox, type Cx, type Tool } from "./Cx";
|
||||||
import { v2, type V2 } from "./V2";
|
import { v2, type V2 } from "./V2";
|
||||||
|
|
||||||
export interface State {
|
export interface State {
|
||||||
@ -20,21 +21,45 @@ export class Normal implements State {
|
|||||||
if (
|
if (
|
||||||
this.cx.board.handleMouseClick(
|
this.cx.board.handleMouseClick(
|
||||||
pos.sub(this.cx.offset),
|
pos.sub(this.cx.offset),
|
||||||
(comp, i) => {},
|
(comp, i) => {
|
||||||
(comp, i) => {},
|
console.log({ comp, i });
|
||||||
(comp) => {},
|
this.cx.connectingWire = new ConnectingWire(
|
||||||
|
{
|
||||||
|
tag: "InputPin",
|
||||||
|
comp,
|
||||||
|
i,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
this.cx.transitionTo(new Wiring(this.cx));
|
||||||
|
},
|
||||||
|
(comp, i) => {
|
||||||
|
this.cx.connectingWire = new ConnectingWire(
|
||||||
|
{
|
||||||
|
tag: "OutputPin",
|
||||||
|
comp,
|
||||||
|
i,
|
||||||
|
},
|
||||||
|
pos,
|
||||||
|
);
|
||||||
|
this.cx.transitionTo(new Wiring(this.cx));
|
||||||
|
},
|
||||||
|
(comp) => {
|
||||||
|
this.cx.selection = new Selection();
|
||||||
|
this.cx.selection.addComponent(comp);
|
||||||
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
|
},
|
||||||
) === "handled"
|
) === "handled"
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
this.cx.addSelectionRect(pos);
|
this.cx.selectionBox = new SelectionBox(pos);
|
||||||
this.cx.transitionTo(new SelectingBox(this.cx));
|
this.cx.transitionTo(new SelectingBox(this.cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove(_deltaPos: V2, pos: V2): void {
|
onMouseMove(_deltaPos: V2, pos: V2): void {
|
||||||
this.cx.board.updateMouseHover(pos.sub(this.cx.offset));
|
this.cx.board.updateMouseHover(pos.sub(this.cx.offset));
|
||||||
this.cx.setRenderNeeded();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown(key: string): void {
|
onKeyDown(key: string): void {
|
||||||
@ -88,7 +113,7 @@ export class Panning implements State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class Placing implements State {
|
export class Placing implements State {
|
||||||
private compDef: ComponentDef;
|
private compDef: ComponentKind;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cx: Cx,
|
private cx: Cx,
|
||||||
@ -131,21 +156,88 @@ export class Placing implements State {
|
|||||||
|
|
||||||
export class Selecting implements State {
|
export class Selecting implements State {
|
||||||
constructor(private cx: Cx) {}
|
constructor(private cx: Cx) {}
|
||||||
|
|
||||||
|
onMouseDown(pos: V2): void {
|
||||||
|
if (
|
||||||
|
this.cx.board.handleMouseClick(
|
||||||
|
pos.sub(this.cx.offset),
|
||||||
|
(_comp, _i) => {},
|
||||||
|
(_comp, _i) => {},
|
||||||
|
(comp) => {
|
||||||
|
if (this.cx.keysPressed.has("Control")) {
|
||||||
|
this.cx.selection?.toggleComponent(comp);
|
||||||
|
} else {
|
||||||
|
this.cx.selection = new Selection();
|
||||||
|
this.cx.selection.addComponent(comp);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) === "handled"
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
if (this.cx.keysPressed.has("Control")) {
|
||||||
|
this.cx.selectionBox = new SelectionBox(pos);
|
||||||
|
this.cx.transitionTo(new SelectingBox(this.cx));
|
||||||
|
} else {
|
||||||
|
this.cx.selection = null;
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SelectingBox implements State {
|
export class SelectingBox implements State {
|
||||||
constructor(private cx: Cx) {}
|
constructor(private cx: Cx) {}
|
||||||
|
|
||||||
onMouseUp(_pos: V2): void {
|
onMouseUp(_pos: V2): void {
|
||||||
this.cx.removeSelectionRect();
|
if (!this.cx.selectionBox) {
|
||||||
|
throw new Error("expected selectionBox to active");
|
||||||
|
}
|
||||||
|
const { pos, size } = this.cx.selectionBox.normalized();
|
||||||
|
const selected = this.cx.board.componentsInRect(
|
||||||
|
pos.sub(this.cx.offset),
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
if (selected.length > 0) {
|
||||||
|
this.cx.selection ??= new Selection();
|
||||||
|
}
|
||||||
|
for (const comp of selected) {
|
||||||
|
this.cx.selection?.addComponent(comp);
|
||||||
|
}
|
||||||
|
if (this.cx.selection) {
|
||||||
|
this.cx.selectionBox = null;
|
||||||
|
this.cx.transitionTo(new Selecting(this.cx));
|
||||||
|
} else {
|
||||||
|
this.cx.selectionBox = null;
|
||||||
this.cx.transitionTo(new Normal(this.cx));
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMouseMove(deltaPos: V2): void {
|
onMouseMove(deltaPos: V2): void {
|
||||||
this.cx.moveSelectionRect(deltaPos);
|
this.cx.selectionBox?.move(deltaPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedTool(): Tool | null {
|
selectedTool(): Tool | null {
|
||||||
return "select";
|
return "select";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class Wiring implements State {
|
||||||
|
constructor(private cx: Cx) {}
|
||||||
|
|
||||||
|
onMouseMove(_deltaPos: V2, pos: V2): void {
|
||||||
|
if (!this.cx.connectingWire) {
|
||||||
|
throw new Error("expected connectingWire to be active");
|
||||||
|
}
|
||||||
|
this.cx.connectingWire.move(pos);
|
||||||
|
this.cx.board.updateMouseHover(pos.sub(this.cx.offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(key: string): void {
|
||||||
|
if (key === "Escape") {
|
||||||
|
this.cx.transitionTo(new Normal(this.cx));
|
||||||
|
this.cx.connectingWire = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user