Updated logic sim
19
index.html
|
|
@ -9,13 +9,16 @@
|
||||||
<link rel="canonical" href="https://os.prozilla.dev/">
|
<link rel="canonical" href="https://os.prozilla.dev/">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/x-icon" sizes="any" href="/favicon.ico?v=3">
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=4">
|
||||||
<link rel="shortcut icon" type="image/svg+xml" href="/icon.svg?v=2"/>
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=4">
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=3">
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=4">
|
||||||
<link rel="mask-icon" href="/icon.svg?v=2" color="#36b29e"/>
|
<link rel="manifest" href="/site.webmanifest?v=4">
|
||||||
<meta name="msapplication-TileColor" content="#0d1114"/>
|
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=4" color="#4cdfff">
|
||||||
<meta name="theme-color" content="#0d1114"/>
|
<link rel="shortcut icon" href="/favicon.ico?v=4">
|
||||||
<link rel="manifest" href="/manifest.json?v=3">
|
<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 -->
|
<!-- Open Graph -->
|
||||||
<meta property="og:title" content="ProzillaOS | Web-based Operating System"/>
|
<meta property="og:title" content="ProzillaOS | Web-based Operating System"/>
|
||||||
|
|
@ -40,7 +43,7 @@
|
||||||
<!-- PWA -->
|
<!-- PWA -->
|
||||||
<meta name="mobile-web-app-capable" content="yes">
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-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 -->
|
<!-- FAQ -->
|
||||||
<script type="application/ld+json">
|
<script type="application/ld+json">
|
||||||
|
|
|
||||||
39
package-lock.json
generated
|
|
@ -22,6 +22,7 @@
|
||||||
"react-draggable": "^4.4.5",
|
"react-draggable": "^4.4.5",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-error-boundary": "^4.0.13",
|
||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
|
"react-router-dom": "^6.23.1",
|
||||||
"react-svg": "^16.1.18",
|
"react-svg": "^16.1.18",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"react-tabs": "^6.0.2",
|
"react-tabs": "^6.0.2",
|
||||||
|
|
@ -1126,6 +1127,14 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@rollup/pluginutils": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/react-ga4/-/react-ga4-2.1.0.tgz",
|
||||||
"integrity": "sha512-ZKS7PGNFqqMd3PJ6+C2Jtz/o1iU9ggiy8Y8nUeksgVuvNISbmrQtJiZNvC/TjDsqD0QlU5Wkgs7i+w9+OjHhhQ=="
|
"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": {
|
"node_modules/react-svg": {
|
||||||
"version": "16.1.18",
|
"version": "16.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/react-svg/-/react-svg-16.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/react-svg/-/react-svg-16.1.18.tgz",
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"react-draggable": "^4.4.5",
|
"react-draggable": "^4.4.5",
|
||||||
"react-error-boundary": "^4.0.13",
|
"react-error-boundary": "^4.0.13",
|
||||||
"react-ga4": "^2.1.0",
|
"react-ga4": "^2.1.0",
|
||||||
|
"react-router-dom": "^6.23.1",
|
||||||
"react-svg": "^16.1.18",
|
"react-svg": "^16.1.18",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"react-tabs": "^6.0.2",
|
"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",
|
"name": "ProzillaOS",
|
||||||
"short_name": "ProzillaOS",
|
"short_name": "ProzillaOS",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/favicon-192.png",
|
"src": "/android-chrome-192x192.png?v=4",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/favicon-512.png",
|
"src": "/android-chrome-512x512.png?v=4",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"theme_color": "#0d1114",
|
"theme_color": "#ffffff",
|
||||||
"background_color": "#0d1114",
|
"background_color": "#ffffff",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
}
|
}
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/colorful-abstract-wallpaper-blue-red-green.png</image:loc>
|
<image:loc>https://os.prozilla.dev/assets/wallpapers/colorful-abstract-wallpaper-blue-red-green.png</image:loc>
|
||||||
</image:image>
|
</image:image>
|
||||||
<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:image>
|
<image:image>
|
||||||
<image:loc>https://os.prozilla.dev/assets/wallpapers/mesh-gradient-wallpaper-red-purple.png</image:loc>
|
<image:loc>https://os.prozilla.dev/assets/wallpapers/mesh-gradient-wallpaper-red-purple.png</image:loc>
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.Header-menu .Dropdown {
|
.Header-menu .Dropdown {
|
||||||
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
.CircuitView {
|
.CircuitView {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: calc(100% - var(--header-height));
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Canvas {
|
.Canvas {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
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 styles from "./CircuitView.module.css";
|
||||||
import { DropdownAction } from "../../actions/actions/DropdownAction";
|
import { DropdownAction } from "../../actions/actions/DropdownAction";
|
||||||
import { HeaderMenu } from "../_utils/header-menu/HeaderMenu";
|
import { HeaderMenu } from "../_utils/header-menu/HeaderMenu";
|
||||||
import { ClickAction } from "../../actions/actions/ClickAction";
|
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 [circuit] = useState(new Circuit("Chip", "#000", 2, 1));
|
||||||
const canvasRef = useRef(null);
|
const canvasRef = useRef(null);
|
||||||
|
|
||||||
|
|
@ -23,9 +31,27 @@ export function CircuitView() {
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<HeaderMenu>
|
<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}>
|
<DropdownAction label="Add" showOnHover={false}>
|
||||||
<ClickAction label="AND gate" onTrigger={() => { circuit.addChip(ChipsManager.CHIPS.AND); }}/>
|
<ClickAction label="AND gate" onTrigger={() => {
|
||||||
<ClickAction label="NOT gate" onTrigger={() => { circuit.addChip(ChipsManager.CHIPS.NOT); }}/>
|
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>
|
</DropdownAction>
|
||||||
</HeaderMenu>
|
</HeaderMenu>
|
||||||
<div className={styles.CircuitView}>
|
<div className={styles.CircuitView}>
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,12 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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 { CircuitView } from "./CircuitView";
|
||||||
import styles from "./LogicSim.module.css";
|
import styles from "./LogicSim.module.css";
|
||||||
|
|
||||||
export function LogicSim() {
|
export function LogicSim({ app }: WindowProps) {
|
||||||
return <div className={styles.LogicSim}>
|
return <div className={styles.LogicSim}>
|
||||||
<CircuitView/>
|
<CircuitView app={app}/>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
|
.SettingsContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.Settings {
|
.Settings {
|
||||||
--tabs-width: 10rem;
|
--tabs-width: 10rem;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ interface SettingsProps extends WindowProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Settings({ tab }: SettingsProps) {
|
export function Settings({ tab }: SettingsProps) {
|
||||||
return (
|
return <div className={styles.SettingsContainer}>
|
||||||
<Tabs
|
<Tabs
|
||||||
defaultIndex={tab ?? 0}
|
defaultIndex={tab ?? 0}
|
||||||
className={styles.Settings}
|
className={styles.Settings}
|
||||||
|
|
@ -52,5 +52,5 @@ export function Settings({ tab }: SettingsProps) {
|
||||||
<AboutSettings/>
|
<AboutSettings/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
z-index: -1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Buttons > button {
|
.Buttons > button {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ export const APPS = {
|
||||||
FILE_EXPLORER: "file-explorer",
|
FILE_EXPLORER: "file-explorer",
|
||||||
CALCULATOR: "calculator",
|
CALCULATOR: "calculator",
|
||||||
BROWSER: "browser",
|
BROWSER: "browser",
|
||||||
|
LOGIC_SIM: "logic-sim",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const APP_NAMES = {
|
export const APP_NAMES = {
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,79 @@
|
||||||
|
import { APPS } from "../apps.config";
|
||||||
|
|
||||||
export const CURSORS = {
|
export const CURSORS = {
|
||||||
default: "default",
|
default: "default",
|
||||||
pointer: "pointer"
|
pointer: "pointer"
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FONT = "outfit";
|
export const FONT = "outfit";
|
||||||
|
export const ENABLE_COLOR_CACHING = true;
|
||||||
|
export const VIRTUAL_PATH = `~/Apps/${APPS.LOGIC_SIM}/`;
|
||||||
|
|
||||||
export const BACKGROUND = {
|
export const BACKGROUND = {
|
||||||
padding: 40,
|
padding: 30,
|
||||||
borderWidth: 8,
|
borderWidth: 7.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const INPUT_OUTPUT = {
|
export const CONTROLLER = {
|
||||||
radius: 24,
|
radius: 25,
|
||||||
borderWidth: 4,
|
borderWidth: 5,
|
||||||
pinOffset: 40,
|
pinOffset: 42.5,
|
||||||
connectorWidth: 10,
|
connectorWidth: 7.5,
|
||||||
|
handleWidth: 15,
|
||||||
|
handleTrackWidth: 22.5,
|
||||||
|
placingOpacity: 0.5,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const WIRE = {
|
export const WIRE = {
|
||||||
width: 4,
|
width: 5,
|
||||||
|
snappingSensitivity: 10,
|
||||||
|
cornerRadius: 25,
|
||||||
|
resolution: 8,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PIN = {
|
export const PIN = {
|
||||||
radius: 8,
|
radius: 10,
|
||||||
|
label: {
|
||||||
|
offset: 10,
|
||||||
|
fontSize: 15,
|
||||||
|
padding: 5,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const CHIP = {
|
export const CHIP = {
|
||||||
width: 140,
|
BorderWidth: 5,
|
||||||
height: 72,
|
padding: 10,
|
||||||
padding: 12,
|
fontSize: 35,
|
||||||
fontSize: 36,
|
placingOutline: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const COLORS = {
|
export const COLORS = {
|
||||||
pin: {
|
pin: {
|
||||||
fill: "black-4",
|
fill: "black-4",
|
||||||
fillHover: "black-3"
|
fillHover: "black-3",
|
||||||
|
labelText: "white-0",
|
||||||
|
labelBackground: "black-4"
|
||||||
},
|
},
|
||||||
inputOutput: {
|
controller: {
|
||||||
stroke: "black-4",
|
stroke: "black-4",
|
||||||
connector: "black-3",
|
connector: "black-4",
|
||||||
on: "red-0",
|
on: "red-0",
|
||||||
onHover: "red-0",
|
|
||||||
off: "red-2",
|
off: "red-2",
|
||||||
offHover: "red-1",
|
hover: "white-0",
|
||||||
|
handle: "black-3",
|
||||||
|
handleHover: "black-4",
|
||||||
},
|
},
|
||||||
background: {
|
background: {
|
||||||
border: "black-0",
|
border: "black-0",
|
||||||
inner: "black-1",
|
outer: "black-1",
|
||||||
outer: "black-2",
|
inner: "black-2",
|
||||||
|
margin: "black-2",
|
||||||
},
|
},
|
||||||
wire: {
|
wire: {
|
||||||
placing: "black-1",
|
placing: "black-1",
|
||||||
},
|
},
|
||||||
chip: {
|
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/abstract-wallpaper-mesh-gradient-cyan.png",
|
||||||
"/assets/wallpapers/colorful-abstract-wallpaper-blue-red-green.png",
|
"/assets/wallpapers/colorful-abstract-wallpaper-blue-red-green.png",
|
||||||
"/assets/wallpapers/mesh-gradient-wallpaper-red-purple.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/flame-abstract-wallpaper-orange.png",
|
||||||
"/assets/wallpapers/wave-abstract-wallpaper-teal.png",
|
"/assets/wallpapers/wave-abstract-wallpaper-teal.png",
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ export function getViewportParams(): Record<string, string> {
|
||||||
const params = {};
|
const params = {};
|
||||||
query.split("&").forEach((param) => {
|
query.split("&").forEach((param) => {
|
||||||
// For some reason, URI components only decode when decoded twice
|
// 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)));
|
const [key, value] = param.split("=").map((item) => decodeURIComponent(decodeURIComponent(item)));
|
||||||
params[key] = value;
|
params[key] = value;
|
||||||
});
|
});
|
||||||
|
|
@ -83,6 +83,10 @@ export function generateUrl(options: { appId: string; fullscreen: boolean; }) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openUrl(url: string) {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
export function copyToClipboard(string: string, onSuccess: (value: void) => void, onFail: (value: void) => void) {
|
export function copyToClipboard(string: string, onSuccess: (value: void) => void, onFail: (value: void) => void) {
|
||||||
navigator.clipboard.writeText(string).then(onSuccess, onFail);
|
navigator.clipboard.writeText(string).then(onSuccess, onFail);
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +36,7 @@ export default class AppsManager {
|
||||||
new App(APP_NAMES.BROWSER, APPS.BROWSER, Browser, {
|
new App(APP_NAMES.BROWSER, APPS.BROWSER, Browser, {
|
||||||
size: new Vector2(700, 500)
|
size: new Vector2(700, 500)
|
||||||
}),
|
}),
|
||||||
new App("Logic Sim", "logic-sim", LogicSim),
|
new App("Logic Sim (WIP)", "logic-sim", LogicSim),
|
||||||
];
|
];
|
||||||
|
|
||||||
static getAppById(id: string): App | null {
|
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);
|
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) => {
|
}).createFolderLink("Documents", (folderLink: VirtualFolderLink) => {
|
||||||
folderLink.setLinkedPath(linkedPaths.documents);
|
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;
|
||||||
|
}
|
||||||