Added logic sim app
This commit is contained in:
parent
35c06deb3c
commit
a9a7c78128
22 changed files with 684 additions and 28 deletions
4
src/components/apps/logic-sim/CircuitView.module.css
Normal file
4
src/components/apps/logic-sim/CircuitView.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.Canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
21
src/components/apps/logic-sim/CircuitView.tsx
Normal file
21
src/components/apps/logic-sim/CircuitView.tsx
Normal file
|
|
@ -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 <canvas ref={canvasRef} className={styles.Canvas}/>;
|
||||
}
|
||||
4
src/components/apps/logic-sim/LogicSim.module.css
Normal file
4
src/components/apps/logic-sim/LogicSim.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.Container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
8
src/components/apps/logic-sim/LogicSim.tsx
Normal file
8
src/components/apps/logic-sim/LogicSim.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { CircuitView } from "./CircuitView";
|
||||
import styles from "./LogicSim.module.css";
|
||||
|
||||
export function LogicSim() {
|
||||
return <div className={styles.Container}>
|
||||
<CircuitView/>
|
||||
</div>;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ export const Taskbar = memo(() => {
|
|||
className={`${styles["Menu-button"]} ${styles["Home-button"]}`}
|
||||
onClick={() => { updateShowHome(!showHome); }}
|
||||
>
|
||||
<ReactSVG src={process.env.PUBLIC_URL + "/assets/logo.svg"}/>
|
||||
<ReactSVG src={(process.env as NodeJS.ProcessEnv).PUBLIC_URL + "/assets/logo.svg"}/>
|
||||
</button>
|
||||
<HomeMenu active={showHome} setActive={updateShowHome} search={search}/>
|
||||
</OutsideClickListener>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export const AppButton: FC<AppButtonProps> = memo(({ app, windowsManager, pins,
|
|||
}}
|
||||
title={app.name}
|
||||
>
|
||||
<ReactSVG src={`${process.env.PUBLIC_URL}/assets/apps/icons/${app.id}.svg`}/>
|
||||
<ReactSVG src={`${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${app.id}.svg`}/>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
};
|
||||
47
src/config/apps/logicSim.config.ts
Normal file
47
src/config/apps/logicSim.config.ts
Normal file
|
|
@ -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",
|
||||
},
|
||||
};
|
||||
|
|
@ -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`;
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/features/apps/logic-sim/chip.ts
Normal file
65
src/features/apps/logic-sim/chip.ts
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
370
src/features/apps/logic-sim/circuit.ts
Normal file
370
src/features/apps/logic-sim/circuit.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
56
src/features/apps/logic-sim/pin.ts
Normal file
56
src/features/apps/logic-sim/pin.ts
Normal file
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
18
src/features/apps/logic-sim/state.ts
Normal file
18
src/features/apps/logic-sim/state.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
30
src/features/apps/logic-sim/wire.ts
Normal file
30
src/features/apps/logic-sim/wire.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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`;
|
||||
}
|
||||
}
|
||||
|
|
@ -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`);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
6
src/types/environment.d.ts
vendored
Normal file
6
src/types/environment.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV: "development" | "production" | "test";
|
||||
PUBLIC_URL: string;
|
||||
}
|
||||
}
|
||||
0
src/globals.d.ts → src/types/globals.d.ts
vendored
0
src/globals.d.ts → src/types/globals.d.ts
vendored
Loading…
Reference in a new issue