From a9a7c781282e80f697899403cb04e13cc9332aca Mon Sep 17 00:00:00 2001 From: Prozilla Date: Sun, 2 Jun 2024 18:28:51 +0200 Subject: [PATCH] Added logic sim app --- .../apps/logic-sim/CircuitView.module.css | 4 + src/components/apps/logic-sim/CircuitView.tsx | 21 + .../apps/logic-sim/LogicSim.module.css | 4 + src/components/apps/logic-sim/LogicSim.tsx | 8 + .../text-editor/overrides/MarkdownImage.tsx | 2 +- src/components/taskbar/Taskbar.tsx | 2 +- src/components/taskbar/app-icon/AppIcon.tsx | 2 +- src/components/windows/WindowsView.tsx | 2 +- src/config/apps.config.ts | 12 +- src/config/apps/logicSim.config.ts | 47 +++ src/features/apps/appsManager.ts | 6 +- src/features/apps/logic-sim/chip.ts | 65 +++ src/features/apps/logic-sim/circuit.ts | 370 ++++++++++++++++++ src/features/apps/logic-sim/pin.ts | 56 +++ src/features/apps/logic-sim/state.ts | 18 + src/features/apps/logic-sim/wire.ts | 30 ++ src/features/math/vector2.ts | 28 +- src/features/modals/modalsManager.ts | 2 +- .../virtual-drive/root/defaultData.ts | 26 +- src/styles/global/variables.css | 1 + src/types/environment.d.ts | 6 + src/{ => types}/globals.d.ts | 0 22 files changed, 684 insertions(+), 28 deletions(-) create mode 100644 src/components/apps/logic-sim/CircuitView.module.css create mode 100644 src/components/apps/logic-sim/CircuitView.tsx create mode 100644 src/components/apps/logic-sim/LogicSim.module.css create mode 100644 src/components/apps/logic-sim/LogicSim.tsx create mode 100644 src/config/apps/logicSim.config.ts create mode 100644 src/features/apps/logic-sim/chip.ts create mode 100644 src/features/apps/logic-sim/circuit.ts create mode 100644 src/features/apps/logic-sim/pin.ts create mode 100644 src/features/apps/logic-sim/state.ts create mode 100644 src/features/apps/logic-sim/wire.ts create mode 100644 src/types/environment.d.ts rename src/{ => types}/globals.d.ts (100%) diff --git a/src/components/apps/logic-sim/CircuitView.module.css b/src/components/apps/logic-sim/CircuitView.module.css new file mode 100644 index 0000000..7881858 --- /dev/null +++ b/src/components/apps/logic-sim/CircuitView.module.css @@ -0,0 +1,4 @@ +.Canvas { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/src/components/apps/logic-sim/CircuitView.tsx b/src/components/apps/logic-sim/CircuitView.tsx new file mode 100644 index 0000000..1afd41a --- /dev/null +++ b/src/components/apps/logic-sim/CircuitView.tsx @@ -0,0 +1,21 @@ +import { useEffect, useRef, useState } from "react"; +import { Circuit } from "../../../features/apps/logic-sim/circuit"; +import styles from "./CircuitView.module.css"; + +export function CircuitView() { + const [circuit, setCircuit] = useState(new Circuit("Chip", "#000", 2, 1)); + const canvasRef = useRef(null); + + useEffect(() => { + if (canvasRef.current == null && circuit.canvas != null) + return; + + circuit.init(canvasRef.current as HTMLCanvasElement); + + return () => { + circuit.cleanup(); + }; + }, [canvasRef, circuit]); + + return ; +} \ No newline at end of file diff --git a/src/components/apps/logic-sim/LogicSim.module.css b/src/components/apps/logic-sim/LogicSim.module.css new file mode 100644 index 0000000..3b5120c --- /dev/null +++ b/src/components/apps/logic-sim/LogicSim.module.css @@ -0,0 +1,4 @@ +.Container { + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/src/components/apps/logic-sim/LogicSim.tsx b/src/components/apps/logic-sim/LogicSim.tsx new file mode 100644 index 0000000..f9f7c60 --- /dev/null +++ b/src/components/apps/logic-sim/LogicSim.tsx @@ -0,0 +1,8 @@ +import { CircuitView } from "./CircuitView"; +import styles from "./LogicSim.module.css"; + +export function LogicSim() { + return
+ +
; +} \ No newline at end of file diff --git a/src/components/apps/text-editor/overrides/MarkdownImage.tsx b/src/components/apps/text-editor/overrides/MarkdownImage.tsx index 9c73df4..3671f7c 100644 --- a/src/components/apps/text-editor/overrides/MarkdownImage.tsx +++ b/src/components/apps/text-editor/overrides/MarkdownImage.tsx @@ -16,7 +16,7 @@ import WindowsManager from "../../../../features/windows/windowsManager"; export function MarkdownImage({ currentFile, alt, src, modalsManager, windowsManager, app, ...props }) { const source = useMemo(() => { if (src.startsWith("public")) { - return src.replace(/^public\//g, `${process.env.PUBLIC_URL}/`); + return src.replace(/^public\//g, `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/`); } return src; diff --git a/src/components/taskbar/Taskbar.tsx b/src/components/taskbar/Taskbar.tsx index 0ddc2e0..25b034f 100644 --- a/src/components/taskbar/Taskbar.tsx +++ b/src/components/taskbar/Taskbar.tsx @@ -145,7 +145,7 @@ export const Taskbar = memo(() => { className={`${styles["Menu-button"]} ${styles["Home-button"]}`} onClick={() => { updateShowHome(!showHome); }} > - + diff --git a/src/components/taskbar/app-icon/AppIcon.tsx b/src/components/taskbar/app-icon/AppIcon.tsx index e0035bb..b383aac 100644 --- a/src/components/taskbar/app-icon/AppIcon.tsx +++ b/src/components/taskbar/app-icon/AppIcon.tsx @@ -77,7 +77,7 @@ export const AppButton: FC = memo(({ app, windowsManager, pins, }} title={app.name} > - + ); }); \ No newline at end of file diff --git a/src/components/windows/WindowsView.tsx b/src/components/windows/WindowsView.tsx index dd3f8b3..296f540 100644 --- a/src/components/windows/WindowsView.tsx +++ b/src/components/windows/WindowsView.tsx @@ -25,7 +25,7 @@ export const WindowsView: FC = memo(() => { useEffect(() => { const resetViewportTitleAndIcon = () => { setViewportTitle(`${NAME} | ${TAG_LINE}`); - setViewportIcon(`${process.env.PUBLIC_URL}/favicon.ico`); + setViewportIcon(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/favicon.ico`); }; if (sortedWindows.length === 0 || sortedWindows[sortedWindows.length - 1].minimized) diff --git a/src/config/apps.config.ts b/src/config/apps.config.ts index e0e5062..0a7fc4a 100644 --- a/src/config/apps.config.ts +++ b/src/config/apps.config.ts @@ -19,10 +19,10 @@ export const APP_NAMES = { }; export const APP_ICONS = { - TERMINAL: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.TERMINAL}.svg`, - SETTINGS: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.SETTINGS}.svg`, - MEDIA_VIEWER: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.MEDIA_VIEWER}.svg`, - TEXT_EDITOR: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.TEXT_EDITOR}.svg`, - FILE_EXPLORER: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.FILE_EXPLORER}.svg`, - CALCULATOR: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.CALCULATOR}.svg`, + TERMINAL: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.TERMINAL}.svg`, + SETTINGS: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.SETTINGS}.svg`, + MEDIA_VIEWER: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.MEDIA_VIEWER}.svg`, + TEXT_EDITOR: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.TEXT_EDITOR}.svg`, + FILE_EXPLORER: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.FILE_EXPLORER}.svg`, + CALCULATOR: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.CALCULATOR}.svg`, }; \ No newline at end of file diff --git a/src/config/apps/logicSim.config.ts b/src/config/apps/logicSim.config.ts new file mode 100644 index 0000000..fb58dcf --- /dev/null +++ b/src/config/apps/logicSim.config.ts @@ -0,0 +1,47 @@ +export const CURSORS = { + default: "default", + pointer: "pointer" +}; + +export const BACKGROUND = { + padding: 40, + borderWidth: 8, +}; + +export const INPUT_OUTPUT = { + radius: 24, + borderWidth: 4, + pinOffset: 40, + connectorWidth: 10, +}; + +export const WIRE = { + width: 4, +}; + +export const PIN = { + radius: 10, +}; + +export const COLORS = { + pin: { + fill: "dark-grey-b", + fillHover: "dark-grey-a" + }, + inputOutput: { + connector: "dark-grey-ca", + on: "red-b", + onHover: "red-a", + off: "dark-grey-d", + offHover: "dark-grey-ca", + stroke: "dark-grey-b" + }, + background: { + outer: "dark-grey-e", + inner: "dark-grey-f", + border: "dark-grey-d", + }, + wire: { + placing: "dark-grey-ca", + }, +}; \ No newline at end of file diff --git a/src/features/apps/appsManager.ts b/src/features/apps/appsManager.ts index ae29064..8383034 100644 --- a/src/features/apps/appsManager.ts +++ b/src/features/apps/appsManager.ts @@ -10,6 +10,7 @@ import Vector2 from "../math/vector2"; import { APPS, APP_NAMES } from "../../config/apps.config"; import { Browser } from "../../components/apps/browser/Browser"; import { IMAGE_FORMATS } from "../../config/apps/mediaViewer.config"; +import { LogicSim } from "../../components/apps/logic-sim/LogicSim"; export default class AppsManager { static APPS: App[] = [ @@ -35,6 +36,7 @@ export default class AppsManager { new App(APP_NAMES.BROWSER, APPS.BROWSER, Browser, { size: new Vector2(700, 500) }), + new App("Logic Sim", "logic-sim", LogicSim), ]; static getAppById(id: string): App | null { @@ -73,9 +75,9 @@ export default class AppsManager { */ static getAppIconUrl(appId: string, iconName?: string): string { if (iconName == null) { - return `${process.env.PUBLIC_URL}/assets/apps/icons/${appId}.svg`; + return `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${appId}.svg`; } else { - return `${process.env.PUBLIC_URL}/assets/apps/${appId}/icons/${iconName}.svg`; + return `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/${appId}/icons/${iconName}.svg`; } } } \ No newline at end of file diff --git a/src/features/apps/logic-sim/chip.ts b/src/features/apps/logic-sim/chip.ts new file mode 100644 index 0000000..3d3c615 --- /dev/null +++ b/src/features/apps/logic-sim/chip.ts @@ -0,0 +1,65 @@ +import Vector2 from "../../math/vector2"; +import { Circuit } from "./circuit"; +import { Pin } from "./pin"; +import { State } from "./state"; + +export class Chip { + color: string; + name: string; + position: Vector2; + circuit: Circuit; + + inputCount: number; + outputCount: number; + inputPins: Pin[]; + outputPins: Pin[]; + logic: (inputStates: State[]) => State[]; + + constructor(circuit: Circuit | null, name: string, color: string, inputCount: number, outputCount: number) { + Object.assign(this, { circuit, name, color, inputCount, outputCount }); + + if (this.circuit == null) + this.circuit = this as unknown as Circuit; + + this.inputPins = []; + for (let i = 0; i < inputCount; i++) { + this.inputPins.push(new Pin(this.circuit, "IN " + i, true, this, i)); + } + + this.outputPins = []; + for (let i = 0; i < outputCount; i++) { + this.outputPins.push(new Pin(this.circuit, "OUT " + i, false, this, i)); + } + } + + setInputState(index: number, state: State) { + if (this.inputPins[index].state.isEqual(state)) + return; + + this.inputPins[index].state = state; + this.update(); + } + + update() { + const inputStates: State[] = []; + for (let i = 0; i < this.inputCount; i++) { + const state = this.inputPins[i].state ?? State.LOW; + inputStates.push(state); + } + + const outputStates: State[] = this.logic(inputStates); + + for (let i = 0; i < this.outputCount; i++) { + this.outputPins[i].state = outputStates[i]; + } + } + + drawPins() { + this.inputPins.forEach((pin) => pin.draw()); + this.outputPins.forEach((pin) => pin.draw()); + } + + draw() { + this.drawPins(); + } +} \ No newline at end of file diff --git a/src/features/apps/logic-sim/circuit.ts b/src/features/apps/logic-sim/circuit.ts new file mode 100644 index 0000000..1d80282 --- /dev/null +++ b/src/features/apps/logic-sim/circuit.ts @@ -0,0 +1,370 @@ +import { BACKGROUND, COLORS, CURSORS, INPUT_OUTPUT, PIN, WIRE } from "../../../config/apps/logicSim.config"; +import Vector2 from "../../math/vector2"; +import { Chip } from "./chip"; +import { Pin } from "./pin"; +import { State } from "./state"; +import { Wire } from "./wire"; + +export class Circuit extends Chip { + canvas: HTMLCanvasElement; + size = Vector2.ZERO; + context: CanvasRenderingContext2D; + colors: { [key: string]: string } = {}; + mousePosition = Vector2.ZERO; + + isPlacing = false; + snapping = false; + placingWire: Wire; + wires: Wire[] = []; + + cursor = CURSORS.default; + + constructor(name: string, color: string, inputCount: number, outputCount: number) { + super(null, name, color, inputCount, outputCount); + } + + resize() { + this.size.x = this.canvas.clientWidth; + this.size.y = this.canvas.clientHeight; + + // Set pin positions to the side of the screen + this.inputPins.forEach((pin, index) => { + const positionX = BACKGROUND.padding + BACKGROUND.borderWidth / 2 + INPUT_OUTPUT.pinOffset; + const positionY = BACKGROUND.padding * 2 + (INPUT_OUTPUT.radius * 2 + 15) * index; + pin.position = new Vector2(positionX, positionY); + }); + this.outputPins.forEach((pin, index) => { + const positionX = this.size.x - (BACKGROUND.padding + BACKGROUND.borderWidth / 2 + INPUT_OUTPUT.pinOffset); + const positionY = BACKGROUND.padding * 2 + (INPUT_OUTPUT.radius * 2 + 15) * index; + pin.position = new Vector2(positionX, positionY); + }); + } + + setMousePosition(event: MouseEvent) { + const rect = this.canvas.getBoundingClientRect(); + this.mousePosition.x = event.clientX - rect.left; + this.mousePosition.y = event.clientY - rect.top; + } + + init(canvas: HTMLCanvasElement) { + this.canvas = canvas; + this.context = this.canvas.getContext("2d"); + this.resize(); + this.mousePosition = Vector2.ZERO; + + // 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.canvas.addEventListener("mousemove", this.onMouseMove); + this.canvas.addEventListener("click", this.onClick); + this.canvas.addEventListener("contextmenu", this.onClick); + + window.addEventListener("keydown", this.onKeyDown); + window.addEventListener("keyup", this.onKeyUp); + + this.render(); + } + + cleanup() { + this.canvas.removeEventListener("mousemove", this.onMouseMove); + this.canvas.removeEventListener("click", this.onClick); + this.canvas.removeEventListener("contextmenu", this.onClick); + + window.removeEventListener("keydown", this.onKeyDown); + window.removeEventListener("keyup", this.onKeyUp); + } + + onMouseMove = (event?: MouseEvent) => { + if (event != null) + this.setMousePosition(event); + + if (this.placingWire != null) { + const anchorCount = this.placingWire.anchorPoints.length; + const lastAnchorPoint = this.placingWire.anchorPoints[anchorCount - 1]; + + if (this.snapping) { + const previousAnchorPoint = anchorCount >= 2 + ? this.placingWire.anchorPoints[anchorCount - 2] + : this.placingWire.inputPin.position; + + const deltaX = Math.abs(this.mousePosition.x - previousAnchorPoint.x); + const deltaY = Math.abs(this.mousePosition.y - previousAnchorPoint.y); + + if (deltaX > deltaY) { + lastAnchorPoint.x = this.mousePosition.x; + lastAnchorPoint.y = previousAnchorPoint.y; + } else { + lastAnchorPoint.x = previousAnchorPoint.x; + lastAnchorPoint.y = this.mousePosition.y; + } + } else { + lastAnchorPoint.x = this.mousePosition.x; + lastAnchorPoint.y = this.mousePosition.y; + } + + } + }; + + onClickPin(pin: Pin) { + if (this.placingWire != null) { + if (!pin.isInput) { + this.placingWire.outputPin = pin; + this.placingWire.anchorPoints.pop(); + + this.placingWire.inputPin.setOutputWire(this.placingWire); + this.placingWire.inputPin.update(); + + this.placingWire = null; + this.isPlacing = false; + } + return; + } + + const inputConnection = pin.isInput ? pin : null; + const outputConnection = pin.isInput ? null : pin; + const anchorPoint = this.mousePosition.clone; + + this.placingWire = new Wire("blue", inputConnection, outputConnection, [anchorPoint]); + this.wires.push(this.placingWire); + } + + onClick = (event: MouseEvent) => { + event.preventDefault(); + this.setMousePosition(event); + + if (event.button === 2) { + if (this.placingWire != null) { + this.placingWire = null; + this.isPlacing = false; + this.wires.pop(); + return; + } + } else if (event.button === 0) { + let eventComplete = false; + + this.inputPins.forEach((pin: Pin) => { + if (this.mousePosition.getDistance(pin.position.x - INPUT_OUTPUT.pinOffset, pin.position.y) <= INPUT_OUTPUT.radius) { + pin.setState(State.invert(pin.state)); + eventComplete = true; + } else if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) { + this.onClickPin(pin); + eventComplete = true; + } + }); + + if (eventComplete) + return; + + this.outputPins.forEach((pin: Pin) => { + if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) { + this.onClickPin(pin); + eventComplete = true; + } + }); + + if (eventComplete) + return; + + if (this.placingWire != null) { + this.placingWire.anchorPoints.push(this.mousePosition.clone); + } + } + }; + + onKeyDown = (event: KeyboardEvent) => { + if (event.key === "Shift") { + event.preventDefault(); + this.snapping = true; + this.onMouseMove(); + return; + } + }; + + onKeyUp = (event: KeyboardEvent) => { + if (event.key === "Shift") { + event.preventDefault(); + this.snapping = false; + this.onMouseMove(); + return; + } + }; + + getColor(key: string) { + if (this.colors[key] != null) + return this.colors[key]; + + const color = getComputedStyle(this.canvas).getPropertyValue("--" + key); + + this.colors[key] = color; + return color; + } + + 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(); + } + + drawLine(style: string, positions: Vector2[], width: number) { + if (positions.length < 2) + return; + + this.context.lineWidth = width; + this.context.lineJoin = "round"; + this.context.lineCap = "round"; + + this.context.beginPath(); + this.context.moveTo(positions[0].x, positions[0].y); + + for (let i = 1; i < positions.length; i++) { + this.context.lineTo(positions[i].x, positions[i].y); + } + + this.context.strokeStyle = style; + this.context.stroke(); + } + + drawBackground() { + let offset = 0; + this.drawRect(this.getColor(COLORS.background.outer), offset, offset, this.size.x, this.size.y); + + offset = BACKGROUND.padding - BACKGROUND.borderWidth; + this.drawRect( + this.getColor(COLORS.background.border), + offset, offset, + this.size.x - offset * 2, this.size.y - offset * 2 + ); + + offset = BACKGROUND.padding; + this.drawRect( + this.getColor(COLORS.background.inner), + offset, offset, + this.size.x - offset * 2, this.size.y - offset * 2 + ); + } + + drawWires() { + this.wires.forEach((wire, index) => { + const positions = [...wire.anchorPoints]; + + if (wire.inputPin != null) + positions.unshift(wire.inputPin.position); + if (wire.outputPin != null) + positions.push(wire.outputPin.position); + + let color: string; + + const isPlacingWire = this.placingWire != null && index == this.wires.length - 1; + if (isPlacingWire) { + color = COLORS.wire.placing; + } else if (wire.state.value === 1) { + color = `${wire.color}-a`; + } else { + color = `${wire.color}-b`; + } + + this.drawLine(this.getColor(color), positions, WIRE.width); + }); + } + + drawInputOutput(state: State, isInteractable: boolean, positionX: number, positionY: number) { + let color: string; + + if (isInteractable && this.mousePosition.getDistance(positionX, positionY) <= INPUT_OUTPUT.radius) { + if (state.value === 1) { + color = COLORS.inputOutput.onHover; + } else { + color = COLORS.inputOutput.offHover; + } + this.cursor = CURSORS.pointer; + } else { + if (state.value === 1) { + color = COLORS.inputOutput.on; + } else { + color = COLORS.inputOutput.off; + } + } + + this.drawCircle( + this.getColor(COLORS.inputOutput.stroke), + positionX, positionY, + INPUT_OUTPUT.radius + ); + this.drawCircle( + this.getColor(color), + positionX, positionY, + INPUT_OUTPUT.radius - INPUT_OUTPUT.borderWidth + ); + } + + drawPins() { + this.inputPins.forEach((pin) => { + const positionX = pin.position.x - INPUT_OUTPUT.pinOffset; + const positionY = pin.position.y; + + this.drawRect( + this.getColor(COLORS.inputOutput.connector), + positionX, positionY - INPUT_OUTPUT.connectorWidth / 2, + INPUT_OUTPUT.pinOffset, INPUT_OUTPUT.connectorWidth + ); + + this.drawInputOutput(pin.state, true, positionX, positionY); + }); + this.outputPins.forEach((pin) => { + const positionX = pin.position.x + INPUT_OUTPUT.pinOffset; + const positionY = pin.position.y; + + this.drawRect( + this.getColor(COLORS.inputOutput.connector), + positionX - INPUT_OUTPUT.pinOffset, positionY - INPUT_OUTPUT.connectorWidth / 2, + INPUT_OUTPUT.pinOffset, INPUT_OUTPUT.connectorWidth + ); + + this.drawInputOutput(pin.state, false, positionX, pin.position.y); + }); + + super.drawPins(); + } + + draw() { + this.drawBackground(); + this.drawWires(); + this.drawPins(); + } + + 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.isPlacing) { + this.canvas.style.cursor = CURSORS.default; + } else { + this.canvas.style.cursor = this.cursor; + } + + window.requestAnimationFrame(() => { + this.render(); + }); + } +} \ No newline at end of file diff --git a/src/features/apps/logic-sim/pin.ts b/src/features/apps/logic-sim/pin.ts new file mode 100644 index 0000000..4f51edf --- /dev/null +++ b/src/features/apps/logic-sim/pin.ts @@ -0,0 +1,56 @@ +import { COLORS, CURSORS, PIN } from "../../../config/apps/logicSim.config"; +import Vector2 from "../../math/vector2"; +import { Chip } from "./chip"; +import { Circuit } from "./circuit"; +import { State } from "./state"; +import { Wire } from "./wire"; + +export class Pin { + name: string; + position: Vector2; + attachedChip: Chip; + index: number; + circuit: Circuit; + + state = State.LOW; + isInput: boolean; + outputWire: Wire; + + constructor(circuit: Circuit, name: string, isInput: boolean, attachedChip: Chip, index: number) { + Object.assign(this, { circuit, name, isInput, attachedChip, index }); + } + + setOutputWire(wire: Wire) { + this.outputWire = wire; + this.outputWire.setState(this.state); + } + + setState(state: State) { + if (this.state.isEqual(state)) + return; + + this.state = state; + this.update(); + } + + update() { + if (this.isInput) { + this.outputWire?.setState(this.state); + } + } + + draw() { + let color = COLORS.pin.fill; + + if (this.circuit.mousePosition.getDistance(this.position.x, this.position.y) <= PIN.radius) { + this.circuit.cursor = CURSORS.pointer; + color = COLORS.pin.fillHover; + } + + this.circuit.drawCircle( + this.circuit.getColor(color), + this.position.x, this.position.y, + PIN.radius + ); + } +} \ No newline at end of file diff --git a/src/features/apps/logic-sim/state.ts b/src/features/apps/logic-sim/state.ts new file mode 100644 index 0000000..707130d --- /dev/null +++ b/src/features/apps/logic-sim/state.ts @@ -0,0 +1,18 @@ +export class State { + value: number; + + static LOW = new State(0); + static HIGH = new State(1); + + constructor(value: number) { + this.value = value; + } + + static invert(state: State) { + return new State(1 - state.value); + } + + isEqual(state: State) { + return this.value === state.value; + } +} \ No newline at end of file diff --git a/src/features/apps/logic-sim/wire.ts b/src/features/apps/logic-sim/wire.ts new file mode 100644 index 0000000..0915d8f --- /dev/null +++ b/src/features/apps/logic-sim/wire.ts @@ -0,0 +1,30 @@ +import Vector2 from "../../math/vector2"; +import { Pin } from "./pin"; +import { State } from "./state"; + +export class Wire { + color: string; + state = State.LOW; + inputPin: Pin; + outputPin: Pin; + anchorPoints: Vector2[]; + + constructor(color: string, inputPin?: Pin, outputPin?: Pin, anchorPoints?: Vector2[]) { + Object.assign(this, { color, inputPin, outputPin, anchorPoints }); + } + + setState(state: State) { + if (this.state.isEqual(state)) + return; + + this.state = state; + this.update(); + } + + update() { + if (this.outputPin == null) + return; + + this.outputPin.setState(this.state); + } +} \ No newline at end of file diff --git a/src/features/math/vector2.ts b/src/features/math/vector2.ts index 2b896cd..42b7927 100644 --- a/src/features/math/vector2.ts +++ b/src/features/math/vector2.ts @@ -2,9 +2,15 @@ export default class Vector2 { x: number; y: number; - static ZERO = new Vector2(0, 0); + static get ZERO() { + return new Vector2(0, 0); + }; - constructor(x: number, y: number) { + get clone() { + return new Vector2(this.x, this.y); + } + + constructor(x: number, y?: number) { this.x = x; this.y = y ?? x; } @@ -14,4 +20,22 @@ export default class Vector2 { this.y = Math.round(this.y); return this; } + + getDistance(x: number, y?: number): number; + getDistance(vector2: Vector2): number; + + getDistance(x: unknown, y?: unknown): number { + let deltaX = 0, deltaY = 0; + + if (x instanceof Vector2) { + const vector2 = x; + deltaX = this.x - vector2.x; + deltaY = this.y - vector2.y; + } else { + deltaX = this.x - (x as number); + deltaY = this.y - (y as number); + } + + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); + } } \ No newline at end of file diff --git a/src/features/modals/modalsManager.ts b/src/features/modals/modalsManager.ts index fec130a..344fb87 100644 --- a/src/features/modals/modalsManager.ts +++ b/src/features/modals/modalsManager.ts @@ -67,6 +67,6 @@ export default class ModalsManager { } static getModalIconUrl(name: string): string { - return `${process.env.PUBLIC_URL}/assets/modals/icons/${name}.svg`; + return `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/modals/icons/${name}.svg`; } } \ No newline at end of file diff --git a/src/features/virtual-drive/root/defaultData.ts b/src/features/virtual-drive/root/defaultData.ts index cc34a5d..91e7372 100644 --- a/src/features/virtual-drive/root/defaultData.ts +++ b/src/features/virtual-drive/root/defaultData.ts @@ -110,7 +110,7 @@ export function loadDefaultData(virtualRoot: VirtualRoot) { folder.createFolders(["outfit", "roboto-mono"]); }).createFolder("screenshots", (folder) => { folder.createFile("screenshot", "png", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/assets/screenshots/screenshot-files-settings-taskbar-desktop.png`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/screenshots/screenshot-files-settings-taskbar-desktop.png`); }); }).createFolder("wallpapers", (folder) => { folder.setProtected(true); @@ -122,34 +122,34 @@ export function loadDefaultData(virtualRoot: VirtualRoot) { }); } }).createFile("banner", "png", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/assets/banner-logo-title.png`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/banner-logo-title.png`); }).createFile("logo", "svg", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/assets/logo.svg`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/logo.svg`); }); }).createFolder("config", (folder) => { folder.createFile("apps", "xml", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/config/apps.xml`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/config/apps.xml`); }).createFile("desktop", "xml", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/config/desktop.xml`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/config/desktop.xml`); }).createFile("taskbar", "xml", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/config/taskbar.xml`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/config/taskbar.xml`); }).createFile("theme", "xml", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/config/theme.xml`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/config/theme.xml`); }); }).createFolder("documents", (folder) => { folder.createFile("info", "md", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/documents/info.md`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/documents/info.md`); }).createFile("links", "md", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/documents/links.md`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/documents/links.md`); }); }).createFile("favicon", "ico", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/favicon.ico`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/favicon.ico`); }).createFile("index", "html", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/index.html`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/index.html`); }).createFile("robots", "txt", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/robots.txt`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/robots.txt`); }).createFile("sitemap", "xml", (file) => { - file.setSource(`${process.env.PUBLIC_URL}/sitemap.xml`); + file.setSource(`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/sitemap.xml`); }); }); diff --git a/src/styles/global/variables.css b/src/styles/global/variables.css index 8b7a92e..d93e8e0 100644 --- a/src/styles/global/variables.css +++ b/src/styles/global/variables.css @@ -26,6 +26,7 @@ --dark-grey-ca: hsl(212, 27%, 12%); --dark-grey-d: hsl(212, 14%, 10%); --dark-grey-e: hsl(212, 12%, 8%); + --dark-grey-f: hsl(212, 10%, 6%); --foreground-color-a: var(--white-a); --foreground-color-b: var(--grey-a); diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts new file mode 100644 index 0000000..0599c8f --- /dev/null +++ b/src/types/environment.d.ts @@ -0,0 +1,6 @@ +declare namespace NodeJS { + interface ProcessEnv { + NODE_ENV: "development" | "production" | "test"; + PUBLIC_URL: string; + } +} \ No newline at end of file diff --git a/src/globals.d.ts b/src/types/globals.d.ts similarity index 100% rename from src/globals.d.ts rename to src/types/globals.d.ts