ProzillaOS/packages/apps/logic-sim/core/circuit.ts
2024-06-16 21:46:44 +02:00

277 lines
No EOL
7.5 KiB
TypeScript

import { BACKGROUND, COLORS, CURSORS, FONT, CONTROLLER, ENABLE_COLOR_CACHING } from "../../../../config/apps/logicSim.config";
import { Vector2 } from "../../../math/vector2";
import { Chip, ChipJson } from "./chips/chip";
import { ControlledPin } from "./pins/controlledPin";
import { InputHandler } from "./inputHandler";
import { Wire, WireJson } from "./wires/wire";
import { clamp } from "../../../_utils/math.utils";
export interface CircuitJson extends ChipJson {
wires: WireJson[];
chips: ChipJson[];
}
export class Circuit extends Chip {
canvas!: HTMLCanvasElement;
size = Vector2.ZERO;
context!: CanvasRenderingContext2D;
colors: { [key: string]: string } = {};
inputHandler: InputHandler;
inputPins: ControlledPin[] = [];
outputPins: ControlledPin[] = [];
wires: Wire[] = [];
chips: Chip[] = [];
cursor = CURSORS.default;
lastId = 0;
constructor(name: string, color: string, inputCount: number, outputCount: number) {
super(null, name, color, false, inputCount, outputCount);
this.inputHandler = new InputHandler(this);
}
resize() {
this.size.x = this.canvas.clientWidth;
this.size.y = this.canvas.clientHeight;
}
init(canvas: HTMLCanvasElement) {
this.canvas = canvas;
this.context = this.canvas.getContext("2d") as CanvasRenderingContext2D;
this.resize();
// Detect size changes of canvas
const observer = new ResizeObserver((entries) => {
entries.forEach(({ target }) => {
if (target === this.canvas && (target.clientWidth != this.size.x || target.clientHeight != this.size.y)) {
this.resize();
}
});
});
observer.observe(this.canvas);
this.inputHandler.init();
this.render();
}
cleanup() {
this.inputHandler.cleanup();
}
reset() {
this.inputPins = [];
this.outputPins = [];
this.wires = [];
this.chips = [];
this.inputHandler.reset();
}
getColor(key: string) {
if (this.colors[key] != null)
return this.colors[key];
const color = getComputedStyle(this.canvas).getPropertyValue("--" + key);
if (ENABLE_COLOR_CACHING)
this.colors[key] = color;
return color;
}
isPointInsideRect(rect: { position: Vector2, size: Vector2 }, point: Vector2) {
return point.x > rect.position.x && point.y > rect.position.y
&& point.x < rect.position.x + rect.size.x && point.y < rect.position.y + rect.size.y;
}
getUniqueId() {
return this.lastId++;
}
getTextRect(size: number, content: string) {
this.context.textBaseline = "middle";
this.context.font = `bold ${size}px ${FONT}`;
const metrics = this.context.measureText(content);
const width = metrics.actualBoundingBoxRight + metrics.actualBoundingBoxLeft;
const height = metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent;
return { x: width, y: height };
}
drawRect(style: string, positionX: number, positionY: number, sizeX: number, sizeY: number) {
this.context.fillStyle = style;
this.context.fillRect(positionX, positionY, sizeX, sizeY);
}
drawCircle(style: string, positionX: number, positionY: number, radius: number) {
this.context.beginPath();
this.context.arc(positionX, positionY, radius, 0, 2 * Math.PI);
this.context.fillStyle = style;
this.context.fill();
}
drawCurvedLine(style: string, positions: Vector2[], width: number, radius: number, resolution: number) {
if (positions.length < 2)
return;
this.context.lineWidth = width;
this.context.lineJoin = "round";
this.context.lineCap = "round";
/**
* Based on https://github.com/SebLague/Digital-Logic-Sim/blob/main/Assets/Modules/Chip%20Creation/Scripts/Chip/Wires/WireRenderer.cs
* TO DO: optimize
*/
const drawPoints: Vector2[] = [];
drawPoints.push(positions[0]);
for (let i = 1; i < positions.length - 1; i++) {
const targetPoint = positions[i];
const targetDir = Vector2.normalize(Vector2.subtract(positions[i], positions[i - 1]));
const distanceToTarget = Vector2.magnitude(Vector2.subtract(positions[i], positions[i - 1]));
const distanceToCurveStart = Math.max(distanceToTarget - radius, distanceToTarget / 2);
const nextTargetDir = Vector2.normalize(Vector2.subtract(positions[i + 1], positions[i]));
const nextLineLength = Vector2.magnitude(Vector2.subtract(positions[i + 1], positions[i]));
const curveStartPoint = Vector2.add(positions[i - 1], Vector2.scale(targetDir, distanceToCurveStart));
const curveEndPoint = Vector2.add(targetPoint, Vector2.scale(nextTargetDir, Math.min(radius, nextLineLength / 2)));
// Bezier
for (let j = 0; j < resolution; j++) {
const t = j / (resolution - 1);
const a = Vector2.lerp(curveStartPoint, targetPoint, t);
const b = Vector2.lerp(targetPoint, curveEndPoint, t);
const p = Vector2.lerp(a, b, t);
if (Vector2.sqrDistance(p, drawPoints[drawPoints.length - 1]) > 0.001) {
drawPoints.push(p);
}
}
}
drawPoints.push(positions[positions.length - 1]);
this.context.beginPath();
this.context.moveTo(drawPoints[0].x, drawPoints[0].y);
for (let i = 1; i < drawPoints.length; i++) {
this.context.lineTo(drawPoints[i].x, drawPoints[i].y);
}
this.context.strokeStyle = style;
this.context.stroke();
}
drawText(style: string, align: CanvasTextAlign, positionX: number, positionY: number, size: number, content: string) {
this.context.fillStyle = style;
this.context.textAlign = align;
this.context.textBaseline = "middle";
this.context.font = `bold ${size}px ${FONT}`;
this.context.fillText(content, positionX, positionY);
}
setDrawingOpacity(alpha: number) {
this.context.globalAlpha = clamp(alpha, 0, 1);
}
resetDrawingOpacity() {
this.setDrawingOpacity(1);
}
drawBackground() {
const margin = CONTROLLER.handleTrackWidth;
const padding = BACKGROUND.padding;
this.drawRect(this.getColor(COLORS.background.margin), 0, 0, this.size.x, this.size.y);
let offset = 0;
this.drawRect(
this.getColor(COLORS.background.outer),
offset + margin, offset,
this.size.x - margin * 2, this.size.y
);
offset = padding - BACKGROUND.borderWidth;
this.drawRect(
this.getColor(COLORS.background.border),
offset + margin, offset,
this.size.x - offset * 2 - margin * 2, this.size.y - offset * 2
);
offset = padding;
this.drawRect(
this.getColor(COLORS.background.inner),
offset + margin, offset,
this.size.x - offset * 2 - margin * 2, this.size.y - offset * 2
);
}
drawWires() {
this.wires.forEach((wire, index) => {
const isPlacingWire = this.inputHandler.placingWire != null && index == this.wires.length - 1;
wire.draw(isPlacingWire);
});
}
drawChips() {
this.chips.forEach((chip, index) => {
const isPlacingChip = this.inputHandler.placingChip != null && index == this.chips.length - 1;
chip.draw(isPlacingChip);
});
}
draw() {
this.drawBackground();
this.drawWires();
this.drawChips();
super.drawPins(false);
}
render() {
if (this.canvas.width != this.size.x)
this.canvas.width = this.size.x;
if (this.canvas.height != this.size.y)
this.canvas.height = this.size.y;
this.cursor = CURSORS.default;
this.draw();
if (this.inputHandler.isPlacing) {
this.canvas.style.cursor = CURSORS.default;
} else {
this.canvas.style.cursor = this.cursor;
}
window.requestAnimationFrame(() => {
this.render();
});
}
toJson() {
const object = super.toJson() as CircuitJson;
if (this.wires.length > 0)
object.wires = this.wires.map((wire) => wire.toJson());
if (this.chips.length > 0)
object.chips = this.chips.map((chip) => chip.toJson());
return object;
}
toString(): string {
const json = this.toJson();
return JSON.stringify(json);
}
}