277 lines
No EOL
7.5 KiB
TypeScript
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);
|
|
}
|
|
} |