mirror of
https://git.sfja.dk/sfja/h6-logicirc.git
synced 2025-05-15 16:58:08 +01:00
place switches and leds
This commit is contained in:
parent
0db1c3b90a
commit
505e869e1f
@ -1,5 +1,5 @@
|
||||
import { EvHandler, EvHandlerRes, Mouse } from "./input.ts";
|
||||
import { Renderer } from "./renderer.ts";
|
||||
import { Renderer, RendererImage } from "./renderer.ts";
|
||||
|
||||
export class CanvasRenderer implements Renderer {
|
||||
constructor(
|
||||
@ -34,9 +34,15 @@ export class CanvasRenderer implements Renderer {
|
||||
g.fill();
|
||||
}
|
||||
|
||||
putImageData(data: ImageData, x: number, y: number, _hash = 0n): void {
|
||||
putImage(
|
||||
data: RendererImage,
|
||||
x: number,
|
||||
y: number,
|
||||
w = data.width,
|
||||
h = data.height,
|
||||
): void {
|
||||
const { g } = this;
|
||||
g.putImageData(data, x, y);
|
||||
g.drawImage(data, x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
|
94
src/grid.ts
94
src/grid.ts
@ -1,14 +1,18 @@
|
||||
import { Mouse } from "./input.ts";
|
||||
import { Renderer } from "./renderer.ts";
|
||||
import { EvHandler, Mouse } from "./input.ts";
|
||||
import { Renderer, RendererImage } from "./renderer.ts";
|
||||
|
||||
export class Grid {
|
||||
private t: Transformation = { s: 2, ox: 0, oy: 0 };
|
||||
private transformer = new Transformer(this.t);
|
||||
private tr = new TransformingRenderer(this.t);
|
||||
|
||||
private _transformingMouse: TransformingMouse;
|
||||
|
||||
constructor(
|
||||
private mouse: Mouse,
|
||||
) {
|
||||
this._transformingMouse = new TransformingMouse(this.mouse, this.t);
|
||||
|
||||
this.mouse.addOnPress(() => {
|
||||
this.transformer.startPan();
|
||||
return "stop";
|
||||
@ -28,11 +32,14 @@ export class Grid {
|
||||
} else {
|
||||
this.transformer.zoomOut();
|
||||
}
|
||||
this.tr.clearCache();
|
||||
return "stop";
|
||||
});
|
||||
}
|
||||
|
||||
transformingMouse(): Mouse {
|
||||
return this._transformingMouse;
|
||||
}
|
||||
|
||||
render(r: Renderer, renderTransformed: (r: Renderer) => void) {
|
||||
this.transformer.updateCanvas(r.width, r.height);
|
||||
this.drawGrid(r);
|
||||
@ -138,7 +145,6 @@ class Transformer {
|
||||
}
|
||||
|
||||
class TransformingRenderer implements Renderer {
|
||||
private imageDataCache = new Map<bigint, ImageData>();
|
||||
private r!: Renderer;
|
||||
|
||||
constructor(
|
||||
@ -150,10 +156,6 @@ class TransformingRenderer implements Renderer {
|
||||
renderTransformed(this);
|
||||
}
|
||||
|
||||
clearCache() {
|
||||
this.imageDataCache.clear();
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this.r.width / this.t.s;
|
||||
}
|
||||
@ -171,49 +173,47 @@ class TransformingRenderer implements Renderer {
|
||||
const { r, t: { s, ox, oy } } = this;
|
||||
r.fillCirc(x * s + ox, y * s + oy, radius * s, color);
|
||||
}
|
||||
putImageData(data: ImageData, x: number, y: number, hash = 0n): void {
|
||||
putImage(
|
||||
data: RendererImage,
|
||||
x: number,
|
||||
y: number,
|
||||
w = data.width,
|
||||
h = data.width,
|
||||
): void {
|
||||
const { r, t: { s, ox, oy } } = this;
|
||||
|
||||
if (hash === 0n || !this.imageDataCache.has(hash)) {
|
||||
const scaledWidth = data.width * s;
|
||||
const scaledHeight = data.height * s;
|
||||
|
||||
const canvas = new OffscreenCanvas(
|
||||
Math.max(data.width, scaledWidth),
|
||||
Math.max(data.height, scaledHeight),
|
||||
);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.putImageData(data, 0, 0);
|
||||
ctx.scale(s, s);
|
||||
|
||||
const scaledData = ctx.getImageData(
|
||||
0,
|
||||
0,
|
||||
scaledWidth,
|
||||
scaledHeight,
|
||||
);
|
||||
|
||||
if (hash === 0n) {
|
||||
r.putImageData(
|
||||
scaledData,
|
||||
x * s + ox,
|
||||
y * s + oy,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (this.imageDataCache.size > 128) {
|
||||
this.imageDataCache.delete(
|
||||
this.imageDataCache.keys().take(1).toArray()[0],
|
||||
);
|
||||
}
|
||||
this.imageDataCache.set(hash, scaledData);
|
||||
console.log(this.imageDataCache);
|
||||
}
|
||||
|
||||
r.putImageData(
|
||||
this.imageDataCache.get(hash)!,
|
||||
r.putImage(
|
||||
data,
|
||||
x * s + ox,
|
||||
y * s + oy,
|
||||
w * s,
|
||||
h * s,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TransformingMouse implements Mouse {
|
||||
constructor(
|
||||
private mouse: Mouse,
|
||||
private t: Transformation,
|
||||
) {}
|
||||
|
||||
get x(): number {
|
||||
return (this.mouse.x - this.t.ox) / this.t.s;
|
||||
}
|
||||
get y(): number {
|
||||
return (this.mouse.y - this.t.oy) / this.t.s;
|
||||
}
|
||||
addOnPress(handler: EvHandler): void {
|
||||
this.mouse.addOnPress(handler);
|
||||
}
|
||||
addOnRelease(handler: EvHandler): void {
|
||||
this.mouse.addOnRelease(handler);
|
||||
}
|
||||
addOnMove(handler: EvHandler): void {
|
||||
this.mouse.addOnMove(handler);
|
||||
}
|
||||
addOnScroll(handler: EvHandler<["up" | "down"]>): void {
|
||||
this.mouse.addOnScroll(handler);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ const g = c.getContext("2d", { alpha: false })!;
|
||||
c.width = document.body.clientWidth;
|
||||
c.height = document.body.clientHeight;
|
||||
c.style.position = "absolute";
|
||||
g.imageSmoothingEnabled = false;
|
||||
|
||||
const r = new CanvasRenderer(c, g);
|
||||
const mouse = new CanvasMouse(c);
|
||||
|
@ -1,30 +1,22 @@
|
||||
import { CanvasRenderer } from "./canvas.ts";
|
||||
import { Renderer } from "./renderer.ts";
|
||||
import { Renderer, RendererImage } from "./renderer.ts";
|
||||
|
||||
export class Painter implements Renderer {
|
||||
private c: OffscreenCanvas;
|
||||
private g: OffscreenCanvasRenderingContext2D;
|
||||
private r: Renderer;
|
||||
|
||||
private hash: bigint = 0n;
|
||||
|
||||
constructor(width: number, height: number) {
|
||||
this.c = new OffscreenCanvas(width, height);
|
||||
this.g = this.c.getContext("2d", { alpha: false })!;
|
||||
this.g = this.c.getContext("2d")!;
|
||||
this.r = new CanvasRenderer(
|
||||
this.c as unknown as HTMLCanvasElement,
|
||||
this.g as unknown as CanvasRenderingContext2D,
|
||||
);
|
||||
this.rehash();
|
||||
}
|
||||
|
||||
private rehash() {
|
||||
this.hash = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
|
||||
}
|
||||
|
||||
render(r: Renderer, x: number, y: number) {
|
||||
const data = this.g.getImageData(0, 0, this.c.width, this.c.height);
|
||||
r.putImageData(data, x, y, this.hash);
|
||||
r.putImage(this.c, x, y, this.c.width, this.c.height);
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
@ -34,19 +26,21 @@ export class Painter implements Renderer {
|
||||
return this.r.height;
|
||||
}
|
||||
clear(color: string): void {
|
||||
this.rehash();
|
||||
this.r.clear(color);
|
||||
}
|
||||
fillRect(x: number, y: number, w: number, h: number, color: string): void {
|
||||
this.rehash();
|
||||
this.r.fillRect(x, y, w, h, color);
|
||||
}
|
||||
fillCirc(x: number, y: number, radius: number, color: string): void {
|
||||
this.rehash();
|
||||
this.r.fillCirc(x, y, radius, color);
|
||||
}
|
||||
putImageData(data: ImageData, x: number, y: number, hash?: bigint): void {
|
||||
this.rehash();
|
||||
this.r.putImageData(data, x, y, hash);
|
||||
putImage(
|
||||
data: RendererImage,
|
||||
x: number,
|
||||
y: number,
|
||||
w = data.width,
|
||||
h = data.height,
|
||||
): void {
|
||||
this.r.putImage(data, x, y, w, h);
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
type N = number;
|
||||
|
||||
export type RendererImage =
|
||||
| HTMLCanvasElement
|
||||
| OffscreenCanvas
|
||||
| HTMLImageElement;
|
||||
|
||||
export interface Renderer {
|
||||
get width(): N;
|
||||
get height(): N;
|
||||
clear(color: string): void;
|
||||
fillRect(x: N, y: N, w: N, h: N, color: string): void;
|
||||
fillCirc(x: N, y: N, radius: N, color: string): void;
|
||||
putImageData(data: ImageData, x: N, y: N, hash?: bigint): void;
|
||||
putImage(data: RendererImage, x: N, y: N, w?: N, h?: N): void;
|
||||
}
|
||||
|
247
src/simulator.ts
247
src/simulator.ts
@ -5,35 +5,260 @@ import { Renderer } from "./renderer.ts";
|
||||
|
||||
export class Simulator {
|
||||
private grid: Grid;
|
||||
|
||||
private myComponent = new Component();
|
||||
private circuit: Circuit;
|
||||
private tooltip: Tooltip;
|
||||
private toolbar: Toolbar;
|
||||
|
||||
constructor(
|
||||
private mouse: Mouse,
|
||||
) {
|
||||
this.grid = new Grid(this.mouse);
|
||||
this.circuit = new Circuit(this.grid.transformingMouse());
|
||||
this.tooltip = new Tooltip(this.circuit, this.grid.transformingMouse());
|
||||
this.toolbar = new Toolbar(this.mouse, this.tooltip);
|
||||
}
|
||||
|
||||
render(r: Renderer) {
|
||||
hover: {
|
||||
if (this.toolbar.hover(this.mouse.x, this.mouse.y) === "break") {
|
||||
break hover;
|
||||
}
|
||||
}
|
||||
|
||||
r.clear("black");
|
||||
this.grid.render(r, (r) => {
|
||||
this.myComponent.render(r);
|
||||
this.grid.render(r, (tr) => {
|
||||
this.circuit.render(tr);
|
||||
this.toolbar.render(r);
|
||||
this.tooltip.render(tr);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Circuit {
|
||||
private components: Component[] = [];
|
||||
class Toolbar {
|
||||
private tools: ComponentFactory[] = [
|
||||
new SwitchComponent(),
|
||||
new LedComponent(),
|
||||
];
|
||||
private previews: Component[] = [];
|
||||
|
||||
private lastWidth = 0;
|
||||
private lastHeight = 0;
|
||||
|
||||
private hoveringComponentIdx?: number;
|
||||
|
||||
constructor(
|
||||
private mouse: Mouse,
|
||||
private tooltip: Tooltip,
|
||||
) {
|
||||
this.fillPreviews();
|
||||
|
||||
this.mouse.addOnPress(() => {
|
||||
const { x, y } = this.mouse;
|
||||
for (const [i, component] of this.previews.entries()) {
|
||||
if (
|
||||
x >= i * 128 + 96 - 48 &&
|
||||
y >= this.lastHeight - 100 - 48 &&
|
||||
x < i * 128 + 96 + component.width * 32 + 32 &&
|
||||
y < this.lastHeight - 100 + component.height * 32 + 32
|
||||
) {
|
||||
this.tooltip.select(this.tools[i].newInstance());
|
||||
return "stop";
|
||||
}
|
||||
}
|
||||
return "bubble";
|
||||
});
|
||||
}
|
||||
|
||||
private fillPreviews(): void {
|
||||
// this.previews = this.tools.map((tool) => tool.newInstance());
|
||||
this.previews = this.tools as unknown as Component[];
|
||||
}
|
||||
|
||||
hover(x: number, y: number): "continue" | "break" {
|
||||
for (const [i, component] of this.previews.entries()) {
|
||||
if (
|
||||
x >= i * 128 + 96 - 48 &&
|
||||
y >= this.lastHeight - 100 - 48 &&
|
||||
x < i * 128 + 96 + component.width * 32 + 32 &&
|
||||
y < this.lastHeight - 100 + component.height * 32 + 32
|
||||
) {
|
||||
this.hoveringComponentIdx = i;
|
||||
return "break";
|
||||
}
|
||||
}
|
||||
this.hoveringComponentIdx = undefined;
|
||||
return "continue";
|
||||
}
|
||||
|
||||
render(r: Renderer): void {
|
||||
this.lastWidth = r.width;
|
||||
this.lastHeight = r.height;
|
||||
|
||||
r.fillRect(0, r.height - 200, r.width, 200, "#aaa");
|
||||
for (const [i, component] of this.previews.entries()) {
|
||||
if (i === this.hoveringComponentIdx) {
|
||||
r.fillRect(
|
||||
i * 128 + 96 - 48,
|
||||
r.height - 100 - 48,
|
||||
component.height * 32 + 64,
|
||||
component.width * 32 + 64,
|
||||
"#00000088",
|
||||
);
|
||||
}
|
||||
component.render(r, i * 128 + 96, r.height - 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Component {
|
||||
private p = new Painter(64, 64);
|
||||
class Tooltip {
|
||||
private selectedComponent?: Component;
|
||||
|
||||
constructor(
|
||||
private circuit: Circuit,
|
||||
public mouse: Mouse,
|
||||
) {
|
||||
this.mouse.addOnPress(() => {
|
||||
if (!this.selectedComponent) {
|
||||
return "bubble";
|
||||
}
|
||||
|
||||
const x = Math.floor(this.mouse.x / 32);
|
||||
const y = Math.floor(this.mouse.y / 32);
|
||||
this.circuit.place(this.selectedComponent, x, y);
|
||||
this.selectedComponent = undefined;
|
||||
return "stop";
|
||||
});
|
||||
}
|
||||
|
||||
render(r: Renderer): void {
|
||||
this.selectedComponent?.render(r, this.mouse.x, this.mouse.y);
|
||||
}
|
||||
|
||||
select(component: Component): void {
|
||||
this.selectedComponent = component;
|
||||
}
|
||||
}
|
||||
|
||||
type PlacedComponent = {
|
||||
component: Component;
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
|
||||
class Circuit {
|
||||
private components: PlacedComponent[] = [];
|
||||
|
||||
constructor(
|
||||
private mouse: Mouse,
|
||||
) {
|
||||
this.mouse.addOnPress(() => {
|
||||
for (const { component, x, y } of this.components) {
|
||||
if (!component.click) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
this.mouse.x >= x * 32 &&
|
||||
this.mouse.y >= y * 32 &&
|
||||
this.mouse.x < x * 32 + component.width * 32 &&
|
||||
this.mouse.y < y * 32 + component.height * 32
|
||||
) {
|
||||
component.click(
|
||||
this.mouse.x - x * 32,
|
||||
this.mouse.y - y * 32,
|
||||
);
|
||||
return "stop";
|
||||
}
|
||||
}
|
||||
return "bubble";
|
||||
});
|
||||
}
|
||||
|
||||
place(component: Component, x: number, y: number): void {
|
||||
this.components.push({ component, x, y });
|
||||
}
|
||||
|
||||
render(r: Renderer): void {
|
||||
for (const { component, x, y } of this.components) {
|
||||
component.render(r, x * 32 + 16, y * 32 + 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Component {
|
||||
get width(): number;
|
||||
get height(): number;
|
||||
render(r: Renderer, x: number, y: number): void;
|
||||
click?(x: number, y: number): void;
|
||||
}
|
||||
|
||||
interface ComponentFactory {
|
||||
newInstance(): Component;
|
||||
}
|
||||
|
||||
class SwitchComponent implements Component, ComponentFactory {
|
||||
private graphicOn = new Painter(96, 64);
|
||||
private graphicOff = new Painter(96, 64);
|
||||
|
||||
private switchOn = false;
|
||||
|
||||
public width = 1;
|
||||
public height = 1;
|
||||
|
||||
constructor() {
|
||||
this.p.clear("black");
|
||||
this.graphicOff.fillRect(64, 30, 16, 4, "black");
|
||||
this.graphicOff.fillCirc(48 + 32, 32, 8, "black");
|
||||
this.graphicOff.fillCirc(48, 32, 16, "black");
|
||||
|
||||
this.graphicOn.fillRect(64, 30, 16, 4, "black");
|
||||
this.graphicOn.fillCirc(48 + 32, 32, 8, "black");
|
||||
this.graphicOn.fillCirc(48, 32, 16, "red");
|
||||
}
|
||||
|
||||
render(r: Renderer) {
|
||||
this.p.render(r, 0, 0);
|
||||
newInstance(): Component {
|
||||
return new SwitchComponent();
|
||||
}
|
||||
|
||||
render(r: Renderer, x: number, y: number): void {
|
||||
const graphic = this.switchOn ? this.graphicOn : this.graphicOff;
|
||||
graphic.render(r, x - 48, y - 32);
|
||||
}
|
||||
|
||||
click(x: number, y: number): void {
|
||||
const onButton = Math.sqrt(
|
||||
(x - 16) ** 2 + (y - 16) ** 2,
|
||||
) <= 16;
|
||||
|
||||
if (onButton) {
|
||||
this.switchOn = !this.switchOn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LedComponent implements Component, ComponentFactory {
|
||||
private graphicOn = new Painter(96, 64);
|
||||
private graphicOff = new Painter(96, 64);
|
||||
|
||||
private switchOn = false;
|
||||
|
||||
public width = 1;
|
||||
public height = 1;
|
||||
|
||||
constructor() {
|
||||
this.graphicOff.fillRect(16, 30, 16, 4, "black");
|
||||
this.graphicOff.fillCirc(16, 32, 8, "black");
|
||||
this.graphicOff.fillCirc(48, 32, 16, "black");
|
||||
|
||||
this.graphicOn.fillRect(16, 30, 16, 4, "black");
|
||||
this.graphicOn.fillCirc(16, 32, 8, "black");
|
||||
this.graphicOn.fillCirc(48, 32, 16, "red");
|
||||
}
|
||||
|
||||
newInstance(): Component {
|
||||
return new LedComponent();
|
||||
}
|
||||
|
||||
render(r: Renderer, x: number, y: number): void {
|
||||
const graphic = this.switchOn ? this.graphicOn : this.graphicOff;
|
||||
graphic.render(r, x - 48, y - 32);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
<title>LogiCirc</title>
|
||||
</head>
|
||||
<body>
|
||||
<script>0</script>
|
||||
<canvas id="editor"></canvas>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
Reference in New Issue
Block a user