add editor

This commit is contained in:
sfja 2026-05-08 03:08:34 +02:00
parent 4640b579ec
commit eb40f5fba9
6 changed files with 137 additions and 82 deletions

View File

@ -1,12 +1,17 @@
import { type ReactElement } from "react";
import { useState, type ReactElement } from "react";
import "./App.css";
import Editor from "./Editor";
import Canvas from "./Canvas";
import { Editor } from "./Editor";
import Toolbar from "./Toolbar";
function App(): ReactElement {
const [editor] = useState(new Editor());
return (
<>
<h1>nandsim</h1>
<Editor></Editor>
<Canvas editor={editor} />
<Toolbar editor={editor} />
</>
);
}

View File

@ -1,5 +1,5 @@
.Editor {
.EditorView {
canvas {
image-rendering: pixelated;
}

51
editor/src/Canvas.tsx Normal file
View File

@ -0,0 +1,51 @@
import { useEffect, useRef, type ReactElement } from "react";
import "./Canvas.css";
import { V2, type Editor } from "./Editor";
type Props = { editor: Editor };
function Canvas({ editor }: Props): ReactElement {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current) return;
editor.render(ref.current);
});
return (
<>
<div className="EditorView">
<canvas
ref={ref}
width={1000}
height={1000}
style={{ width: 1000, height: 1000, backgroundColor: "black" }}
tabIndex={0}
onMouseDown={(ev) => {
const pos = V2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
editor.mouseDown(pos);
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}}
onMouseUp={(ev) => {
const pos = V2(ev.nativeEvent.offsetX, ev.nativeEvent.offsetY);
editor.mouseUp(pos);
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}}
onMouseMove={(ev) => {
editor.mouseMove(V2(ev.movementX, ev.movementY));
editor.renderIfNeeded(ev.target as HTMLCanvasElement);
}}
onKeyDown={(ev) => {
console.log(ev.key);
}}
onKeyUp={(ev) => {
console.log(ev.key);
}}
/>
</div>
</>
);
}
export default Canvas;

58
editor/src/Editor.ts Normal file
View File

@ -0,0 +1,58 @@
export type V2 = { x: number; y: number };
export const V2 = (x: number, y: number): V2 => ({ x, y });
export class Editor {
private offset = V2(0, 0);
private dragging = false;
private renderNeeded = false;
render(canvas: HTMLCanvasElement) {
const cx = canvas.getContext("2d")!;
cx.imageSmoothingEnabled = false;
cx.fillStyle = "#666";
cx.fillRect(0, 0, canvas.width, canvas.height);
const gridSize = { x: 20, y: 20 };
const dotSize = { x: 2, y: 2 };
cx.fillStyle = "#111";
for (let y = 0; y < canvas.width / gridSize.x + 1; ++y) {
for (let x = 0; x < canvas.height / gridSize.y + 1; ++x) {
cx.fillRect(
(this.offset.x % gridSize.x) + x * gridSize.x - dotSize.x / 2,
(this.offset.y % gridSize.y) + y * gridSize.y - dotSize.y / 2,
dotSize.x,
dotSize.y,
);
}
}
}
renderIfNeeded(canvas: HTMLCanvasElement) {
if (this.renderNeeded) {
this.render(canvas);
this.renderNeeded = false;
}
}
mouseDown(pos: V2) {
this.dragging = true;
}
mouseUp(pos: V2) {
this.dragging = false;
}
mouseMove(deltaPos: V2) {
if (this.dragging) {
this.offset.x += deltaPos.x;
this.offset.y += deltaPos.y;
this.renderNeeded = true;
}
}
selectTool(tool: Tool) {}
}
type Tool = "and" | "not" | "pin in" | "pin out";

View File

@ -1,78 +0,0 @@
import { useEffect, useRef, type ReactElement } from "react";
import "./Editor.css";
function Editor(): ReactElement {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (!ref.current) return;
let offset = { x: 0, y: 0 };
let dragging = false;
const canvas = ref.current;
const cx = canvas.getContext("2d")!;
cx.imageSmoothingEnabled = false;
const render = () => {
cx.fillStyle = "white";
cx.fillRect(0, 0, canvas.width, canvas.height);
const gridSize = { x: 20, y: 20 };
const dotSize = { x: 4, y: 4 };
cx.fillStyle = "gray";
for (let y = 0; y < canvas.width / gridSize.x + 1; ++y) {
for (let x = 0; x < canvas.height / gridSize.y + 1; ++x) {
cx.fillRect(
(offset.x % gridSize.x) + x * gridSize.x - dotSize.x / 2,
(offset.y % gridSize.y) + y * gridSize.y - dotSize.y / 2,
dotSize.x,
dotSize.y,
);
}
}
};
function mousedownHandler(ev: MouseEvent) {
dragging = true;
}
function mouseupHandler(ev: MouseEvent) {
dragging = false;
}
function mousemoveHandler(ev: MouseEvent) {
if (dragging) {
offset.x += ev.movementX;
offset.y += ev.movementY;
render();
}
}
canvas.addEventListener("mousedown", mousedownHandler);
canvas.addEventListener("mouseup", mouseupHandler);
canvas.addEventListener("mousemove", mousemoveHandler);
render();
return () => {
canvas.removeEventListener("mousedown", mousedownHandler);
canvas.removeEventListener("mouseup", mouseupHandler);
canvas.removeEventListener("mousemove", mousemoveHandler);
};
});
return (
<>
<div className="Editor">
<canvas
ref={ref}
width={1000}
height={1000}
style={{ width: 1000, height: 1000, backgroundColor: "black" }}
/>
</div>
</>
);
}
export default Editor;

19
editor/src/Toolbar.tsx Normal file
View File

@ -0,0 +1,19 @@
import type { ReactElement } from "react";
import type { Editor } from "./Editor";
type Props = { editor: Editor };
function Toolbar({ editor }: Props): ReactElement {
return (
<>
<div>
<button onClick={() => editor.selectTool("and")}>and</button>
<button onClick={() => editor.selectTool("not")}>not</button>
<button onClick={() => editor.selectTool("pin in")}>pin in</button>
<button onClick={() => editor.selectTool("pin out")}>pin out</button>
</div>
</>
);
}
export default Toolbar;