Updated logic sim
19
index.html
|
|
@ -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
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
BIN
public/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
BIN
public/assets/wallpapers/colorful-mesh-gradient-red-green.png
Normal file
|
After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 211 KiB |
9
public/browserconfig.xml
Normal 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
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 19 KiB |
BIN
public/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/icon.png
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 56 KiB |
BIN
public/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
44
public/safari-pinned-tab.svg
Normal 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 |
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -152,6 +152,7 @@
|
|||
}
|
||||
|
||||
.Header-menu .Dropdown {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 100%;
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
}
|
||||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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%);
|
||||
}
|
||||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -1,3 +1,11 @@
|
|||
.SettingsContainer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.Settings {
|
||||
--tabs-width: 10rem;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
margin-top: auto;
|
||||
z-index: -1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.Buttons > button {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export const APPS = {
|
|||
FILE_EXPLORER: "file-explorer",
|
||||
CALCULATOR: "calculator",
|
||||
BROWSER: "browser",
|
||||
LOGIC_SIM: "logic-sim",
|
||||
};
|
||||
|
||||
export const APP_NAMES = {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
};
|
||||
|
|
@ -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",
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
176
src/features/apps/logic-sim/chips/chip.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
107
src/features/apps/logic-sim/chips/chipsManager.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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])];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
277
src/features/apps/logic-sim/core/circuit.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
503
src/features/apps/logic-sim/core/inputHandler.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
121
src/features/apps/logic-sim/pins/controlledPin.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
125
src/features/apps/logic-sim/pins/pin.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
93
src/features/apps/logic-sim/wires/wire.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +74,8 @@ export function loadDefaultData(virtualRoot: VirtualRoot) {
|
|||
}).createFolderLink("Documents", (folderLink: VirtualFolderLink) => {
|
||||
folderLink.setLinkedPath(linkedPaths.documents);
|
||||
});
|
||||
});
|
||||
})
|
||||
.createFolder("Apps");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
27
src/hooks/apps/appFolder.ts
Normal 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;
|
||||
}
|
||||