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 }) {
|
export function MarkdownImage({ currentFile, alt, src, modalsManager, windowsManager, app, ...props }) {
|
||||||
const source = useMemo(() => {
|
const source = useMemo(() => {
|
||||||
if (src.startsWith("public")) {
|
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;
|
return src;
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ export const Taskbar = memo(() => {
|
||||||
className={`${styles["Menu-button"]} ${styles["Home-button"]}`}
|
className={`${styles["Menu-button"]} ${styles["Home-button"]}`}
|
||||||
onClick={() => { updateShowHome(!showHome); }}
|
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>
|
</button>
|
||||||
<HomeMenu active={showHome} setActive={updateShowHome} search={search}/>
|
<HomeMenu active={showHome} setActive={updateShowHome} search={search}/>
|
||||||
</OutsideClickListener>
|
</OutsideClickListener>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ export const AppButton: FC<AppButtonProps> = memo(({ app, windowsManager, pins,
|
||||||
}}
|
}}
|
||||||
title={app.name}
|
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>
|
</button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
@ -25,7 +25,7 @@ export const WindowsView: FC = memo(() => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const resetViewportTitleAndIcon = () => {
|
const resetViewportTitleAndIcon = () => {
|
||||||
setViewportTitle(`${NAME} | ${TAG_LINE}`);
|
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)
|
if (sortedWindows.length === 0 || sortedWindows[sortedWindows.length - 1].minimized)
|
||||||
|
|
|
||||||
|
|
@ -19,10 +19,10 @@ export const APP_NAMES = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const APP_ICONS = {
|
export const APP_ICONS = {
|
||||||
TERMINAL: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.TERMINAL}.svg`,
|
TERMINAL: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.TERMINAL}.svg`,
|
||||||
SETTINGS: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.SETTINGS}.svg`,
|
SETTINGS: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.SETTINGS}.svg`,
|
||||||
MEDIA_VIEWER: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.MEDIA_VIEWER}.svg`,
|
MEDIA_VIEWER: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.MEDIA_VIEWER}.svg`,
|
||||||
TEXT_EDITOR: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.TEXT_EDITOR}.svg`,
|
TEXT_EDITOR: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.TEXT_EDITOR}.svg`,
|
||||||
FILE_EXPLORER: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.FILE_EXPLORER}.svg`,
|
FILE_EXPLORER: `${(process.env as NodeJS.ProcessEnv).PUBLIC_URL}/assets/apps/icons/${APPS.FILE_EXPLORER}.svg`,
|
||||||
CALCULATOR: `${process.env.PUBLIC_URL}/assets/apps/icons/${APPS.CALCULATOR}.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 { APPS, APP_NAMES } from "../../config/apps.config";
|
||||||
import { Browser } from "../../components/apps/browser/Browser";
|
import { Browser } from "../../components/apps/browser/Browser";
|
||||||
import { IMAGE_FORMATS } from "../../config/apps/mediaViewer.config";
|
import { IMAGE_FORMATS } from "../../config/apps/mediaViewer.config";
|
||||||
|
import { LogicSim } from "../../components/apps/logic-sim/LogicSim";
|
||||||
|
|
||||||
export default class AppsManager {
|
export default class AppsManager {
|
||||||
static APPS: App[] = [
|
static APPS: App[] = [
|
||||||
|
|
@ -35,6 +36,7 @@ export default class AppsManager {
|
||||||
new App(APP_NAMES.BROWSER, APPS.BROWSER, Browser, {
|
new App(APP_NAMES.BROWSER, APPS.BROWSER, Browser, {
|
||||||
size: new Vector2(700, 500)
|
size: new Vector2(700, 500)
|
||||||
}),
|
}),
|
||||||
|
new App("Logic Sim", "logic-sim", LogicSim),
|
||||||
];
|
];
|
||||||
|
|
||||||
static getAppById(id: string): App | null {
|
static getAppById(id: string): App | null {
|
||||||
|
|
@ -73,9 +75,9 @@ export default class AppsManager {
|
||||||
*/
|
*/
|
||||||
static getAppIconUrl(appId: string, iconName?: string): string {
|
static getAppIconUrl(appId: string, iconName?: string): string {
|
||||||
if (iconName == null) {
|
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 {
|
} 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;
|
x: number;
|
||||||
y: 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.x = x;
|
||||||
this.y = y ?? x;
|
this.y = y ?? x;
|
||||||
}
|
}
|
||||||
|
|
@ -14,4 +20,22 @@ export default class Vector2 {
|
||||||
this.y = Math.round(this.y);
|
this.y = Math.round(this.y);
|
||||||
return this;
|
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 {
|
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"]);
|
folder.createFolders(["outfit", "roboto-mono"]);
|
||||||
}).createFolder("screenshots", (folder) => {
|
}).createFolder("screenshots", (folder) => {
|
||||||
folder.createFile("screenshot", "png", (file) => {
|
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) => {
|
}).createFolder("wallpapers", (folder) => {
|
||||||
folder.setProtected(true);
|
folder.setProtected(true);
|
||||||
|
|
@ -122,34 +122,34 @@ export function loadDefaultData(virtualRoot: VirtualRoot) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).createFile("banner", "png", (file) => {
|
}).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) => {
|
}).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) => {
|
}).createFolder("config", (folder) => {
|
||||||
folder.createFile("apps", "xml", (file) => {
|
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) => {
|
}).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) => {
|
}).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) => {
|
}).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) => {
|
}).createFolder("documents", (folder) => {
|
||||||
folder.createFile("info", "md", (file) => {
|
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) => {
|
}).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) => {
|
}).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) => {
|
}).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) => {
|
}).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) => {
|
}).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-ca: hsl(212, 27%, 12%);
|
||||||
--dark-grey-d: hsl(212, 14%, 10%);
|
--dark-grey-d: hsl(212, 14%, 10%);
|
||||||
--dark-grey-e: hsl(212, 12%, 8%);
|
--dark-grey-e: hsl(212, 12%, 8%);
|
||||||
|
--dark-grey-f: hsl(212, 10%, 6%);
|
||||||
|
|
||||||
--foreground-color-a: var(--white-a);
|
--foreground-color-a: var(--white-a);
|
||||||
--foreground-color-b: var(--grey-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