fix project
This commit is contained in:
parent
ae14bc3724
commit
c1e72f9ae2
@ -11,13 +11,12 @@ function App(): ReactElement {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1>nandsim</h1>
|
||||
<div className="Editor">
|
||||
<div>
|
||||
<Toolbar editor={editor} canvasRef={canvasRef} />
|
||||
</div>
|
||||
<main>
|
||||
<Tabbar editor={editor} />
|
||||
<Tabbar editor={editor} canvasRef={canvasRef} />
|
||||
<Canvas editor={editor} canvasRef={canvasRef} />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@ -9,9 +9,28 @@ type Props = {
|
||||
|
||||
function Canvas({ editor, canvasRef }: Props): ReactElement {
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
if (canvasRef.current) {
|
||||
editor.render(canvasRef.current);
|
||||
}
|
||||
|
||||
const unsubscribe = editor.events.subscribe(["RenderRequest"], (ev) => {
|
||||
if (canvasRef.current) {
|
||||
editor.render(canvasRef.current);
|
||||
}
|
||||
});
|
||||
|
||||
function onResize() {
|
||||
if (canvasRef.current) {
|
||||
editor.render(canvasRef.current);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("resize", onResize);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", onResize);
|
||||
unsubscribe();
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
@ -24,30 +43,24 @@ function Canvas({ editor, canvasRef }: Props): ReactElement {
|
||||
onMouseDown={(ev) => {
|
||||
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
editor.events.send({ tag: "MouseDown", pos });
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
onMouseUp={(ev) => {
|
||||
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
editor.events.send({ tag: "MouseUp", pos });
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
onMouseMove={(ev) => {
|
||||
const deltaPos = v2(ev.movementX, ev.movementY);
|
||||
const pos = v2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
|
||||
editor.events.send({ tag: "MouseMove", pos, deltaPos });
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
onMouseLeave={(ev) => {
|
||||
onMouseLeave={(_ev) => {
|
||||
editor.events.send({ tag: "MouseLeave" });
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
onKeyDown={(ev) => {
|
||||
editor.events.send({ tag: "KeyDown", key: ev.key });
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
onKeyUp={(ev) => {
|
||||
editor.events.send({ tag: "KeyUp", key: ev.key });
|
||||
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,17 +1,63 @@
|
||||
import { useEffect, useState, type ReactElement } from "react";
|
||||
import { useEffect, useState, type ReactElement, type RefObject } from "react";
|
||||
import type { Editor } from "./editor/Editor";
|
||||
|
||||
type Props = { editor: Editor };
|
||||
type Props = { editor: Editor; canvasRef: RefObject<HTMLCanvasElement | null> };
|
||||
|
||||
function Tabbar({ editor }: Props): ReactElement {
|
||||
const [selectedTool, setSelectedTool] = useState("select");
|
||||
function Tabbar({ editor, canvasRef }: Props): ReactElement {
|
||||
const [updateId, update] = useState(0);
|
||||
const [selectedTab, setSelectedTab] = useState(0);
|
||||
|
||||
useEffect(() =>
|
||||
editor.events.subscribe(["ShowSelectedTab"], (ev) => {
|
||||
setSelectedTab(ev.idx);
|
||||
update(updateId + 1);
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="Tabbar">
|
||||
<button className="active"><unnamed></button>
|
||||
<button>Component one</button>
|
||||
<button>Another components</button>
|
||||
<div>
|
||||
{editor.availableBoardEditors().map((tab, idx) => (
|
||||
<button
|
||||
key={`${idx}${updateId}`}
|
||||
className={selectedTab === idx ? "active" : ""}
|
||||
onClick={() => {
|
||||
editor.events.send({ tag: "SelectTab", idx });
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
className="add"
|
||||
onClick={() => {
|
||||
editor.events.send({ tag: "CreateTab" });
|
||||
canvasRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.events.send({ tag: "SaveComponent" });
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const name = prompt("New component name:");
|
||||
if (!name) return;
|
||||
editor.events.send({ tag: "RenameComponent", newName: name });
|
||||
}}
|
||||
>
|
||||
Rename
|
||||
</button>
|
||||
<button onClick={() => {}}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -17,9 +17,9 @@ function Toolbar({ editor, canvasRef }: Props): ReactElement {
|
||||
<div className="Toolbar">
|
||||
<h2>Toolbar</h2>
|
||||
<div>
|
||||
{editor.tools().map((tool, key) => (
|
||||
{editor.availableTools().map((tool, key) => (
|
||||
<button
|
||||
key={`${key}`}
|
||||
key={key}
|
||||
className={selectedTool === tool ? "active" : ""}
|
||||
onClick={() => {
|
||||
editor.events.send({ tag: "SelectTool", tool });
|
||||
@ -30,9 +30,7 @@ function Toolbar({ editor, canvasRef }: Props): ReactElement {
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
<button className="add">+</button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -21,6 +21,27 @@ export class Board {
|
||||
|
||||
constructor() {}
|
||||
|
||||
static withExample(repo: ComponentRepo): Board {
|
||||
const board = new Board();
|
||||
board.placeComponent(repo.get("input"), v2(100, 100));
|
||||
board.placeComponent(repo.get("input"), v2(100, 200));
|
||||
board.placeComponent(repo.get("and"), v2(300, 150));
|
||||
board.placeComponent(repo.get("output"), v2(500, 150));
|
||||
board.addWire(
|
||||
{ tag: "OutputPin", comp: board.components[0], i: 0 },
|
||||
{ tag: "InputPin", comp: board.components[2], i: 0 },
|
||||
);
|
||||
board.addWire(
|
||||
{ tag: "OutputPin", comp: board.components[1], i: 0 },
|
||||
{ tag: "InputPin", comp: board.components[2], i: 1 },
|
||||
);
|
||||
board.addWire(
|
||||
{ tag: "OutputPin", comp: board.components[2], i: 0 },
|
||||
{ tag: "InputPin", comp: board.components[3], i: 0 },
|
||||
);
|
||||
return board;
|
||||
}
|
||||
|
||||
canPlaceComponent(kind: ComponentKind, pos: V2): boolean {
|
||||
return !this.components.some((comp) =>
|
||||
rectsCollide(comp.pos, comp.kind.size, pos, kind.size),
|
||||
@ -194,6 +215,22 @@ export class Board {
|
||||
this.wires = this.wires.filter((wire) => !wire.isSelected(selection));
|
||||
}
|
||||
|
||||
toComponentKind(name: string): ComponentKind {
|
||||
const inputCount = this.components.filter(
|
||||
(comp) => comp.kind.label === "input",
|
||||
).length;
|
||||
const outputCount = this.components.filter(
|
||||
(comp) => comp.kind.label === "output",
|
||||
).length;
|
||||
const pinMax = Math.max(inputCount, outputCount);
|
||||
return new ComponentKind(
|
||||
v2(60 + name.length * 5, 40 + 10 * pinMax),
|
||||
name,
|
||||
new Array<null>(inputCount).fill(null),
|
||||
new Array<null>(outputCount).fill(null),
|
||||
);
|
||||
}
|
||||
|
||||
toIr(): ir.Component {
|
||||
console.log("Lowering to IR");
|
||||
|
||||
@ -337,6 +374,10 @@ export class ComponentRepo {
|
||||
return repo;
|
||||
}
|
||||
|
||||
available(): string[] {
|
||||
return [...this.defs.keys()];
|
||||
}
|
||||
|
||||
add(ident: string, kind: ComponentKind) {
|
||||
this.defs.set(ident, kind);
|
||||
}
|
||||
|
||||
@ -10,31 +10,39 @@ import { v2, type V2 } from "./V2";
|
||||
import { ViewPos } from "./ViewPos";
|
||||
import { type ComponentKind } from "./Board";
|
||||
import type { EventUnsub } from "./events";
|
||||
import { Project } from "./Project";
|
||||
|
||||
export class Editor {
|
||||
public events = new EventBus();
|
||||
public viewpos = new ViewPos(this.events);
|
||||
private renderNeeded = false;
|
||||
public mouse = new Mouse(this.events);
|
||||
|
||||
private state: State = new Normal(this);
|
||||
public project = Project.loadLocalStoreOrInitNew(this.events);
|
||||
public board = this.project.currentBoard();
|
||||
|
||||
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);
|
||||
private state: State = new Normal(this);
|
||||
|
||||
constructor() {
|
||||
this.state.enter();
|
||||
|
||||
this.events.subscribe(
|
||||
["MouseDown", "MouseUp", "MouseMove", "KeyDown", "KeyUp", "SelectTool"],
|
||||
[
|
||||
"MouseDown",
|
||||
"MouseUp",
|
||||
"MouseMove",
|
||||
"KeyDown",
|
||||
"KeyUp",
|
||||
"SelectTool",
|
||||
"CreateTab",
|
||||
"SelectTab",
|
||||
"SaveComponent",
|
||||
"RenameComponent",
|
||||
],
|
||||
(ev) => {
|
||||
switch (ev.tag) {
|
||||
case "KeyDown":
|
||||
@ -45,10 +53,30 @@ export class Editor {
|
||||
break;
|
||||
case "SelectTool":
|
||||
this.onSelectTool(ev.tool);
|
||||
break;
|
||||
case "CreateTab": {
|
||||
const idx = this.project.newTab();
|
||||
this.switchTab(idx);
|
||||
break;
|
||||
}
|
||||
this.renderNeeded = true;
|
||||
case "SelectTab": {
|
||||
this.switchTab(ev.idx);
|
||||
break;
|
||||
}
|
||||
case "SaveComponent": {
|
||||
this.project.saveComponent();
|
||||
break;
|
||||
}
|
||||
case "RenameComponent": {
|
||||
this.project.renameComponent(ev.newName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.events.send({ tag: "RenderRequest" });
|
||||
},
|
||||
);
|
||||
|
||||
this.state.enter();
|
||||
}
|
||||
|
||||
render(canvas: HTMLCanvasElement) {
|
||||
@ -62,29 +90,12 @@ export class Editor {
|
||||
this.connectingWire?.render(r);
|
||||
}
|
||||
|
||||
renderIfNeeded(canvas: HTMLCanvasElement) {
|
||||
if (this.renderNeeded) {
|
||||
this.render(canvas);
|
||||
this.renderNeeded = false;
|
||||
}
|
||||
availableBoardEditors(): string[] {
|
||||
return this.project.availableBoardEditors();
|
||||
}
|
||||
|
||||
private onSelectTool(tool: string) {
|
||||
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.events.send({ tag: "ShowSelectedTool", tool });
|
||||
availableTools(): string[] {
|
||||
return this.project.availableTools();
|
||||
}
|
||||
|
||||
transitionTo(newState: State) {
|
||||
@ -118,9 +129,34 @@ export class Editor {
|
||||
// const sim = new Sim(comp, [], []);
|
||||
// sim.simulate();
|
||||
}
|
||||
private onSelectTool(tool: string) {
|
||||
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.events.send({ tag: "ShowSelectedTool", tool });
|
||||
}
|
||||
|
||||
tools(): string[] {
|
||||
return ["select", "pan", "input", "output", "and", "or", "not"];
|
||||
private switchTab(idx: number) {
|
||||
this.project.switchTab(idx);
|
||||
this.events.send({ tag: "ShowSelectedTab", idx });
|
||||
this.selectionBox = null;
|
||||
this.componentPlacer = null;
|
||||
this.selection = null;
|
||||
this.connectingWire = null;
|
||||
this.viewpos.offset.assign(v2(0, 0));
|
||||
this.board = this.project.currentBoard();
|
||||
this.transitionTo(new Normal(this));
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +172,13 @@ class Normal implements State {
|
||||
|
||||
enter(): void {
|
||||
this.unsubscribe = this.cx.events.subscribe(
|
||||
["MouseDownOffset", "MouseMoveOffset", "MouseDragBegin", "KeyDown"],
|
||||
[
|
||||
"MouseDownOffset",
|
||||
"MouseMoveOffset",
|
||||
"MouseDragBegin",
|
||||
"KeyDown",
|
||||
"MouseDoubleClick",
|
||||
],
|
||||
(ev) => {
|
||||
switch (ev.tag) {
|
||||
case "MouseDownOffset":
|
||||
@ -157,6 +199,15 @@ class Normal implements State {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "MouseDoubleClick": {
|
||||
this.cx.board.handleMouseClick(ev.pos, {
|
||||
onComponentClicked: (comp) => {
|
||||
if (comp.kind.label === "input") {
|
||||
}
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@ -256,7 +307,7 @@ class Placing implements State {
|
||||
private cx: Editor,
|
||||
private tool: string,
|
||||
) {
|
||||
this.compDef = this.cx.componentRepo.get(this.tool);
|
||||
this.compDef = this.cx.project.componentRepo.get(this.tool);
|
||||
}
|
||||
|
||||
enter(): void {
|
||||
|
||||
100
editor/src/editor/Project.ts
Normal file
100
editor/src/editor/Project.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { Board, ComponentRepo, type Component } from "./Board";
|
||||
import { type EventBus } from "./events";
|
||||
|
||||
export class Project {
|
||||
private current: BoardEditor;
|
||||
private selectedIdx = 0;
|
||||
|
||||
private constructor(
|
||||
private events: EventBus,
|
||||
private boardEditors: BoardEditor[],
|
||||
private components: Component[],
|
||||
public componentRepo: ComponentRepo,
|
||||
) {
|
||||
this.current = boardEditors[this.selectedIdx];
|
||||
}
|
||||
|
||||
static loadLocalStoreOrInitNew(events: EventBus): Project {
|
||||
if (globalThis.localStorage.getItem("nandsim")) {
|
||||
return this.loadLocalStorage();
|
||||
} else {
|
||||
return this.initNew(events);
|
||||
}
|
||||
}
|
||||
static initNew(events: EventBus): Project {
|
||||
const repo = ComponentRepo.withDefaults();
|
||||
return new Project(
|
||||
events,
|
||||
[
|
||||
{
|
||||
name: "(Unnamed)",
|
||||
board: Board.withExample(repo),
|
||||
},
|
||||
],
|
||||
[],
|
||||
repo,
|
||||
);
|
||||
}
|
||||
|
||||
static loadLocalStorage(): Project {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
currentBoard(): Board {
|
||||
return this.current.board;
|
||||
}
|
||||
|
||||
availableBoardEditors(): string[] {
|
||||
return this.boardEditors.map((e) => e.name);
|
||||
}
|
||||
|
||||
availableTools(): string[] {
|
||||
return this.componentRepo
|
||||
.available()
|
||||
.filter((e) => e !== this.current.name);
|
||||
}
|
||||
|
||||
newTab(): number {
|
||||
this.boardEditors.push({
|
||||
name: `(Unnamed ${this.boardEditors.length})`,
|
||||
board: new Board(),
|
||||
});
|
||||
this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx });
|
||||
return this.boardEditors.length - 1;
|
||||
}
|
||||
|
||||
switchTab(idx: number) {
|
||||
this.selectedIdx = idx;
|
||||
this.current = this.boardEditors[this.selectedIdx];
|
||||
this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx });
|
||||
this.events.send({ tag: "ShowSelectedTool", tool: this.current.name });
|
||||
}
|
||||
|
||||
closeTab(idx: number) {
|
||||
this.boardEditors.splice(idx, 1);
|
||||
if (this.boardEditors.length === 0) {
|
||||
this.newTab();
|
||||
}
|
||||
this.selectedIdx = 0;
|
||||
this.current = this.boardEditors[this.selectedIdx];
|
||||
this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx });
|
||||
}
|
||||
|
||||
renameComponent(newName: string) {
|
||||
this.current.name = newName;
|
||||
this.events.send({ tag: "ShowSelectedTab", idx: this.selectedIdx });
|
||||
}
|
||||
|
||||
saveComponent() {
|
||||
this.componentRepo.add(
|
||||
this.current.name,
|
||||
this.current.board.toComponentKind(this.current.name),
|
||||
);
|
||||
this.events.send({ tag: "ShowSelectedTool", tool: this.current.name });
|
||||
}
|
||||
}
|
||||
|
||||
type BoardEditor = {
|
||||
name: string;
|
||||
board: Board;
|
||||
};
|
||||
@ -25,6 +25,11 @@ export class V2 {
|
||||
return new V2(Math.abs(this.x), Math.abs(this.y));
|
||||
}
|
||||
|
||||
assign(rhs: V2) {
|
||||
this.x = rhs.x;
|
||||
this.y = rhs.y;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `V2(${this.x}, ${this.y})`;
|
||||
}
|
||||
|
||||
@ -16,8 +16,13 @@ export type Event =
|
||||
}
|
||||
| { tag: "KeyDown" | "KeyUp"; key: string }
|
||||
| { tag: "SelectTool" | "ShowSelectedTool"; tool: string }
|
||||
| { tag: "CreateTab" }
|
||||
| { tag: "SelectTab" | "ShowSelectedTab"; idx: number }
|
||||
| { tag: "MouseDownOffset"; pos: V2; absPos: V2 }
|
||||
| { tag: "MouseMoveOffset"; pos: V2; deltaPos: V2 };
|
||||
| { tag: "MouseMoveOffset"; pos: V2; deltaPos: V2 }
|
||||
| { tag: "RenderRequest" }
|
||||
| { tag: "SaveComponent" | "CloseComponent" }
|
||||
| { tag: "RenameComponent"; newName: string };
|
||||
|
||||
export type EventOf<Tag extends Event["tag"]> = Event & { tag: Tag };
|
||||
|
||||
|
||||
@ -45,7 +45,14 @@
|
||||
.Tabbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
justify-content: space-between;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding-left: 5px;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user