Updated logic sim

This commit is contained in:
Prozilla 2024-06-06 22:19:29 +02:00
parent 55e66c391a
commit a369a20701
No known key found for this signature in database
GPG key ID: 5858DFE71CAF31EE
48 changed files with 1680 additions and 701 deletions

View file

@ -9,13 +9,16 @@
<link rel="canonical" href="https://os.prozilla.dev/">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" sizes="any" href="/favicon.ico?v=3">
<link rel="shortcut icon" type="image/svg+xml" href="/icon.svg?v=2"/>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=3">
<link rel="mask-icon" href="/icon.svg?v=2" color="#36b29e"/>
<meta name="msapplication-TileColor" content="#0d1114"/>
<meta name="theme-color" content="#0d1114"/>
<link rel="manifest" href="/manifest.json?v=3">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=4">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=4">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4">
<link rel="manifest" href="/site.webmanifest?v=4">
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=4" color="#4cdfff">
<link rel="shortcut icon" href="/favicon.ico?v=4">
<meta name="apple-mobile-web-app-title" content="ProzillaOS">
<meta name="application-name" content="ProzillaOS">
<meta name="msapplication-TileColor" content="#0d1114">
<meta name="theme-color" content="#0d1114">
<!-- Open Graph -->
<meta property="og:title" content="ProzillaOS | Web-based Operating System"/>
@ -40,7 +43,7 @@
<!-- PWA -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-status-bar-style" content="translucent-black">
<!-- FAQ -->
<script type="application/ld+json">

39
package-lock.json generated
View file

@ -22,6 +22,7 @@
"react-draggable": "^4.4.5",
"react-error-boundary": "^4.0.13",
"react-ga4": "^2.1.0",
"react-router-dom": "^6.23.1",
"react-svg": "^16.1.18",
"react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.2",
@ -1126,6 +1127,14 @@
"node": ">= 8"
}
},
"node_modules/@remix-run/router": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.16.1.tgz",
"integrity": "sha512-es2g3dq6Nb07iFxGk5GuHN20RwBZOsuDQN7izWIisUcv9r+d2C5jQxqmgkdebXgReWfiyUabcki6Fg77mSNrig==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@rollup/pluginutils": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
@ -5185,6 +5194,36 @@
"resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz",
"integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ=="
},
"node_modules/react-router": {
"version": "6.23.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.23.1.tgz",
"integrity": "sha512-fzcOaRF69uvqbbM7OhvQyBTFDVrrGlsFdS3AL+1KfIBtGETibHzi3FkoTRyiDJnWNc2VxrfvR+657ROHjaNjqQ==",
"dependencies": {
"@remix-run/router": "1.16.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8"
}
},
"node_modules/react-router-dom": {
"version": "6.23.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.23.1.tgz",
"integrity": "sha512-utP+K+aSTtEdbWpC+4gxhdlPFwuEfDKq8ZrPFU65bbRJY+l706qjR7yaidBpo3MSeA/fzwbXWbKBI6ftOnP3OQ==",
"dependencies": {
"@remix-run/router": "1.16.1",
"react-router": "6.23.1"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"react": ">=16.8",
"react-dom": ">=16.8"
}
},
"node_modules/react-svg": {
"version": "16.1.18",
"resolved": "https://registry.npmjs.org/react-svg/-/react-svg-16.1.18.tgz",

View file

@ -28,6 +28,7 @@
"react-draggable": "^4.4.5",
"react-error-boundary": "^4.0.13",
"react-ga4": "^2.1.0",
"react-router-dom": "^6.23.1",
"react-svg": "^16.1.18",
"react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

9
public/browserconfig.xml Normal file
View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png?v=4"/>
<TileColor>#0d1114</TileColor>
</tile>
</msapplication>
</browserconfig>

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 56 KiB

BIN
public/mstile-150x150.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,44 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2780 4953 c-19 -8 -47 -31 -61 -51 -24 -35 -239 -404 -362 -622 -8
-14 -38 -65 -67 -115 -28 -49 -62 -108 -75 -130 -12 -22 -44 -78 -71 -124 -39
-68 -48 -94 -48 -132 0 -38 -4 -49 -21 -57 -17 -8 -24 -4 -44 29 -29 46 -48
54 -251 105 -8 2 -51 13 -95 25 -44 11 -87 23 -95 24 -8 2 -97 27 -197 54
-100 28 -192 53 -205 55 -13 3 -70 18 -128 34 -58 16 -116 27 -129 25 -63 -9
-111 -59 -112 -116 -1 -28 56 -268 81 -337 4 -14 23 -81 40 -150 144 -558 161
-605 230 -626 25 -8 27 -11 16 -27 -12 -15 -16 -16 -41 -3 -38 20 -72 13 -167
-33 -46 -21 -117 -55 -158 -74 -68 -31 -114 -53 -290 -134 -281 -131 -290
-137 -290 -198 1 -51 22 -74 161 -171 74 -51 143 -100 154 -108 27 -19 327
-230 389 -272 26 -19 60 -34 75 -34 15 0 28 -6 28 -12 0 -7 4 -30 8 -50 6 -34
5 -36 -14 -31 -32 8 -76 -15 -95 -50 -9 -18 -24 -44 -34 -59 -45 -72 -198
-315 -207 -330 -5 -9 -36 -60 -68 -112 -50 -81 -58 -99 -52 -126 11 -51 34
-58 200 -64 572 -21 575 -21 599 -11 11 5 21 3 26 -5 20 -32 82 -102 155 -175
44 -43 79 -83 77 -88 -1 -5 2 -6 8 -2 6 3 24 -7 41 -24 17 -17 34 -31 39 -31
5 0 13 -6 17 -14 4 -7 18 -18 31 -25 12 -7 20 -17 16 -23 -4 -7 -2 -8 5 -4 7
4 26 -3 44 -16 60 -45 243 -143 298 -160 22 -6 39 -15 39 -20 0 -4 3 -7 8 -6
10 3 85 -23 100 -35 7 -5 15 -7 19 -4 3 4 13 2 23 -3 23 -12 192 -50 250 -57
25 -2 72 -8 105 -13 62 -8 332 -5 395 5 19 3 67 10 105 16 92 15 96 16 125 27
14 5 27 10 30 10 35 0 349 122 380 148 8 7 22 14 30 16 8 2 17 5 20 8 4 5 37
28 207 141 13 9 32 25 42 35 11 11 37 35 60 54 49 42 131 124 161 163 12 15
26 24 32 21 7 -4 8 -2 4 4 -4 7 10 33 32 60 50 59 136 194 184 287 33 64 105
239 114 275 2 8 11 42 20 75 64 234 80 469 48 715 -4 36 -8 77 -9 93 0 15 -3
27 -6 27 -5 0 -33 121 -34 145 0 6 -2 12 -5 15 -3 3 -18 43 -34 90 -16 47 -46
123 -67 168 -22 46 -36 89 -32 95 3 5 2 7 -4 4 -13 -8 -46 50 -37 66 4 6 4 9
-1 5 -4 -4 -34 31 -66 77 -32 47 -71 102 -88 122 -16 20 -28 40 -25 45 3 5 2
7 -3 6 -4 -1 -55 45 -113 103 -100 99 -259 229 -280 229 -6 0 -10 5 -10 10 0
6 -6 10 -13 10 -7 0 -22 8 -33 18 -10 10 -34 24 -51 31 -18 7 -33 18 -33 24 0
6 -3 7 -7 4 -3 -4 -13 -1 -22 7 -9 8 -20 15 -24 15 -4 0 -27 9 -50 20 -39 19
-42 23 -41 58 1 37 -12 68 -82 184 -19 31 -34 62 -34 68 0 6 -4 11 -10 11 -5
0 -10 4 -10 9 0 4 -37 71 -81 147 -45 76 -95 164 -111 194 -17 30 -34 60 -38
65 -4 6 -17 26 -27 45 -78 138 -234 402 -245 413 -40 40 -111 53 -168 30z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,19 +1,19 @@
{
"name": "ProzillaOS",
"short_name": "ProzillaOS",
"icons": [
"icons": [
{
"src": "/favicon-192.png",
"src": "/android-chrome-192x192.png?v=4",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicon-512.png",
"src": "/android-chrome-512x512.png?v=4",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#0d1114",
"background_color": "#0d1114",
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View file

@ -20,7 +20,7 @@
<image:loc>https://os.prozilla.dev/assets/wallpapers/colorful-abstract-wallpaper-blue-red-green.png</image:loc>
</image:image>
<image:image>
<image:loc>https://os.prozilla.dev/assets/wallpapers/colorful-mesh-gradient-yellow-blue-green.png</image:loc>
<image:loc>https://os.prozilla.dev/assets/wallpapers/colorful-mesh-gradient-red-green.png</image:loc>
</image:image>
<image:image>
<image:loc>https://os.prozilla.dev/assets/wallpapers/mesh-gradient-wallpaper-red-purple.png</image:loc>

View file

@ -152,6 +152,7 @@
}
.Header-menu .Dropdown {
position: relative;
display: block;
width: auto;
height: 100%;

View file

@ -1,10 +1,12 @@
.CircuitView {
display: flex;
width: 100%;
height: calc(100% - var(--header-height));
flex-grow: 1;
}
.Canvas {
flex: 1;
width: 100%;
height: 100%;
}

View file

@ -1,12 +1,20 @@
import { useEffect, useRef, useState } from "react";
import { Circuit } from "../../../features/apps/logic-sim/circuit";
import { Circuit } from "../../../features/apps/logic-sim/core/circuit";
import styles from "./CircuitView.module.css";
import { DropdownAction } from "../../actions/actions/DropdownAction";
import { HeaderMenu } from "../_utils/header-menu/HeaderMenu";
import { ClickAction } from "../../actions/actions/ClickAction";
import { ChipsManager } from "../../../features/apps/logic-sim/chipsManager";
import { ChipsManager } from "../../../features/apps/logic-sim/chips/chipsManager";
import App from "../../../features/apps/app";
import { useAppFolder } from "../../../hooks/apps/appFolder";
import { openUrl } from "../../../features/_utils/browser.utils";
export function CircuitView() {
interface CircuitViewProps {
app: App;
}
export function CircuitView({ app }: CircuitViewProps) {
const virtualFolder = useAppFolder(app);
const [circuit] = useState(new Circuit("Chip", "#000", 2, 1));
const canvasRef = useRef(null);
@ -23,9 +31,27 @@ export function CircuitView() {
return <>
<HeaderMenu>
<DropdownAction label="Circuit" showOnHover={false}>
<ClickAction label="New" onTrigger={() => { circuit.reset(); }}/>
<ClickAction label="Save" onTrigger={() => {
ChipsManager.saveCircuit(circuit, virtualFolder);
}}/>
<ClickAction label="Load" onTrigger={() => {
ChipsManager.loadCircuit(circuit, virtualFolder);
}}/>
</DropdownAction>
<DropdownAction label="Add" showOnHover={false}>
<ClickAction label="AND gate" onTrigger={() => { circuit.addChip(ChipsManager.CHIPS.AND); }}/>
<ClickAction label="NOT gate" onTrigger={() => { circuit.addChip(ChipsManager.CHIPS.NOT); }}/>
<ClickAction label="AND gate" onTrigger={() => {
circuit.inputHandler.startChipPlacement(ChipsManager.CHIPS.AND);
}}/>
<ClickAction label="NOT gate" onTrigger={() => {
circuit.inputHandler.startChipPlacement(ChipsManager.CHIPS.NOT);
}}/>
</DropdownAction>
<DropdownAction label="Help" showOnHover={false}>
<ClickAction label="Digital Electronics Glossary" onTrigger={() => {
openUrl("http://www.pmcgibbon.net/teachcte/electron/degloss1.htm");
}}/>
</DropdownAction>
</HeaderMenu>
<div className={styles.CircuitView}>

View file

@ -3,4 +3,12 @@
flex-direction: column;
width: 100%;
height: 100%;
}
.LogicSim {
--black-0: hsl(210, 15%, 55%);
--black-1: hsl(210, 15%, 40%);
--black-2: hsl(210, 15%, 30%);
--black-3: hsl(210, 15%, 20%);
--black-4: hsl(210, 15%, 12.5%);
}

View file

@ -1,8 +1,9 @@
import { WindowProps } from "../../windows/WindowView";
import { CircuitView } from "./CircuitView";
import styles from "./LogicSim.module.css";
export function LogicSim() {
export function LogicSim({ app }: WindowProps) {
return <div className={styles.LogicSim}>
<CircuitView/>
<CircuitView app={app}/>
</div>;
}

View file

@ -1,3 +1,11 @@
.SettingsContainer {
width: 100%;
height: 100%;
position: sticky;
top: 0;
left: 0;
}
.Settings {
--tabs-width: 10rem;

View file

@ -14,7 +14,7 @@ interface SettingsProps extends WindowProps {
}
export function Settings({ tab }: SettingsProps) {
return (
return <div className={styles.SettingsContainer}>
<Tabs
defaultIndex={tab ?? 0}
className={styles.Settings}
@ -52,5 +52,5 @@ export function Settings({ tab }: SettingsProps) {
<AboutSettings/>
</TabPanel>
</Tabs>
);
</div>;
}

View file

@ -16,7 +16,7 @@
align-items: center;
padding: 0.25rem;
margin-top: auto;
z-index: -1;
z-index: 1;
}
.Buttons > button {

View file

@ -6,6 +6,7 @@ export const APPS = {
FILE_EXPLORER: "file-explorer",
CALCULATOR: "calculator",
BROWSER: "browser",
LOGIC_SIM: "logic-sim",
};
export const APP_NAMES = {

View file

@ -1,59 +1,79 @@
import { APPS } from "../apps.config";
export const CURSORS = {
default: "default",
pointer: "pointer"
};
export const FONT = "outfit";
export const ENABLE_COLOR_CACHING = true;
export const VIRTUAL_PATH = `~/Apps/${APPS.LOGIC_SIM}/`;
export const BACKGROUND = {
padding: 40,
borderWidth: 8,
padding: 30,
borderWidth: 7.5,
};
export const INPUT_OUTPUT = {
radius: 24,
borderWidth: 4,
pinOffset: 40,
connectorWidth: 10,
export const CONTROLLER = {
radius: 25,
borderWidth: 5,
pinOffset: 42.5,
connectorWidth: 7.5,
handleWidth: 15,
handleTrackWidth: 22.5,
placingOpacity: 0.5,
};
export const WIRE = {
width: 4,
width: 5,
snappingSensitivity: 10,
cornerRadius: 25,
resolution: 8,
};
export const PIN = {
radius: 8,
radius: 10,
label: {
offset: 10,
fontSize: 15,
padding: 5,
}
};
export const CHIP = {
width: 140,
height: 72,
padding: 12,
fontSize: 36,
BorderWidth: 5,
padding: 10,
fontSize: 35,
placingOutline: 10,
};
export const COLORS = {
pin: {
fill: "black-4",
fillHover: "black-3"
fillHover: "black-3",
labelText: "white-0",
labelBackground: "black-4"
},
inputOutput: {
controller: {
stroke: "black-4",
connector: "black-3",
connector: "black-4",
on: "red-0",
onHover: "red-0",
off: "red-2",
offHover: "red-1",
hover: "white-0",
handle: "black-3",
handleHover: "black-4",
},
background: {
border: "black-0",
inner: "black-1",
outer: "black-2",
outer: "black-1",
inner: "black-2",
margin: "black-2",
},
wire: {
placing: "black-1",
},
chip: {
text: "black-4"
text: "black-4",
outline: "white-0",
}
};

View file

@ -5,7 +5,7 @@ export const WALLPAPERS = [
"/assets/wallpapers/abstract-wallpaper-mesh-gradient-cyan.png",
"/assets/wallpapers/colorful-abstract-wallpaper-blue-red-green.png",
"/assets/wallpapers/mesh-gradient-wallpaper-red-purple.png",
"/assets/wallpapers/colorful-mesh-gradient-yellow-blue-green.png",
"/assets/wallpapers/colorful-mesh-gradient-red-green.png",
"/assets/wallpapers/flame-abstract-wallpaper-orange.png",
"/assets/wallpapers/wave-abstract-wallpaper-teal.png",
];

View file

@ -47,7 +47,7 @@ export function getViewportParams(): Record<string, string> {
const params = {};
query.split("&").forEach((param) => {
// For some reason, URI components only decode when decoded twice
// Please find a fix, or create a custom function
// TO DO: Please find a fix, or create a custom function
const [key, value] = param.split("=").map((item) => decodeURIComponent(decodeURIComponent(item)));
params[key] = value;
});
@ -83,6 +83,10 @@ export function generateUrl(options: { appId: string; fullscreen: boolean; }) {
return url;
}
export function openUrl(url: string) {
window.open(url, "_blank");
}
export function copyToClipboard(string: string, onSuccess: (value: void) => void, onFail: (value: void) => void) {
navigator.clipboard.writeText(string).then(onSuccess, onFail);
}

View file

@ -36,7 +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),
new App("Logic Sim (WIP)", "logic-sim", LogicSim),
];
static getAppById(id: string): App | null {

View file

@ -1,110 +0,0 @@
import { CHIP, COLORS } from "../../../config/apps/logicSim.config";
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;
size: Vector2;
circuit: Circuit;
isCircuit = true;
inputCount: number;
outputCount: number;
inputPins: Pin[];
outputPins: Pin[];
logic: (inputStates: State[]) => State[];
constructor(circuit: Circuit | null, name: string, color: string, size: Vector2, inputCount: number, outputCount: number) {
Object.assign(this, { circuit, name, color, size, 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));
}
}
setCircuit(circuit: Circuit) {
this.circuit = circuit;
this.inputPins.concat(this.outputPins).forEach((pin) => { pin.circuit = circuit; });
}
setLogic(logic: (inputStates: State[]) => State[]) {
this.logic = logic;
return this;
}
update() {
if (this.logic == null)
return;
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].setState(outputStates[i]);
}
}
drawPins(reposition = true) {
this.inputPins.forEach((pin, index) => {
if (reposition) {
const gap = this.size.y / (this.inputCount + 1);
pin.position.x = this.position.x;
pin.position.y = this.position.y + gap * (index + 1);
}
pin.draw();
});
this.outputPins.forEach((pin, index) => {
if (reposition) {
const gap = this.size.y / (this.outputCount + 1);
pin.position.x = this.position.x + this.size.x;
pin.position.y = this.position.y + gap * (index + 1);
}
pin.draw();
});
}
draw() {
this.circuit.drawRect(
this.circuit.getColor(this.color + "-1"),
this.position.x, this.position.y,
this.size.x, this.size.y
);
this.circuit.drawRect(
this.circuit.getColor(this.color + "-0"),
this.position.x + CHIP.padding, this.position.y + CHIP.padding,
this.size.x - CHIP.padding * 2, this.size.y - CHIP.padding * 2
);
this.circuit.drawText(
this.circuit.getColor(COLORS.chip.text),
"center",
this.position.x + this.size.x / 2, this.position.y + this.size.y / 2,
CHIP.fontSize,
this.name
);
this.drawPins();
}
}

View file

@ -0,0 +1,176 @@
import { CHIP, COLORS, PIN } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2";
import { Circuit } from "../core/circuit";
import { Pin, PinJson } from "../pins/pin";
import { State } from "../_utils/state";
export interface ChipJson {
color: string;
name: string;
position: {
x: number;
y: number;
};
inputPins: PinJson[];
outputPins: PinJson[];
}
export class Chip {
color: string;
name: string;
position = Vector2.ZERO;
size: Vector2;
circuit: Circuit;
isCircuit = false;
isBlueprint = false;
inputCount = 0;
outputCount = 0;
inputPins: Pin[];
outputPins: Pin[];
logic: (inputStates: State[]) => State[];
constructor(circuit: Circuit | null, name: string, color: string, isBlueprint: boolean, inputCount: number, outputCount: number) {
Object.assign(this, { circuit, name, color, isBlueprint, inputCount, outputCount });
if (this.circuit == null && !isBlueprint) {
this.circuit = this as unknown as Circuit;
this.isCircuit = true;
}
if (this.isCircuit || this.isBlueprint)
return;
if (this.circuit != null) {
const textRect = this.circuit.getTextRect(CHIP.fontSize, this.name);
const width = textRect.x + (CHIP.padding + CHIP.BorderWidth) * 2;
const minHeight = textRect.y + (CHIP.padding + CHIP.BorderWidth) * 2;
this.size = new Vector2(width, minHeight);
}
this.inputPins = [];
for (let i = 0; i < inputCount; i++) {
const newPin = new Pin(this.circuit, "IN " + i, true, this);
this.inputPins.push(newPin);
if (this.isCircuit)
newPin.isControlled = true;
}
this.outputPins = [];
for (let i = 0; i < outputCount; i++) {
const newPin = new Pin(this.circuit, "OUT " + i, false, this);
this.outputPins.push(newPin);
if (this.isCircuit)
newPin.isControlled = true;
}
}
setCircuit(circuit: Circuit) {
this.circuit = circuit;
this.inputPins.concat(this.outputPins).forEach((pin) => { pin.circuit = circuit; });
}
setLogic(logic: (inputStates: State[]) => State[]) {
this.logic = logic;
return this;
}
update() {
if (this.logic == null)
return;
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].setState(outputStates[i]);
}
}
drawPins(reposition = true) {
this.inputPins.forEach((pin, index) => {
if (reposition) {
const gap = (this.size.y - this.inputCount * PIN.radius * 2) / (this.inputCount + 1);
pin.position.x = this.position.x;
pin.position.y = this.position.y + gap * (index + 1) + PIN.radius * (2 * index + 1);
}
const isPlacingPin = this.circuit.inputHandler.isPlacingPin(pin, index);
pin.draw(isPlacingPin);
});
this.outputPins.forEach((pin, index) => {
if (reposition) {
const gap = (this.size.y - this.outputCount * PIN.radius * 2) / (this.outputCount + 1);
pin.position.x = this.position.x + this.size.x;
pin.position.y = this.position.y + gap * (index + 1) + PIN.radius * (2 * index + 1);
}
const isPlacingPin = this.circuit.inputHandler.isPlacingPin(pin, index);
pin.draw(isPlacingPin);
});
}
draw(isPlacing: boolean) {
this.circuit.drawRect(
this.circuit.getColor(this.color + "-1"),
this.position.x, this.position.y,
this.size.x, this.size.y
);
this.circuit.drawRect(
this.circuit.getColor(this.color + "-0"),
this.position.x + CHIP.BorderWidth, this.position.y + CHIP.BorderWidth,
this.size.x - CHIP.BorderWidth * 2, this.size.y - CHIP.BorderWidth * 2
);
this.circuit.drawText(
this.circuit.getColor(COLORS.chip.text),
"center",
this.position.x + this.size.x / 2, this.position.y + this.size.y / 2,
CHIP.fontSize,
this.name
);
if (isPlacing) {
this.circuit.setDrawingOpacity(0.25);
this.circuit.drawRect(
this.circuit.getColor(COLORS.chip.outline),
this.position.x - CHIP.placingOutline, this.position.y - CHIP.placingOutline,
this.size.x + CHIP.placingOutline * 2, this.size.y + CHIP.placingOutline * 2
);
this.circuit.resetDrawingOpacity();
}
this.drawPins();
}
toJson(): ChipJson {
const object = {
color: this.color,
name: this.name,
position: {
x: this.position.x,
y: this.position.y
},
} as ChipJson;
if (this.inputPins.length > 0)
object.inputPins = this.inputPins.map((pin) => pin.toJson());
if (this.outputPins.length > 0)
object.outputPins = this.outputPins.map((pin) => pin.toJson());
return object;
}
}

View file

@ -0,0 +1,107 @@
import { Chip } from "./chip";
import { State } from "../_utils/state";
import { Circuit, CircuitJson } from "../core/circuit";
import { VirtualFolder } from "../../../virtual-drive/folder";
import { ControlledPin } from "../pins/controlledPin";
import Vector2 from "../../../math/vector2";
import { Pin } from "../pins/pin";
import { Wire } from "../wires/wire";
export class ChipsManager {
static CHIPS = {
AND: new Chip(null, "AND", "blue", true, 2, 1).setLogic((inputStates: State[]) => {
if (inputStates[0].value === 1 && inputStates[1].value === 1) {
return [State.HIGH];
} else {
return [State.LOW];
}
}),
NOT: new Chip(null, "NOT", "red", true, 1, 1).setLogic((inputStates: State[]) => {
return [State.invert(inputStates[0])];
}),
};
static saveCircuit(circuit: Circuit, virtualFolder: VirtualFolder) {
virtualFolder.createFile(circuit.name, "json", (file) => {
file.setContent(circuit.toString());
});
}
static loadCircuit(circuit: Circuit, virtualFolder: VirtualFolder) {
circuit.reset();
const virtualFile = virtualFolder.findFile(circuit.name, "json");
if (virtualFile == null)
return;
virtualFile.read().then((content) => {
const data = JSON.parse(content) as CircuitJson;
circuit.color = data.color;
circuit.name = data.name;
const pins: { [id: number]: Pin } = {};
// Load input pins
circuit.inputCount = data.inputPins?.length ?? 0;
data.inputPins?.forEach((pinData) => {
const newPin = new ControlledPin(circuit, pinData.name, true, pinData.id);
newPin.position = pinData.position as Vector2;
circuit.inputPins.push(newPin);
pins[pinData.id] = newPin;
});
// Load output pins
circuit.outputCount = data.outputPins?.length ?? 0;
data.outputPins?.forEach((pinData) => {
const newPin = new ControlledPin(circuit, pinData.name, false, pinData.id);
newPin.position = pinData.position as Vector2;
circuit.outputPins.push(newPin);
pins[pinData.id] = newPin;
});
// Load chips
data.chips?.forEach((chipData) => {
const newChip = new Chip(circuit, chipData.name, chipData.color, false, 0, 0);
newChip.position = chipData.position as Vector2;
// Load input pins
newChip.inputCount = chipData.inputPins?.length ?? 0;
chipData.inputPins?.forEach((pinData) => {
const newPin = new Pin(circuit, pinData.name, true, newChip, pinData.id);
newChip.inputPins.push(newPin);
pins[pinData.id] = newPin;
});
// Load output pins
newChip.outputCount = chipData.outputPins?.length ?? 0;
chipData.outputPins?.forEach((pinData) => {
const newPin = new Pin(circuit, pinData.name, false, newChip, pinData.id);
newChip.outputPins.push(newPin);
pins[pinData.id] = newPin;
});
// Load logic
newChip.setLogic((ChipsManager.CHIPS[chipData.name] as Chip).logic);
newChip.update();
circuit.chips.push(newChip);
});
// Load wires
data.wires?.forEach((wireData) => {
const inputPin = pins[wireData.inputId];
const outputPin = pins[wireData.outputId];
const anchorPoints = wireData.anchorPoints as Vector2[] ?? [];
const newWire = new Wire(circuit, wireData.color, inputPin, outputPin, anchorPoints);
inputPin?.addOutputWire(newWire);
circuit.wires.push(newWire);
});
}).catch((error) => {
console.error(error);
});
}
}

View file

@ -1,19 +0,0 @@
import { CHIP } from "../../../config/apps/logicSim.config";
import Vector2 from "../../math/vector2";
import { Chip } from "./chip";
import { State } from "./state";
export class ChipsManager {
static CHIPS = {
AND: new Chip(null, "AND", "cyan", new Vector2(CHIP.width, CHIP.height), 2, 1).setLogic((inputStates: State[]) => {
if (inputStates[0].value === 1 && inputStates[1].value === 1) {
return [State.HIGH];
} else {
return [State.LOW];
}
}),
NOT: new Chip(null, "NOT", "yellow", new Vector2(CHIP.width, CHIP.height), 1, 1).setLogic((inputStates: State[]) => {
return [State.invert(inputStates[0])];
}),
};
}

View file

@ -1,413 +0,0 @@
import { BACKGROUND, COLORS, CURSORS, FONT, INPUT_OUTPUT, PIN } 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;
isCircuit = true;
isPlacing = false;
snapping = false;
placingWire: Wire;
wires: Wire[] = [];
placingChip: Chip;
chips: Chip[] = [];
cursor = CURSORS.default;
constructor(name: string, color: string, inputCount: number, outputCount: number) {
super(null, name, color, null, 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;
}
}
if (this.placingChip != null) {
this.placingChip.position.x = this.mousePosition.x - this.placingChip.size.x / 2;
this.placingChip.position.y = this.mousePosition.y - this.placingChip.size.y / 2;
}
};
onClickPin(pin: Pin, isInputOutput = false) {
const isInputPin = isInputOutput ? !pin.isInput : pin.isInput;
if (this.placingWire != null) {
if (isInputPin) {
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 = isInputPin ? null : pin;
const outputConnection = isInputPin ? pin : null;
const anchorPoint = this.mousePosition.clone;
this.placingWire = new Wire(this, "red", 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;
}
console.log(this);
} 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, true);
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, true);
eventComplete = true;
}
});
if (eventComplete)
return;
this.chips.forEach((chip) => {
chip.inputPins.concat(chip.outputPins).forEach((pin) => {
if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) {
this.onClickPin(pin);
eventComplete = true;
}
});
});
if (eventComplete)
return;
// Create wire anchor point
if (this.placingWire != null) {
this.placingWire.anchorPoints.push(this.mousePosition.clone);
return;
}
// Place chip
if (this.placingChip != null) {
this.placingChip = null;
this.isPlacing = false;
return;
}
}
};
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;
}
};
addChip(chip: Chip) {
const newChip = new Chip(this, chip.name, chip.color, chip.size, chip.inputCount, chip.outputCount);
newChip.setLogic(chip.logic);
newChip.position = new Vector2(
this.mousePosition.x - newChip.size.x / 2,
this.mousePosition.y - newChip.size.y / 2
);
this.placingChip = newChip;
this.isPlacing = true;
this.chips.push(newChip);
}
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();
}
drawText(style: string, align: CanvasTextAlign, positionX: number, positionY: number, size: number, content: string) {
this.context.fillStyle = style;
this.context.textAlign = align;
this.context.textBaseline = "middle";
this.context.font = `bold ${size}px ${FONT}`;
this.context.fillText(content, positionX, positionY);
}
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 isPlacingWire = this.placingWire != null && index == this.wires.length - 1;
wire.draw(isPlacingWire);
});
}
drawChips() {
this.chips.forEach((chip) => { chip.draw(); });
}
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(false);
}
draw() {
this.drawBackground();
this.drawWires();
this.drawChips();
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();
});
}
}

View file

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

View file

@ -0,0 +1,503 @@
import { CONTROLLER, PIN, WIRE } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2";
import { Chip } from "../chips/chip";
import { Circuit } from "./circuit";
import { ControlledPin } from "../pins/controlledPin";
import { Pin } from "../pins/pin";
import { State } from "../_utils/state";
import { Wire } from "../wires/wire";
export class InputHandler {
circuit: Circuit;
canvas: HTMLCanvasElement;
mousePosition = Vector2.ZERO;
isPlacing = false;
snapping = false;
placingOffset = Vector2.ZERO;
previousPlacement: Vector2 | undefined = null;
placingWire: Wire;
placingChip: Chip;
placingPin: ControlledPin;
constructor(circuit: Circuit) {
Object.assign(this, { circuit });
}
setMousePosition(event: MouseEvent) {
const rect = this.canvas.getBoundingClientRect();
this.mousePosition.x = event.clientX - rect.left;
this.mousePosition.y = event.clientY - rect.top;
}
init() {
this.canvas = this.circuit.canvas;
this.mousePosition = Vector2.ZERO;
// TO DO: add support for touch devices
this.canvas.addEventListener("mousemove", this.onMouseMove);
this.canvas.addEventListener("mouseup", this.onMouseUp);
this.canvas.addEventListener("contextmenu", this.onMouseUp);
this.canvas.addEventListener("mousedown", this.onMouseDown);
window.addEventListener("keydown", this.onKeyDown);
window.addEventListener("keyup", this.onKeyUp);
}
cleanup() {
this.canvas.removeEventListener("mousemove", this.onMouseMove);
this.canvas.removeEventListener("mouseup", this.onMouseUp);
this.canvas.removeEventListener("contextmenu", this.onMouseUp);
this.canvas.removeEventListener("mousedown", this.onMouseDown);
window.removeEventListener("keydown", this.onKeyDown);
window.removeEventListener("keyup", this.onKeyUp);
}
reset() {
this.placingWire = null;
this.placingChip = null;
this.placingPin = null;
this.previousPlacement = null;
this.placingOffset = Vector2.ZERO;
this.isPlacing = false;
}
onMouseMove = (event?: MouseEvent) => {
if (event != null)
this.setMousePosition(event);
if (this.placingWire != null) {
this.updateWirePlacement();
return;
}
if (this.placingChip != null) {
this.updateChipPlacement();
return;
}
const isHoveringPinHandle = (pin: ControlledPin) => {
const top = pin.position.y - CONTROLLER.radius;
const bottom = pin.position.y + CONTROLLER.radius;
return (this.mousePosition.y > top && this.mousePosition.y < bottom);
};
if (this.placingPin != null) {
let invalidPlacement = this.mousePosition.x > CONTROLLER.handleTrackWidth && this.mousePosition.x < this.circuit.size.x - CONTROLLER.handleTrackWidth;
if (invalidPlacement) {
this.cancelPinPlacement();
return;
}
if (this.placingPin.isInput) {
this.circuit.inputPins.forEach((pin, index) => {
if (invalidPlacement || index == this.circuit.inputPins.length - 1)
return;
if (isHoveringPinHandle(pin))
invalidPlacement = true;
});
} else {
this.circuit.outputPins.forEach((pin, index) => {
if (invalidPlacement || index == this.circuit.outputPins.length - 1)
return;
if (isHoveringPinHandle(pin))
invalidPlacement = true;
});
}
if (invalidPlacement) {
this.cancelPinPlacement();
} else {
this.updatePinPlacement();
}
} else {
if (this.mousePosition.x < CONTROLLER.handleTrackWidth) {
let invalidPlacement = false;
this.circuit.inputPins.forEach((pin) => {
if (invalidPlacement)
return;
if (isHoveringPinHandle(pin))
invalidPlacement = true;
});
if (!invalidPlacement)
this.startPinPlacement(true);
} else if (this.mousePosition.x > this.circuit.size.x - CONTROLLER.handleTrackWidth) {
let invalidPlacement = false;
this.circuit.outputPins.forEach((pin) => {
if (invalidPlacement)
return;
if (isHoveringPinHandle(pin))
invalidPlacement = true;
});
if (!invalidPlacement)
this.startPinPlacement(false);
}
}
};
onClickPin(pin: Pin) {
if (this.placingWire != null) {
this.endWirePlacement(pin);
} else {
this.startWirePlacement(pin);
}
}
onMouseUp = (event: MouseEvent) => {
event.preventDefault();
this.setMousePosition(event);
if (event.button === 2) {
if (this.placingWire != null)
this.cancelWirePlacement();
if (this.placingChip != null)
this.cancelChipPlacement();
} else if (event.button === 0) {
let eventComplete = false;
this.circuit.inputPins.forEach((pin: Pin) => {
if (this.mousePosition.getDistance(pin.position.x - CONTROLLER.pinOffset, pin.position.y) <= CONTROLLER.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.circuit.outputPins.forEach((pin: Pin) => {
if (this.mousePosition.getDistance(pin.position.x, pin.position.y) <= PIN.radius) {
this.onClickPin(pin);
eventComplete = true;
}
});
if (eventComplete)
return;
this.circuit.chips.forEach((chip) => {
chip.inputPins.concat(chip.outputPins).forEach((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.anchorWirePlacement();
if (this.placingChip != null)
this.endChipPlacement();
if (this.placingPin != null)
this.endPinPlacement();
}
};
onMouseDown = (event: MouseEvent) => {
event.preventDefault();
this.setMousePosition(event);
if (event.button !== 0 || this.isPlacing)
return;
this.circuit.chips.forEach((chip, index) => {
if (!this.isPlacing && this.circuit.isPointInsideRect(chip, this.mousePosition)) {
let hoveringPin = false;
chip.inputPins.concat(chip.outputPins).forEach((pin) => {
if (pin.position.getDistance(this.mousePosition.x, this.mousePosition.y) <= PIN.radius)
hoveringPin = true;
});
if (!hoveringPin)
this.editChipPlacement(chip, index);
}
});
};
onKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case "Shift":
event.preventDefault();
this.snapping = true;
this.onMouseMove();
break;
case "Backspace":
case "Delete":
case "Escape":
event.preventDefault();
if (this.placingWire != null)
this.cancelWirePlacement();
if (this.placingChip != null)
this.cancelChipPlacement();
break;
}
};
onKeyUp = (event: KeyboardEvent) => {
switch (event.key) {
case "Shift":
event.preventDefault();
this.snapping = false;
this.onMouseMove();
break;
}
};
startWirePlacement(pin: Pin) {
const isInputPin = pin.isPointingRight;
// Start wire placement
const inputPin = isInputPin ? pin : null;
const outputPin = !isInputPin ? pin : null;
const anchorPoint = this.mousePosition.clone;
this.placingWire = new Wire(this.circuit, "red", inputPin, outputPin, [anchorPoint]);
if (!isInputPin)
this.placingWire.placedBackwards = true;
this.circuit.wires.push(this.placingWire);
}
snapWireHorizontally(lastAnchorPoint: Vector2, previousAnchorPoint: Vector2) {
lastAnchorPoint.x = this.mousePosition.x;
lastAnchorPoint.y = previousAnchorPoint.y;
// Snapping horizontal wire to other wires
let anchorPoints: Vector2[] = [];
this.circuit.wires.forEach((wire, index) => {
if (index < this.circuit.wires.length - 1)
anchorPoints = anchorPoints.concat(wire.anchorPoints);
});
let closestPositionX: number | undefined;
let closestDistance: number | undefined;
anchorPoints.forEach((point) => {
const distance = Math.abs(this.mousePosition.x - point.x);
if (closestDistance == null || closestDistance > distance) {
closestPositionX = point.x;
closestDistance = distance;
}
});
if (closestPositionX != null && closestDistance < WIRE.snappingSensitivity)
lastAnchorPoint.x = closestPositionX;
}
snapWireVertically(lastAnchorPoint: Vector2, previousAnchorPoint: Vector2) {
lastAnchorPoint.x = previousAnchorPoint.x;
lastAnchorPoint.y = this.mousePosition.y;
// Snapping vertical wire to pins
let pins: Pin[];
if (!this.placingWire.placedBackwards) {
pins = this.circuit.outputPins;
this.circuit.chips.forEach((chip) => {
pins = pins.concat(chip.inputPins);
});
} else {
pins = this.circuit.inputPins;
this.circuit.chips.forEach((chip) => {
pins = pins.concat(chip.outputPins);
});
}
let closestPositionY: number | undefined;
let closestDistance: number | undefined;
pins.forEach((pin) => {
const distance = Math.abs(this.mousePosition.y - pin.position.y);
if (closestDistance == null || closestDistance > distance) {
closestPositionY = pin.position.y;
closestDistance = distance;
}
});
if (closestPositionY != null && closestDistance < WIRE.snappingSensitivity)
lastAnchorPoint.y = closestPositionY;
}
updateWirePlacement() {
const anchorCount = this.placingWire.anchorPoints.length;
const lastAnchorPoint = this.placingWire.anchorPoints[anchorCount - 1];
if (!this.snapping) {
lastAnchorPoint.x = this.mousePosition.x;
lastAnchorPoint.y = this.mousePosition.y;
} else {
// Wire snapping
let previousAnchorPoint: Vector2;
if (anchorCount >= 2) {
previousAnchorPoint = this.placingWire.anchorPoints[anchorCount - 2];
} else if (!this.placingWire.placedBackwards) {
previousAnchorPoint = this.placingWire.inputPin.position;
} else {
previousAnchorPoint = this.placingWire.outputPin.position;
}
const deltaX = Math.abs(this.mousePosition.x - previousAnchorPoint.x);
const deltaY = Math.abs(this.mousePosition.y - previousAnchorPoint.y);
if (deltaX > deltaY) {
this.snapWireHorizontally(lastAnchorPoint, previousAnchorPoint);
} else {
this.snapWireVertically(lastAnchorPoint, previousAnchorPoint);
}
}
}
anchorWirePlacement() {
this.placingWire.anchorPoints.push(this.mousePosition.clone);
}
cancelWirePlacement() {
this.placingWire = null;
this.isPlacing = false;
this.circuit.wires.pop();
}
endWirePlacement(pin: Pin) {
const isInputPin = pin.isPointingRight;
let correctPlacement = false;
if (!isInputPin && !this.placingWire.placedBackwards) {
this.placingWire.outputPin = pin;
correctPlacement = true;
} else if (isInputPin && this.placingWire.placedBackwards) {
this.placingWire.inputPin = pin;
correctPlacement = true;
}
if (correctPlacement) {
this.placingWire.anchorPoints.pop();
this.placingWire.inputPin.addOutputWire(this.placingWire);
this.placingWire.inputPin.update();
this.placingWire = null;
this.isPlacing = false;
}
}
startChipPlacement(chip: Chip) {
const newChip = new Chip(this.circuit, chip.name, chip.color, false, chip.inputCount, chip.outputCount);
newChip.setLogic(chip.logic);
newChip.position = new Vector2(
this.mousePosition.x - newChip.size.x / 2,
this.mousePosition.y - newChip.size.y / 2
);
this.placingChip = newChip;
this.isPlacing = true;
this.circuit.chips.push(newChip);
}
editChipPlacement(chip: Chip, index: number) {
this.placingOffset = new Vector2(
(chip.position.x + chip.size.x / 2) - this.mousePosition.x,
(chip.position.y + chip.size.y / 2) - this.mousePosition.y
);
this.previousPlacement = chip.position.clone;
this.circuit.chips.push(this.circuit.chips.splice(index, 1)[0]);
this.placingChip = chip;
this.isPlacing = true;
}
updateChipPlacement() {
this.placingChip.position.x = this.mousePosition.x - this.placingChip.size.x / 2 + this.placingOffset.x;
this.placingChip.position.y = this.mousePosition.y - this.placingChip.size.y / 2 + this.placingOffset.y;
}
cancelChipPlacement() {
if (this.previousPlacement != null) {
this.placingChip.position = this.previousPlacement;
this.previousPlacement = null;
} else {
this.circuit.chips.pop();
}
this.placingChip = null;
this.isPlacing = false;
}
endChipPlacement() {
this.placingChip = null;
this.isPlacing = false;
this.placingOffset = Vector2.ZERO;
}
startPinPlacement(isInput: boolean) {
const newPin = new ControlledPin(this.circuit, "PIN", isInput);
newPin.position.x = CONTROLLER.handleTrackWidth + CONTROLLER.pinOffset + CONTROLLER.radius;
newPin.position.y = this.mousePosition.y;
if (isInput) {
this.circuit.inputPins.push(newPin);
} else {
newPin.position.x = this.circuit.size.x - newPin.position.x;
this.circuit.outputPins.push(newPin);
}
this.placingPin = newPin;
}
updatePinPlacement() {
this.placingPin.position.y = this.mousePosition.y;
}
cancelPinPlacement() {
if (this.placingPin.isInput) {
this.circuit.inputPins.pop();
} else {
this.circuit.outputPins.pop();
}
this.placingPin = null;
}
endPinPlacement() {
this.placingPin = null;
}
isPlacingPin(pin: Pin, index: number) {
if (!pin.isControlled || this.placingPin == null || this.placingPin.isInput != pin.isInput)
return false;
if (pin.isInput) {
return index == this.circuit.inputPins.length - 1;
} else {
return index == this.circuit.outputPins.length - 1;
}
}
}

View file

@ -1,56 +0,0 @@
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.ZERO;
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() {
// TO DO: clean this mess
this.outputWire?.setState(this.state);
this.attachedChip?.update();
}
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
);
}
}

View file

@ -0,0 +1,121 @@
import { COLORS, CONTROLLER, CURSORS } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2";
import { Circuit } from "../core/circuit";
import { Pin } from "./pin";
export class ControlledPin extends Pin {
constructor(circuit: Circuit, name: string, isInput: boolean, id?: number) {
super(circuit, name, isInput, circuit, id);
this.isControlled = true;
}
drawControllerHandle(isPlacing: boolean) {
const size = { x: CONTROLLER.handleWidth, y: CONTROLLER.radius * 2 };
let positionX = this.position.x;
const positionY = this.position.y - size.y / 2;
if (this.isInput) {
positionX -= CONTROLLER.pinOffset + CONTROLLER.handleTrackWidth + CONTROLLER.radius;
} else {
positionX += CONTROLLER.pinOffset + (CONTROLLER.handleTrackWidth - CONTROLLER.handleWidth) + CONTROLLER.radius;
}
const rect = {
position: { x: positionX, y: positionY } as Vector2,
size: { x: size.x, y: size.y } as Vector2
};
let color: string;
if (this.circuit.isPointInsideRect(rect, this.circuit.inputHandler.mousePosition)) {
color = COLORS.controller.handleHover;
this.circuit.cursor = CURSORS.pointer;
} else {
color = COLORS.controller.handle;
}
if (isPlacing)
this.circuit.setDrawingOpacity(CONTROLLER.placingOpacity);
this.circuit.drawRect(
this.circuit.getColor(color),
rect.position.x, rect.position.y,
rect.size.x, rect.size.y
);
if (isPlacing)
this.circuit.resetDrawingOpacity();
}
drawController(isPlacing: boolean) {
const positionX = this.isInput ? this.position.x - CONTROLLER.pinOffset : this.position.x + CONTROLLER.pinOffset;
const positionY = this.position.y;
let color: string;
if (this.state.value === 1) {
color = COLORS.controller.on;
} else {
color = COLORS.controller.off;
}
if (isPlacing)
this.circuit.setDrawingOpacity(CONTROLLER.placingOpacity);
this.circuit.drawCircle(
this.circuit.getColor(COLORS.controller.stroke),
positionX, positionY,
CONTROLLER.radius
);
this.circuit.drawCircle(
this.circuit.getColor(color),
positionX, positionY,
CONTROLLER.radius - CONTROLLER.borderWidth
);
const isInteractable = this.isInput && this.isControlled && !isPlacing;
if (isInteractable && this.circuit.inputHandler.mousePosition.getDistance(positionX, positionY) <= CONTROLLER.radius) {
this.circuit.setDrawingOpacity(0.125);
this.circuit.drawCircle(
this.circuit.getColor(COLORS.controller.hover),
positionX, positionY,
CONTROLLER.radius - CONTROLLER.borderWidth
);
this.circuit.resetDrawingOpacity();
this.circuit.cursor = CURSORS.pointer;
}
if (isPlacing)
this.circuit.resetDrawingOpacity();
}
drawConnector(isPlacing: boolean) {
if (isPlacing)
return;
const positionX = this.isInput ? this.position.x - CONTROLLER.pinOffset : this.position.x;
const positionY = this.position.y;
if (isPlacing)
this.circuit.setDrawingOpacity(CONTROLLER.placingOpacity);
this.circuit.drawRect(
this.circuit.getColor(COLORS.controller.connector),
positionX, positionY - CONTROLLER.connectorWidth / 2,
CONTROLLER.pinOffset, CONTROLLER.connectorWidth
);
if (isPlacing)
this.circuit.resetDrawingOpacity();
}
draw(isPlacing: boolean) {
this.drawConnector(isPlacing);
this.drawController(isPlacing);
this.drawControllerHandle(isPlacing);
super.draw(isPlacing);
}
}

View file

@ -0,0 +1,125 @@
import { COLORS, CONTROLLER, CURSORS, PIN } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2";
import { Chip } from "../chips/chip";
import { Circuit } from "../core/circuit";
import { State } from "../_utils/state";
import { Wire } from "../wires/wire";
export interface PinJson {
name: string;
id: number;
position: {
x: number;
y: number;
};
}
export class Pin {
id: number;
name: string;
position = Vector2.ZERO;
attachedChip: Chip;
circuit: Circuit;
state = State.LOW;
isInput: boolean;
isControlled: boolean = false;
outputWires: Wire[] = [];
constructor(circuit: Circuit, name: string, isInput: boolean, attachedChip: Chip, id?: number) {
Object.assign(this, { circuit, name, isInput, attachedChip });
this.id = id ?? this.circuit.getUniqueId();
}
addOutputWire(wire: Wire) {
this.outputWires.push(wire);
wire.setState(this.state);
}
setState(state: State) {
if (this.state.isEqual(state))
return;
this.state = state;
this.update();
}
update() {
// TO DO: clean this mess
this.outputWires.forEach((wire) => {
wire.setState(this.state);
});
this.attachedChip?.update();
}
get isPointingRight() {
return this.isInput === this.isControlled;
}
draw(isPlacing: boolean) {
let color = COLORS.pin.fill;
if (this.circuit.inputHandler.mousePosition.getDistance(this.position.x, this.position.y) <= PIN.radius) {
this.circuit.cursor = CURSORS.pointer;
color = COLORS.pin.fillHover;
// Draw label
let positionX = this.position.x;
const leftAligned = this.isPointingRight;
const textRect = this.circuit.getTextRect(PIN.label.fontSize, this.name);
if (leftAligned) {
positionX += PIN.radius + PIN.label.offset;
} else {
positionX -= PIN.radius + PIN.label.offset;
}
const backgroundSize = {
x: textRect.x + PIN.label.padding * 2,
y: textRect.y + PIN.label.padding * 2
};
this.circuit.drawRect(
this.circuit.getColor(COLORS.pin.labelBackground),
leftAligned ? positionX : positionX - backgroundSize.x, this.position.y - textRect.y / 2 - PIN.label.padding,
backgroundSize.x, backgroundSize.y
);
if (leftAligned) {
positionX += PIN.label.padding;
} else {
positionX -= PIN.label.padding;
}
this.circuit.drawText(
this.circuit.getColor(COLORS.pin.labelText),
leftAligned ? "left" : "right",
positionX, this.position.y,
PIN.label.fontSize,
this.name
);
}
if (isPlacing)
this.circuit.setDrawingOpacity(CONTROLLER.placingOpacity);
this.circuit.drawCircle(
this.circuit.getColor(color),
this.position.x, this.position.y,
PIN.radius
);
if (isPlacing)
this.circuit.resetDrawingOpacity();
}
toJson(): PinJson {
const object = {
name: this.name,
id: this.id,
position: this.position
} as PinJson;
return object;
}
}

View file

@ -1,54 +0,0 @@
import { WIRE } from "../../../config/apps/logicSim.config";
import Vector2 from "../../math/vector2";
import { Circuit } from "./circuit";
import { Pin } from "./pin";
import { State } from "./state";
export class Wire {
color: string;
state = State.LOW;
inputPin: Pin;
outputPin: Pin;
anchorPoints: Vector2[];
circuit: Circuit;
constructor(circuit: Circuit, color: string, inputPin?: Pin, outputPin?: Pin, anchorPoints?: Vector2[]) {
Object.assign(this, { circuit, 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);
}
draw(isPlacingWire: boolean) {
const positions = [...this.anchorPoints];
if (this.inputPin != null)
positions.unshift(this.inputPin.position);
if (this.outputPin != null)
positions.push(this.outputPin.position);
let color: string;
if (isPlacingWire) {
color = `${this.color}-2`;
} else if (this.state.value === 1) {
color = `${this.color}-0`;
} else {
color = `${this.color}-2`;
}
this.circuit.drawLine(this.circuit.getColor(color), positions, WIRE.width);
}
}

View file

@ -0,0 +1,93 @@
import { WIRE } from "../../../../config/apps/logicSim.config";
import Vector2 from "../../../math/vector2";
import { Circuit } from "../core/circuit";
import { Pin } from "../pins/pin";
import { State } from "../_utils/state";
export interface WireJson {
color: string;
inputId: number;
outputId: number;
anchorPoints: {
x: number;
y: number;
}[];
}
export class Wire {
color: string;
state = State.LOW;
inputPin: Pin;
outputPin: Pin;
anchorPoints: Vector2[];
circuit: Circuit;
placedBackwards = false;
constructor(circuit: Circuit, color: string, inputPin?: Pin, outputPin?: Pin, anchorPoints?: Vector2[]) {
Object.assign(this, { circuit, 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);
}
draw(isPlacing: boolean) {
const positions = [...this.anchorPoints];
if (this.inputPin != null) {
if (!this.placedBackwards) {
positions.unshift(this.inputPin.position);
} else {
positions.push(this.inputPin.position);
}
}
if (this.outputPin != null) {
if (!this.placedBackwards) {
positions.push(this.outputPin.position);
} else {
positions.unshift(this.outputPin.position);
}
}
let color: string;
if (isPlacing) {
color = `${this.color}-2`;
} else if (this.state.value === 1) {
color = `${this.color}-0`;
} else {
color = `${this.color}-2`;
}
this.circuit.drawCurvedLine(this.circuit.getColor(color), positions, WIRE.width, WIRE.cornerRadius, WIRE.resolution);
}
toJson() {
const object = {
color: this.color
} as WireJson;
if (this.inputPin != null)
object.inputId = this.inputPin.id;
if (this.outputPin != null)
object.outputId = this.outputPin.id;
if (this.anchorPoints != null)
object.anchorPoints = this.anchorPoints;
return object;
}
}

View file

@ -38,4 +38,38 @@ export default class Vector2 {
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
}
static add(vector2A: Vector2, vector2B: Vector2) {
return new Vector2(vector2A.x + vector2B.x, vector2A.y + vector2B.y);
}
static subtract(vector2A: Vector2, vector2B: Vector2) {
return new Vector2(vector2A.x - vector2B.x, vector2A.y - vector2B.y);
}
static scale(vector2: Vector2, scalar: number) {
return new Vector2(vector2.x * scalar, vector2.y * scalar);
}
static magnitude(vector2: Vector2) {
return Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y);
}
static normalize(vector2: Vector2) {
const magnitude = Vector2.magnitude(vector2);
return magnitude === 0 ? Vector2.ZERO : Vector2.scale(vector2, 1 / magnitude);
}
static sqrDistance(vector2A: Vector2, vector2B: Vector2) {
const dx = vector2A.x - vector2B.x;
const dy = vector2A.y - vector2B.y;
return dx * dx + dy * dy;
}
static lerp(vector2A: Vector2, vector2B: Vector2, t: number) {
return new Vector2(
vector2A.x + (vector2B.x - vector2A.x) * t,
vector2A.y + (vector2B.y - vector2A.y) * t
);
}
}

View file

@ -74,7 +74,8 @@ export function loadDefaultData(virtualRoot: VirtualRoot) {
}).createFolderLink("Documents", (folderLink: VirtualFolderLink) => {
folderLink.setLinkedPath(linkedPaths.documents);
});
});
})
.createFolder("Apps");
});
});

View file

@ -0,0 +1,27 @@
import { useEffect, useState } from "react";
import App from "../../features/apps/app";
import { VirtualFolder } from "../../features/virtual-drive/folder";
import { useVirtualRoot } from "../virtual-drive/virtualRootContext";
export function useAppFolder(app: App): VirtualFolder {
const virtualRoot = useVirtualRoot();
const [folder, setFolder] = useState<VirtualFolder>(null);
useEffect(() => {
if (folder != null)
return;
const parentFolder = virtualRoot.navigate("~/Apps") as VirtualFolder;
if (parentFolder == null) {
console.warn("Folder missing: '~/Apps'");
return;
}
parentFolder.createFolder(app.id, (newFolder) => {
setFolder(newFolder);
});
}, [virtualRoot]);
return folder;
}