├── .gitattributes
├── .gitignore
├── Banner.jpg
├── LICENSE
├── LogicBoard.swiftpm
├── .swiftpm
│ ├── playgrounds
│ │ ├── CachedManifest.plist
│ │ ├── DocumentThumbnail.plist
│ │ ├── DocumentThumbnail.png
│ │ └── Workspace.plist
│ └── xcode
│ │ ├── package.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcuserdata
│ │ │ └── joseadolfot.xcuserdatad
│ │ │ ├── IDEFindNavigatorScopes.plist
│ │ │ └── UserInterfaceState.xcuserstate
│ │ └── xcuserdata
│ │ └── joseadolfot.xcuserdatad
│ │ ├── xcdebugger
│ │ └── Breakpoints_v2.xcbkptlist
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
├── Assets.xcassets
│ ├── AND.imageset
│ │ ├── AND.png
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── AppIcon.png
│ │ └── Contents.json
│ ├── BUFFER.imageset
│ │ ├── BUFFER.png
│ │ └── Contents.json
│ ├── Contents.json
│ ├── InputFalse.imageset
│ │ ├── Contents.json
│ │ └── InputFalse.png
│ ├── InputTrue.imageset
│ │ ├── Contents.json
│ │ └── InputTrue.png
│ ├── NAND.imageset
│ │ ├── Contents.json
│ │ └── NAND.png
│ ├── NOR.imageset
│ │ ├── Contents.json
│ │ └── NOR.png
│ ├── NOT.imageset
│ │ ├── Contents.json
│ │ └── NOT.png
│ ├── OR.imageset
│ │ ├── Contents.json
│ │ └── OR.png
│ ├── OutputFalse.imageset
│ │ ├── Contents.json
│ │ └── OutputFalse.png
│ ├── OutputTrue.imageset
│ │ ├── Contents.json
│ │ └── OutputTrue.png
│ ├── XNOR.imageset
│ │ ├── Contents.json
│ │ └── XNOR.png
│ ├── XOR.imageset
│ │ ├── Contents.json
│ │ └── XOR.png
│ └── background.imageset
│ │ ├── Contents.json
│ │ └── background.jpg
├── LogicApp.swift
├── LogicBoard
│ ├── BoardState.swift
│ ├── Circuit
│ │ ├── Circuit+Connection.swift
│ │ ├── Circuit+Device.swift
│ │ ├── Circuit+Examples.swift
│ │ ├── Circuit+Solver.swift
│ │ ├── Circuit.swift
│ │ ├── CircuitData.swift
│ │ └── CircuitDocument.swift
│ ├── Components
│ │ ├── Connection
│ │ │ └── Connection.swift
│ │ └── Device
│ │ │ ├── Device+CanvasScene.swift
│ │ │ ├── Device.swift
│ │ │ ├── DeviceName.swift
│ │ │ ├── DevicePin.swift
│ │ │ ├── Gate
│ │ │ ├── And.swift
│ │ │ ├── Buffer.swift
│ │ │ ├── Nand.swift
│ │ │ ├── Nor.swift
│ │ │ ├── Not.swift
│ │ │ ├── Or.swift
│ │ │ ├── Xnor.swift
│ │ │ └── Xor.swift
│ │ │ ├── Input
│ │ │ └── PushButton.swift
│ │ │ └── Output
│ │ │ └── Display.swift
│ ├── Extensions
│ │ └── Nameable.swift
│ ├── LogicBoard.swift
│ └── LogicEnvironment.swift
├── Package.swift
├── Popover
│ ├── LICENSE
│ └── Sources
│ │ ├── Popover+Attributes.swift
│ │ ├── Popover+Calculations.swift
│ │ ├── Popover+Context.swift
│ │ ├── Popover+Lifecycle.swift
│ │ ├── Popover+Positioning.swift
│ │ ├── Popover.swift
│ │ ├── PopoverContainerView.swift
│ │ ├── PopoverGestureContainer.swift
│ │ ├── PopoverModel.swift
│ │ ├── PopoverUtilities.swift
│ │ ├── PopoverWindows.swift
│ │ ├── Popovers.swift
│ │ ├── SwiftUI
│ │ ├── FrameTag.swift
│ │ ├── Modifiers.swift
│ │ └── Readers.swift
│ │ └── Templates
│ │ ├── Alert.swift
│ │ ├── Blur.swift
│ │ ├── Container.swift
│ │ ├── Extensions.swift
│ │ ├── Hero.swift
│ │ ├── Menu
│ │ ├── Menu+SwiftUI.swift
│ │ ├── Menu+UIKit.swift
│ │ ├── Menu.swift
│ │ └── Model
│ │ │ ├── MenuGestureModel.swift
│ │ │ └── MenuModel.swift
│ │ ├── Shadow.swift
│ │ ├── Shapes.swift
│ │ ├── Templates.swift
│ │ └── Views.swift
└── Views
│ ├── BoardView.swift
│ ├── Canvas
│ ├── CanvasScene+Animation.swift
│ ├── CanvasScene+Color.swift
│ ├── CanvasScene+Device.swift
│ ├── CanvasScene+Texture.swift
│ ├── CanvasScene.swift
│ ├── CanvasView.swift
│ └── Gestures
│ │ ├── Pan+OneFinger.swift
│ │ ├── Pan+TwoFinger.swift
│ │ ├── Pencil.swift
│ │ ├── Pinch.swift
│ │ └── Tap.swift
│ ├── Color
│ └── Color.swift
│ ├── Controls
│ └── ActionControlView.swift
│ ├── Pickers
│ ├── CircuitPickerView.swift
│ ├── DevicePickerView.swift
│ └── SystemPickerView.swift
│ └── ViewStyles
│ ├── ButtonStyles
│ ├── ActionButtonStyle.swift
│ ├── CircleButtonStyle.swift
│ ├── CircuitButtonStyle.swift
│ ├── NoFlickerButtonStyle.swift
│ └── SystemButtonStyle.swift
│ └── LabelStyles
│ └── ActionLabelStyle.swift
├── README.md
└── Screenshots
├── Screenshot1.gif
├── Screenshot2.gif
└── Screenshot3.gif
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 |
--------------------------------------------------------------------------------
/Banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/Banner.jpg
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Jose Adolfo Talactac
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/playgrounds/CachedManifest.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CachedManifest
6 |
7 | manifestData
8 |
9 | eyJkZXBlbmRlbmNpZXMiOlt7InNvdXJjZUNvbnRyb2wiOlt7ImlkZW50aXR5
10 | IjoicG9wb3ZlcnMiLCJsb2NhdGlvbiI6eyJyZW1vdGUiOlsiaHR0cHM6XC9c
11 | L2dpdGh1Yi5jb21cL2FoZXplXC9Qb3BvdmVycyJdfSwicHJvZHVjdEZpbHRl
12 | ciI6bnVsbCwicmVxdWlyZW1lbnQiOnsicmFuZ2UiOlt7Imxvd2VyQm91bmQi
13 | OiIxLjMuMiIsInVwcGVyQm91bmQiOiIyLjAuMCJ9XX19XX1dLCJkaXNwbGF5
14 | TmFtZSI6IkxvZ2ljQm9hcmQiLCJwYWNrYWdlS2luZCI6eyJyb290Ijp7fX0s
15 | InBsYXRmb3JtcyI6W3sib3B0aW9ucyI6W10sInBsYXRmb3JtTmFtZSI6Imlv
16 | cyIsInZlcnNpb24iOiIxNi4wIn1dLCJwcm9kdWN0cyI6W3sibmFtZSI6Ikxv
17 | Z2ljQm9hcmQiLCJzZXR0aW5ncyI6W3sidGVhbUlkZW50aWZpZXIiOlsiNlNL
18 | NFk0RFRNRSJdfSx7ImRpc3BsYXlWZXJzaW9uIjpbIjEuMCJdfSx7ImJ1bmRs
19 | ZVZlcnNpb24iOlsiMSJdfSx7ImlPU0FwcEluZm8iOlt7ImFjY2VudENvbG9y
20 | Ijp7InByZXNldENvbG9yIjp7InByZXNldENvbG9yIjp7InJhd1ZhbHVlIjoi
21 | YnJvd24ifX19LCJhcHBDYXRlZ29yeSI6eyJyYXdWYWx1ZSI6InB1YmxpYy5h
22 | cHAtY2F0ZWdvcnkuZWR1Y2F0aW9uIn0sImFwcEljb24iOnsiYXNzZXQiOnsi
23 | bmFtZSI6IkFwcEljb24ifX0sImNhcGFiaWxpdGllcyI6W3siZmlsZUFjY2Vz
24 | c0xvY2F0aW9uIjoiZG93bmxvYWRzRm9sZGVyIiwiZmlsZUFjY2Vzc01vZGUi
25 | OiJyZWFkV3JpdGUiLCJwdXJwb3NlIjoiZmlsZUFjY2VzcyJ9LHsiZmlsZUFj
26 | Y2Vzc0xvY2F0aW9uIjoidXNlclNlbGVjdGVkRmlsZXMiLCJmaWxlQWNjZXNz
27 | TW9kZSI6InJlYWRXcml0ZSIsInB1cnBvc2UiOiJmaWxlQWNjZXNzIn1dLCJz
28 | dXBwb3J0ZWREZXZpY2VGYW1pbGllcyI6WyJwYWQiLCJwaG9uZSJdLCJzdXBw
29 | b3J0ZWRJbnRlcmZhY2VPcmllbnRhdGlvbnMiOlt7InBvcnRyYWl0Ijp7fX0s
30 | eyJsYW5kc2NhcGVSaWdodCI6e319LHsibGFuZHNjYXBlTGVmdCI6e319LHsi
31 | cG9ydHJhaXRVcHNpZGVEb3duIjp7ImNvbmRpdGlvbiI6eyJkZXZpY2VGYW1p
32 | bGllcyI6WyJwYWQiXX19fV19XX1dLCJ0YXJnZXRzIjpbIkFwcE1vZHVsZSJd
33 | LCJ0eXBlIjp7ImV4ZWN1dGFibGUiOm51bGx9fV0sInRhcmdldE1hcCI6eyJB
34 | cHBNb2R1bGUiOnsiZGVwZW5kZW5jaWVzIjpbeyJwcm9kdWN0IjpbIlBvcG92
35 | ZXJzIiwiUG9wb3ZlcnMiLG51bGwsbnVsbF19XSwiZXhjbHVkZSI6W10sIm5h
36 | bWUiOiJBcHBNb2R1bGUiLCJwYXRoIjoiLiIsInJlc291cmNlcyI6W3sicGF0
37 | aCI6IlJlc291cmNlcyIsInJ1bGUiOnsicHJvY2VzcyI6e319fV0sInNldHRp
38 | bmdzIjpbXSwidHlwZSI6ImV4ZWN1dGFibGUifX0sInRhcmdldHMiOlt7ImRl
39 | cGVuZGVuY2llcyI6W3sicHJvZHVjdCI6WyJQb3BvdmVycyIsIlBvcG92ZXJz
40 | IixudWxsLG51bGxdfV0sImV4Y2x1ZGUiOltdLCJuYW1lIjoiQXBwTW9kdWxl
41 | IiwicGF0aCI6Ii4iLCJyZXNvdXJjZXMiOlt7InBhdGgiOiJSZXNvdXJjZXMi
42 | LCJydWxlIjp7InByb2Nlc3MiOnt9fX1dLCJzZXR0aW5ncyI6W10sInR5cGUi
43 | OiJleGVjdXRhYmxlIn1dLCJ0b29sc1ZlcnNpb24iOnsiX3ZlcnNpb24iOiI1
44 | LjcuMCJ9fQ==
45 |
46 | manifestHash
47 |
48 | jbyhkwndT6RWd4hKxKRJlgJSFyHaYdEebuUn2YuRIXI=
49 |
50 | schemaVersion
51 | 4
52 | swiftPMVersionString
53 | 5.8.0
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DocumentThumbnailConfiguration
6 |
7 | accentColorHash
8 | ukeIsiaqjcLm3HQki7n2GM+oyVngwmwUe+SPaDmgsIg=
9 | appIconHash
10 | aAneklUa6ah6uQ25MydaKTnGTrM2wgW9IrnWWr7/Wys=
11 | thumbnailIsPrerendered
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/playgrounds/Workspace.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AppSettings
6 |
7 | appIconPlaceholderGlyphName
8 | paper
9 | appSettingsVersion
10 | 1
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/xcode/package.xcworkspace/xcuserdata/joseadolfot.xcuserdatad/IDEFindNavigatorScopes.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/xcode/package.xcworkspace/xcuserdata/joseadolfot.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/.swiftpm/xcode/package.xcworkspace/xcuserdata/joseadolfot.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/xcode/xcuserdata/joseadolfot.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/.swiftpm/xcode/xcuserdata/joseadolfot.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | LogicBoard.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | AppModule
16 |
17 | primary
18 |
19 |
20 | LogicBoard
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/AND.imageset/AND.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/AND.imageset/AND.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/AND.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AND.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/AppIcon.appiconset/AppIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/AppIcon.appiconset/AppIcon.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AppIcon.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/BUFFER.imageset/BUFFER.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/BUFFER.imageset/BUFFER.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/BUFFER.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "BUFFER.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/InputFalse.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "InputFalse.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/InputFalse.imageset/InputFalse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/InputFalse.imageset/InputFalse.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/InputTrue.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "InputTrue.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/InputTrue.imageset/InputTrue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/InputTrue.imageset/InputTrue.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/NAND.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "NAND.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/NAND.imageset/NAND.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/NAND.imageset/NAND.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/NOR.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "NOR.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/NOR.imageset/NOR.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/NOR.imageset/NOR.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/NOT.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "NOT.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/NOT.imageset/NOT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/NOT.imageset/NOT.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/OR.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "OR.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/OR.imageset/OR.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/OR.imageset/OR.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/OutputFalse.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "OutputFalse.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/OutputFalse.imageset/OutputFalse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/OutputFalse.imageset/OutputFalse.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/OutputTrue.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "OutputTrue.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/OutputTrue.imageset/OutputTrue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/OutputTrue.imageset/OutputTrue.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/XNOR.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "XNOR.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/XNOR.imageset/XNOR.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/XNOR.imageset/XNOR.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/XOR.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "XOR.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/XOR.imageset/XOR.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/XOR.imageset/XOR.png
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/background.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "background.jpg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Assets.xcassets/background.imageset/background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/LogicBoard.swiftpm/Assets.xcassets/background.imageset/background.jpg
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @main
4 | struct MyApp: App {
5 | let envi = LogicEnvironment()
6 | var body: some Scene {
7 | WindowGroup {
8 | BoardView()
9 | .environmentObject(envi)
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/BoardState.swift:
--------------------------------------------------------------------------------
1 | public enum BoardState {
2 | case file, select, add, wire, simulate
3 | }
4 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Circuit/Circuit+Connection.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension Circuit {
4 | public func addConnection(from start: DevicePin,
5 | to end: DevicePin,
6 | points: [CGPoint]) {
7 | guard end.terminal < end.device.inputs.count
8 | && start.terminal < start.device.outputs.count else {
9 | return
10 | }
11 | let connection = Connection(from: start,
12 | to: end,
13 | points: points)
14 | connections.append(connection)
15 | canvasScene.addChild(connection.lineNode)
16 | var startDevice = start.device
17 | var endDevice = end.device
18 |
19 |
20 | startDevice.outcomingConnections[start.terminal]?.append(connection)
21 | endDevice.incomingConnections[end.terminal] = connection
22 | }
23 |
24 | public func addConnection(_ connection: Connection) {
25 | connections.append(connection)
26 | canvasScene.addChild(connection.lineNode)
27 | var startDevice = connection.start.device
28 | var endDevice = connection.end.device
29 |
30 | startDevice.outcomingConnections[connection.start.terminal]?.append(connection)
31 | endDevice.incomingConnections[connection.end.terminal] = connection
32 | }
33 |
34 |
35 | public func remove(_ connection: Connection) {
36 | guard let index = connections.firstIndex(of: connection) else {
37 | return
38 | }
39 | var start = connection.start
40 | var end = connection.end
41 |
42 | guard let startIndex = start.device.outcomingConnections[start.terminal]?.firstIndex(of: connection)
43 | else { return }
44 |
45 | start.device.outcomingConnections[start.terminal]?.remove(at: startIndex)
46 | end.device.incomingConnections[end.terminal] = nil
47 | end.device.pinNodes.enumerated().forEach {
48 | if $0 == end.terminal {
49 | $1.occupied(false)
50 | $1.colorOccupied(false)
51 | }
52 | }
53 |
54 | canvasScene.parentCircuit?.selectedConnection = nil
55 | connection.lineNode.removeFromParent()
56 | connections.remove(at: index)
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Circuit/Circuit+Device.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension Circuit {
4 | public func add(_ device: inout some Device, at position: CGPoint) {
5 | devices.append(device)
6 | device.assignParentCircuit(self)
7 | canvasScene.addDevice(device, at: position)
8 | }
9 |
10 | public func remove(_ device: some Device) {
11 | for (_, connection) in device.incomingConnections {
12 | if let connection = connection {
13 | remove(connection)
14 | }
15 | }
16 |
17 | for (_, connections) in device.outcomingConnections {
18 | for connection in connections {
19 | remove(connection)
20 | }
21 | }
22 |
23 | if let index = devices.firstIndex(where: {$0.id == device.id}) {
24 | device.spriteNode.removeFromParent()
25 | devices.remove(at: index)
26 | }
27 | }
28 |
29 | public func toggleAndSolve(_ pushButton: PushButton) {
30 | pushButton.toggle()
31 | solve(pushButton)
32 | }
33 |
34 | public func resetVisited() {
35 | for i in 0..= currentLevel {
31 | return
32 | }
33 | device.level = level
34 | device.visited = true
35 |
36 | for (_, connections) in device.outcomingConnections {
37 | for connection in connections {
38 | recursiveAssignLevel(start: &connection.end.device, level: level + 1)
39 | }
40 | }
41 | }
42 |
43 | func perLevelSolve() {
44 | let keys = arrangedDevices.keys.sorted {
45 | guard let first = $0, let second = $1 else {
46 | return false
47 | }
48 | return first < second
49 | }
50 |
51 | var queue: [(DevicePin, Bool)] = []
52 |
53 | for key in keys {
54 | guard let key = key else {
55 | guard let devices = arrangedDevices[key] else { continue }
56 | for device in devices {
57 | solve(device)
58 | }
59 | continue
60 | }
61 |
62 | guard let devices = arrangedDevices[key] else { continue }
63 | for device in devices {
64 | device.run()
65 | device.updateWire()
66 | for (_, connections) in device.outcomingConnections {
67 | for connection in connections {
68 | connection.updateColor()
69 | let endDevice = connection.end.device
70 | let endTerminal = connection.end.terminal
71 | endDevice.wireNodes[endTerminal].strokeColorTransition(to: connection.state ? .canvasGreen : .canvasYellow)
72 | if endDevice is Display {
73 | var endDevice = endDevice
74 | endDevice.inputs[0] = connection.state
75 | } else if device.id != endDevice.id {
76 | queue.append((connection.end, connection.state))
77 | }
78 | }
79 | }
80 | }
81 | }
82 |
83 | for item in queue {
84 | var item = item
85 | item.0.device.inputs[item.0.terminal] = item.1
86 | }
87 | resetVisited()
88 | }
89 |
90 | func solve(_ startDevice: any Device) {
91 | recursiveSolve(startDevice)
92 | resetVisited()
93 | }
94 |
95 | func recursiveSolve(_ device: any Device,
96 | path: [any Device] = []) {
97 | device.run()
98 | device.updateWire()
99 |
100 | for (_, connections) in device.outcomingConnections {
101 | for connection in connections {
102 | connection.updateColor()
103 | connection.end.device.updateWire()
104 | if !path.contains(where: {$0.id == device.id}) {
105 | connection.end.device.inputs[connection.end.terminal] = connection.start.device.outputs[connection.start.terminal]
106 | recursiveSolve(connection.end.device, path: path + [device])
107 | }
108 | }
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Circuit/Circuit.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import Combine
3 |
4 | public class Circuit: Nameable, Identifiable, Equatable, ObservableObject {
5 | public var devices: Array = Array()
6 | public var connections: [Connection] = [Connection]()
7 |
8 | public static var count = 0
9 | public var name: String
10 | public var id: String
11 | public var isExample: Bool = false
12 |
13 | public var state: BoardState = .add {
14 | didSet {
15 | canvasScene.state = state
16 | if state == .simulate {
17 | initialSolve()
18 | devices.forEach { $0.updateWire() }
19 | connections.forEach { $0.updateColor() }
20 | } else if oldValue == .simulate {
21 | resetState()
22 | devices.forEach { $0.resetWire() }
23 | connections.forEach { $0.resetColor() }
24 | }
25 | }
26 | }
27 | @Published public var selectedDevice: (any Device)?
28 | @Published public var selectedConnection: Connection?
29 | @Published public var canvasScene: CanvasScene
30 |
31 | public var arrangedDevices: [Int? : [any Device]] = [Int? : [any Device]]()
32 |
33 | public init() {
34 | id = "CIRCUIT" + String(Circuit.count)
35 | Circuit.count += 1
36 | name = id
37 | canvasScene = CanvasScene()
38 | canvasScene.name = id
39 | canvasScene.parentCircuit = self
40 | canvasScene.scaleMode = .resizeFill
41 | canvasScene.backgroundColor = UIColor.canvasBackground
42 | }
43 |
44 | deinit {
45 | print("Deinit circuit")
46 | }
47 |
48 | public func hardReset() {
49 | resetState()
50 | initialSolve()
51 | }
52 |
53 | public func resetState() {
54 | arrangedDevices = [Int? : [any Device]]()
55 | for device in devices {
56 | var device = device
57 | for i in 0.. Bool {
73 | return lhs.id == rhs.id
74 | }
75 |
76 | public func changeState(to state: BoardState) {
77 | self.state = state
78 | }
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Circuit/CircuitData.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import UniformTypeIdentifiers
3 | import SwiftUI
4 |
5 | struct CircuitData: Codable {
6 | var name: String
7 | var deviceDatas: [DeviceData]
8 | var connectionData: [ConnectionData]
9 |
10 | func makeCircuit() -> Circuit {
11 | var circuit = Circuit()
12 | circuit.changeName(to: name)
13 |
14 | for deviceData in deviceDatas {
15 | var device = deviceData.makeDevice()
16 | circuit.add(&device, at: .zero)
17 | device.spriteNode.position = deviceData.location
18 | }
19 |
20 | for connectionData in connectionData {
21 | guard let startPoint = connectionData.points.first,
22 | let endPoint = connectionData.points.last else {
23 | continue
24 | }
25 |
26 | let startNodes = circuit.canvasScene.nodes(at: startPoint)
27 | let endNodes = circuit.canvasScene.nodes(at: endPoint)
28 |
29 | guard let startPinNode = startNodes.first(where: { $0.name?.contains("PIN") ?? false }),
30 | let endPinNode = endNodes.first(where: { $0.name?.contains("PIN") ?? false }) else {
31 | continue
32 | }
33 |
34 | guard let startDevice = startPinNode.parent?.parentDevice,
35 | let endDevice = endPinNode.parent?.parentDevice else {
36 | continue
37 | }
38 |
39 | guard let startTerminal = startPinNode.terminal,
40 | let endTerminal = endPinNode.terminal else {
41 | continue
42 | }
43 |
44 | let startDevicePin = (startDevice, startTerminal)
45 | let endDevicePin = (endDevice, endTerminal)
46 |
47 | endPinNode.occupied(true)
48 | endPinNode.colorOccupied(true)
49 |
50 | let connection = Connection(from: startDevicePin, to: endDevicePin, points: connectionData.points)
51 | circuit.addConnection(connection)
52 | }
53 |
54 | return circuit
55 | }
56 | }
57 |
58 | struct DeviceData: Codable {
59 | let deviceName : DeviceName
60 | let location: CGPoint
61 |
62 | func makeDevice() -> any Device {
63 | let device = deviceName.deviceNameToDevice()
64 | return device
65 | }
66 | }
67 |
68 | struct ConnectionData: Codable {
69 | let points: [CGPoint]
70 | }
71 |
72 |
73 | extension Circuit {
74 | func makeCircuitData() -> CircuitData {
75 | var deviceDatas = [DeviceData]()
76 | var connectionDatas = [ConnectionData]()
77 | for device in self.devices {
78 | deviceDatas.append(DeviceData(deviceName: DeviceName.deviceStringToDeviceName(string: device.deviceName), location: device.spriteNode.position))
79 | }
80 | for connection in self.connections {
81 | connectionDatas.append(ConnectionData(points: connection.points))
82 | }
83 |
84 | return CircuitData(name: self.name, deviceDatas: deviceDatas, connectionData: connectionDatas)
85 | }
86 | }
87 |
88 | extension UTType {
89 | static let circ = UTType(exportedAs: "com.joseadolfo.circ", conformingTo: .json)
90 | }
91 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Circuit/CircuitDocument.swift:
--------------------------------------------------------------------------------
1 | import UniformTypeIdentifiers
2 | import SwiftUI
3 |
4 | struct CircuitDocument: FileDocument {
5 | static var readableContentTypes: [UTType] = [.circ, .json]
6 |
7 | var circuitData: CircuitData
8 |
9 | init(circuitData: CircuitData) {
10 | self.circuitData = circuitData
11 | }
12 |
13 | init(configuration: ReadConfiguration) throws {
14 | guard let data = configuration.file.regularFileContents else {
15 | throw CocoaError(.fileReadCorruptFile)
16 | }
17 | circuitData = try JSONDecoder().decode(CircuitData.self, from: data)
18 | }
19 |
20 | func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
21 | let data = try JSONEncoder().encode(self.circuitData)
22 | let fileWrapper = FileWrapper(regularFileWithContents: data)
23 | return fileWrapper
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Connection/Connection.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Connection: Identifiable, Equatable {
4 | var start: DevicePin
5 | var end: DevicePin
6 |
7 | public static var count = 0
8 | public var id: String
9 |
10 | public var points: [CGPoint] = []
11 | public var lineNode: SKShapeNode = SKShapeNode()
12 |
13 | public var state: Bool { start.device.outputs[start.terminal] }
14 |
15 |
16 | public init(from start: DevicePin,
17 | to end: DevicePin,
18 | points: [CGPoint]) {
19 | self.start = start
20 | self.end = end
21 | self.points = points
22 |
23 | Connection.count += 1
24 | id = "CONNECTION" + String(Connection.count)
25 |
26 | lineNode.path = Self.makePath(points: points)
27 | lineNode.strokeColor = SKColor.darkGray
28 | lineNode.lineWidth = 8
29 | lineNode.lineJoin = .round
30 | lineNode.lineCap = .round
31 | lineNode.zPosition = -10000
32 | lineNode.name = "CONNECTION"
33 | lineNode.userData = NSMutableDictionary()
34 | lineNode.userData?.setValue(self, forKey: "connection")
35 | }
36 |
37 | public static func == (lhs: Connection, rhs: Connection) -> Bool {
38 | return lhs.id == rhs.id
39 | }
40 |
41 | public static func makePath(points: [CGPoint]) -> CGMutablePath {
42 | let path = CGMutablePath()
43 | points.enumerated().forEach { (i, point) in
44 | i == 0 ? path.move(to: point) : path.addLine(to: point)
45 | }
46 | return path
47 | }
48 |
49 | public func updateStartPoint(to point: CGPoint) {
50 | points[0] = point
51 | let path = Self.makePath(points: points)
52 | lineNode.path = path
53 | }
54 |
55 | public func updateEndPoint(to point: CGPoint) {
56 | let lastIndex = points.count - 1
57 | points[lastIndex] = point
58 | let path = Self.makePath(points: points)
59 | lineNode.path = path
60 | }
61 |
62 | public func updateColor() {
63 | lineNode.strokeColorTransition(to: state ? .canvasGreen : .canvasYellow, duration: 1/60)
64 | }
65 |
66 | public func resetColor() {
67 | lineNode.strokeColorTransition(to: .darkGray)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Device+CanvasScene.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension Device {
4 | func updateWire() {
5 | wireNodes.forEach { wireNode in
6 | guard let terminal = wireNode.terminal else {
7 | return
8 | }
9 | if wireNode.getPinType() == .input {
10 | guard let incomingConnection = incomingConnections[terminal],
11 | let incomingConnection = incomingConnection else {
12 | return
13 | }
14 | wireNode.strokeColorTransition(to: incomingConnection.state ? .canvasGreen : .canvasYellow, duration: 1/60)
15 | } else {
16 | wireNode.strokeColorTransition(to: outputs[terminal] ? .canvasGreen : .canvasYellow, duration: 1/60)
17 | }
18 | }
19 | }
20 |
21 | func resetWire() {
22 | wireNodes.forEach { wireNode in
23 | wireNode.strokeColorTransition(to: .darkGray)
24 |
25 | }
26 | }
27 |
28 | static func tagPath() -> CGMutablePath {
29 | let path = CGMutablePath()
30 | path.move(to: CGPoint(x: -12, y: -8))
31 | path.addLine(to: CGPoint(x: 0, y: -8))
32 | path.addLine(to: CGPoint(x: 12, y: 0))
33 | path.addLine(to: CGPoint(x: 0, y: 8))
34 | path.addLine(to: CGPoint(x: -12, y: 8))
35 | path.addLine(to: CGPoint(x: -12, y: -8))
36 | return path
37 | }
38 |
39 | static func twoInputGateSprite(wires: inout [SKShapeNode], pins: inout [SKSpriteNode]) -> SKSpriteNode {
40 | let spriteNode = SKSpriteNode()
41 |
42 | let wireNode1 = SKShapeNode()
43 | let wirePath1 = CGMutablePath()
44 | wirePath1.move(to: CGPoint(x: -128, y: 64))
45 | wirePath1.addLine(to: CGPoint(x: 0, y: 64))
46 | wireNode1.path = wirePath1
47 | wireNode1.strokeColor = SKColor.darkGray
48 | wireNode1.lineWidth = 8
49 | wireNode1.name = "WIRE1"
50 | wireNode1.userData = NSMutableDictionary()
51 | wireNode1.userData?.setValue(PinType.input, forKey: "type")
52 | wireNode1.userData?.setValue(Int(0), forKey: "terminal")
53 | wires.append(wireNode1)
54 | spriteNode.addChild(wireNode1)
55 |
56 | let wireNode2 = SKShapeNode()
57 | let wirePath2 = CGMutablePath()
58 | wirePath2.move(to: CGPoint(x: -128, y: -64))
59 | wirePath2.addLine(to: CGPoint(x: 0, y: -64))
60 | wireNode2.path = wirePath2
61 | wireNode2.strokeColor = SKColor.darkGray
62 | wireNode2.lineWidth = 8
63 | wireNode2.name = "WIRE2"
64 | wireNode2.userData = NSMutableDictionary()
65 | wireNode2.userData?.setValue(PinType.input, forKey: "type")
66 | wireNode2.userData?.setValue(Int(1), forKey: "terminal")
67 | wires.append(wireNode2)
68 | spriteNode.addChild(wireNode2)
69 |
70 | let wireNode3 = SKShapeNode()
71 | let wirePath3 = CGMutablePath()
72 | wirePath3.move(to: CGPoint(x: 0, y: 0))
73 | wirePath3.addLine(to: CGPoint(x: 128, y: 0))
74 | wireNode3.path = wirePath3
75 | wireNode3.strokeColor = SKColor.darkGray
76 | wireNode3.lineWidth = 8
77 | wireNode3.name = "WIRE3"
78 | wireNode3.userData = NSMutableDictionary()
79 | wireNode3.userData?.setValue(PinType.output, forKey: "type")
80 | wireNode3.userData?.setValue(Int(0), forKey: "terminal")
81 | wires.append(wireNode3)
82 | spriteNode.addChild(wireNode3)
83 |
84 | let pinNode1 = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
85 | pinNode1.position = .init(x: -128, y: 64)
86 | pinNode1.name = "PIN1"
87 | pinNode1.userData = NSMutableDictionary()
88 | pinNode1.userData?.setValue(PinType.input, forKey: "type")
89 | pinNode1.userData?.setValue(Int(0), forKey: "terminal")
90 | pins.append(pinNode1)
91 | spriteNode.addChild(pinNode1)
92 |
93 | let pinShapeNode1 = SKShapeNode(rect: .init(x: -8, y: -8, width: 16, height: 16), cornerRadius: 4)
94 | pinShapeNode1.fillColor = SKColor.gray
95 | pinShapeNode1.strokeColor = SKColor.darkGray
96 | pinShapeNode1.lineWidth = 4
97 | pinShapeNode1.lineJoin = .round
98 | pinNode1.addChild(pinShapeNode1)
99 |
100 | let pinNode2 = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
101 | pinNode2.position = .init(x: -128, y: -64)
102 | pinNode2.name = "PIN2"
103 | pinNode2.userData = NSMutableDictionary()
104 | pinNode2.userData?.setValue(PinType.input, forKey: "type")
105 | pinNode2.userData?.setValue(Int(1), forKey: "terminal")
106 | pins.append(pinNode2)
107 | spriteNode.addChild(pinNode2)
108 |
109 | let pinShapeNode2 = SKShapeNode(rect: .init(x: -8, y: -8, width: 16, height: 16), cornerRadius: 4)
110 | pinShapeNode2.fillColor = SKColor.gray
111 | pinShapeNode2.strokeColor = SKColor.darkGray
112 | pinShapeNode2.lineWidth = 4
113 | pinShapeNode2.lineJoin = .round
114 | pinNode2.addChild(pinShapeNode2)
115 |
116 | let pinNode3 = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
117 | pinNode3.position = .init(x: 128, y: 0)
118 | pinNode3.name = "PIN3"
119 | pinNode3.userData = NSMutableDictionary()
120 | pinNode3.userData?.setValue(PinType.output, forKey: "type")
121 | pinNode3.userData?.setValue(false, forKey: "occupied")
122 | pinNode3.userData?.setValue(Int(0), forKey: "terminal")
123 | pins.append(pinNode3)
124 | spriteNode.addChild(pinNode3)
125 |
126 | let pinShapeNode3 = SKShapeNode(rect: .init(x: -8, y: -8, width: 16, height: 16), cornerRadius: 4)
127 | pinShapeNode3.fillColor = SKColor.gray
128 | pinShapeNode3.strokeColor = SKColor.darkGray
129 | pinShapeNode3.lineWidth = 4
130 | pinShapeNode3.lineJoin = .round
131 | pinNode3.addChild(pinShapeNode3)
132 |
133 | return spriteNode
134 | }
135 | }
136 |
137 | extension SKNode {
138 | var terminal: Int? {
139 | guard let terminal = self.userData?.value(forKey: "terminal") as? Int else { return nil }
140 | return terminal
141 | }
142 |
143 | var parentDevice: (any Device)? {
144 | guard let parentDevice = self.userData?.value(forKey: "parentDevice") as? any Device else { return nil }
145 | return parentDevice
146 | }
147 |
148 | var parentConnection: Connection? {
149 | guard let connection = self.userData?.value(forKey: "connection") as? Connection else { return nil }
150 | return connection
151 | }
152 |
153 | func getPinType() -> PinType? {
154 | self.userData?.value(forKey: "type") as? PinType
155 | }
156 |
157 | func isOccupied() -> Bool {
158 | guard let occupied = self.userData?.value(forKey: "occupied") as? Bool else { return false }
159 | return occupied
160 | }
161 |
162 | func occupied(_ isOcuppied: Bool) {
163 | self.userData?.setValue(isOcuppied, forKey: "occupied")
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Device.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public protocol Device: Identifiable {
4 | var parentCircuit: Circuit? { get set }
5 |
6 | var inputs: [Bool] { get set }
7 | var outputs: [Bool] { get set }
8 |
9 | var deviceName: String { get }
10 | var imageName: String { get }
11 |
12 | static var count: Int { get set }
13 | var id: String { get set }
14 |
15 | var incomingConnections: [Int: Connection?] { get set }
16 | var outcomingConnections: [Int: [Connection]] { get set }
17 |
18 | var visited: Bool { get set }
19 | var level: Int? { get set }
20 |
21 | var spriteNode: SKSpriteNode { get set }
22 | var imageNode: SKSpriteNode { get set }
23 | var wireNodes: [SKShapeNode] { get set }
24 | var pinNodes: [SKSpriteNode] { get set }
25 |
26 | func run()
27 | }
28 |
29 | extension Device {
30 | var inputCount: Int { inputs.count }
31 | var outputCount: Int { outputs.count }
32 |
33 | static func ==(lhs: any Device, rhs: any Device) -> Bool {
34 | return lhs.id == rhs.id
35 | }
36 |
37 | mutating func assignParentCircuit(_ parent: Circuit) {
38 | self.parentCircuit = parent
39 | }
40 |
41 | mutating func resetVisited() {
42 | visited = false
43 | }
44 | }
45 |
46 | protocol Input: Device { }
47 |
48 | protocol Output: Device { }
49 |
50 | public protocol Gate: Device { }
51 |
52 | public protocol Expandable: Gate { }
53 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/DeviceName.swift:
--------------------------------------------------------------------------------
1 | public enum DeviceName: String, Identifiable, Codable {
2 | case AND, NAND
3 | case OR, NOR
4 | case XOR, XNOR
5 | case NOT, BUFFER
6 | case PUSHBUTTON
7 | case DISPLAY
8 |
9 | public var id: String { deviceName }
10 |
11 | public var imageName: String {
12 | switch self {
13 | case .AND:
14 | return "AND"
15 | case .NAND:
16 | return "NAND"
17 | case .OR:
18 | return "OR"
19 | case .NOR:
20 | return "NOR"
21 | case .XOR:
22 | return "XOR"
23 | case .XNOR:
24 | return "XNOR"
25 | case .NOT:
26 | return "NOT"
27 | case .BUFFER:
28 | return "BUFFER"
29 | case .PUSHBUTTON:
30 | return "InputFalse"
31 | case .DISPLAY:
32 | return "OutputFalse"
33 | }
34 | }
35 |
36 | public var deviceName: String {
37 | switch self {
38 | case .AND:
39 | return "AND GATE"
40 | case .NAND:
41 | return "NAND GATE"
42 | case .OR:
43 | return "OR GATE"
44 | case .NOR:
45 | return "NOR GATE"
46 | case .XOR:
47 | return "XOR GATE"
48 | case .XNOR:
49 | return "XNOR GATE"
50 | case .NOT:
51 | return "NOT GATE"
52 | case .BUFFER:
53 | return "BUFFER"
54 | case .PUSHBUTTON:
55 | return "BUTTON"
56 | case .DISPLAY:
57 | return "DISPLAY"
58 | }
59 | }
60 |
61 | public static func deviceStringToDeviceName(string: String) -> DeviceName {
62 | switch string {
63 | case "AND GATE":
64 | return .AND
65 | case "NAND GATE":
66 | return .NAND
67 | case "OR GATE":
68 | return .OR
69 | case "NOR GATE":
70 | return .NOR
71 | case "XOR GATE":
72 | return .XOR
73 | case "XNOR GATE":
74 | return .XNOR
75 | case "NOT GATE":
76 | return .NOT
77 | case "BUFFER":
78 | return .BUFFER
79 | case "DISPLAY":
80 | return .DISPLAY
81 | default:
82 | return .PUSHBUTTON
83 | }
84 | }
85 |
86 | public func deviceNameToDevice() -> any Device {
87 | switch self {
88 | case .AND:
89 | return And()
90 | case .NAND:
91 | return Nand()
92 | case .OR:
93 | return Or()
94 | case .NOR:
95 | return Nor()
96 | case .XOR:
97 | return Xor()
98 | case .XNOR:
99 | return Xnor()
100 | case .NOT:
101 | return Not()
102 | case .BUFFER:
103 | return Buffer()
104 | case .DISPLAY:
105 | return Display()
106 | case .PUSHBUTTON:
107 | return PushButton()
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/DevicePin.swift:
--------------------------------------------------------------------------------
1 | public typealias DevicePin = (device: any Device, terminal: Int)
2 |
3 | public enum PinType {
4 | case input, output
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Gate/And.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class And: Gate, Expandable {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "AND GATE"
7 | public var imageName: String = "AND"
8 |
9 | public var inputs: [Bool] = [false, false]
10 | public var outputs: [Bool] = [false]
11 |
12 | public var incomingConnections: [Int: Connection?] = [0: nil,
13 | 1: nil]
14 | public var outcomingConnections: [Int: [Connection]] = [0: []]
15 |
16 | public static var count = 0
17 | public var id: String
18 |
19 | public var visited: Bool = false
20 | public var level: Int?
21 |
22 | public var spriteNode: SKSpriteNode
23 | public var imageNode: SKSpriteNode
24 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
25 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
26 |
27 | public init() {
28 | Self.count += 1
29 | id = "AND" + String(Self.count)
30 |
31 | spriteNode = Self.twoInputGateSprite(wires: &wireNodes, pins: &pinNodes)
32 |
33 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
34 | imageNode.size = CGSize(width: 256, height: 256)
35 | imageNode.position = .init(x: 8, y: 0)
36 | imageNode.name = "MAIN-" + id
37 |
38 | spriteNode.addChild(imageNode)
39 | spriteNode.userData = NSMutableDictionary()
40 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
41 | }
42 |
43 | public func run() {
44 | outputs[0] = inputs.allSatisfy({$0})
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Gate/Buffer.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Buffer: Gate {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "BUFFER"
7 | public var imageName: String = "BUFFER"
8 |
9 | public var inputs: [Bool] = [false]
10 | public var outputs: [Bool] = [true]
11 |
12 | public var incomingConnections: [Int: Connection?] = [0: nil]
13 | public var outcomingConnections: [Int: [Connection]] = [0: []]
14 |
15 | public static var count = 0
16 | public var id: String
17 |
18 | public var visited: Bool = false
19 | public var level: Int?
20 |
21 | public var spriteNode: SKSpriteNode = SKSpriteNode()
22 | public var imageNode: SKSpriteNode
23 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
24 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
25 |
26 | public init() {
27 | Self.count += 1
28 | id = "BUFFER" + String(Self.count)
29 |
30 | let wireNode1 = SKShapeNode()
31 | let wirePath1 = CGMutablePath()
32 | wirePath1.move(to: CGPoint(x: -64, y: 0))
33 | wirePath1.addLine(to: CGPoint(x: 0, y: 0))
34 | wireNode1.path = wirePath1
35 | wireNode1.strokeColor = SKColor.darkGray
36 | wireNode1.lineWidth = 8
37 | wireNode1.name = "WIRE1"
38 | wireNode1.userData = NSMutableDictionary()
39 | wireNode1.userData?.setValue(PinType.input, forKey: "type")
40 | wireNode1.userData?.setValue(Int(0), forKey: "terminal")
41 |
42 | wireNodes.append(wireNode1)
43 | spriteNode.addChild(wireNode1)
44 |
45 | let wireNode2 = SKShapeNode()
46 | let wirePath2 = CGMutablePath()
47 | wirePath2.move(to: CGPoint(x: 0, y: 0))
48 | wirePath2.addLine(to: CGPoint(x: 64, y: 0))
49 | wireNode2.path = wirePath2
50 | wireNode2.strokeColor = SKColor.darkGray
51 | wireNode2.lineWidth = 8
52 | wireNode2.name = "WIRE2"
53 | wireNode2.userData = NSMutableDictionary()
54 | wireNode2.userData?.setValue(PinType.output, forKey: "type")
55 | wireNode2.userData?.setValue(Int(0), forKey: "terminal")
56 |
57 | wireNodes.append(wireNode2)
58 | spriteNode.addChild(wireNode2)
59 |
60 | let pinNode1 = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
61 | pinNode1.position = .init(x: -64, y: 0)
62 | pinNode1.name = "PIN1"
63 | pinNode1.userData = NSMutableDictionary()
64 | pinNode1.userData?.setValue(PinType.input, forKey: "type")
65 | pinNode1.userData?.setValue(Int(0), forKey: "terminal")
66 | pinNodes.append(pinNode1)
67 | spriteNode.addChild(pinNode1)
68 |
69 | let pinShapeNode1 = SKShapeNode(rect: .init(origin: .init(x: -8, y: -8), size: .init(width: 16, height: 16)), cornerRadius: 4)
70 | pinShapeNode1.fillColor = SKColor.gray
71 | pinShapeNode1.strokeColor = SKColor.darkGray
72 | pinShapeNode1.lineWidth = 4
73 | pinNode1.addChild(pinShapeNode1)
74 |
75 | let pinNode2 = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
76 | pinNode2.position = .init(x: 64, y: 0)
77 | pinNode2.name = "PIN2"
78 | pinNode2.userData = NSMutableDictionary()
79 | pinNode2.userData?.setValue(PinType.output, forKey: "type")
80 | pinNode2.userData?.setValue(false, forKey: "occupied")
81 | pinNode2.userData?.setValue(Int(0), forKey: "terminal")
82 | pinNodes.append(pinNode2)
83 | spriteNode.addChild(pinNode2)
84 |
85 | let pinShapeNode2 = SKShapeNode(rect: .init(origin: .init(x: -8, y: -8), size: .init(width: 16, height: 16)), cornerRadius: 4)
86 | pinShapeNode2.fillColor = SKColor.gray
87 | pinShapeNode2.strokeColor = SKColor.darkGray
88 | pinShapeNode2.lineWidth = 4
89 | pinShapeNode2.lineJoin = .round
90 | pinNode2.addChild(pinShapeNode2)
91 |
92 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
93 | imageNode.size = CGSize(width: 128, height: 128)
94 | imageNode.position = .init(x: 8, y: 0)
95 | imageNode.name = "MAIN-" + id
96 |
97 | spriteNode.addChild(imageNode)
98 | spriteNode.userData = NSMutableDictionary()
99 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
100 | }
101 |
102 | public func run() {
103 | outputs[0] = inputs[0]
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Gate/Nand.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Nand: Gate, Expandable {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "NAND GATE"
7 | public var imageName: String = "NAND"
8 |
9 | public var inputs: [Bool] = [false, false]
10 | public var outputs: [Bool] = [true]
11 |
12 | public var incomingConnections: [Int: Connection?] = [0: nil,
13 | 1: nil]
14 | public var outcomingConnections: [Int: [Connection]] = [0: []]
15 |
16 | public static var count = 0
17 | public var id: String
18 |
19 | public var visited: Bool = false
20 | public var level: Int?
21 |
22 | public var spriteNode: SKSpriteNode = SKSpriteNode()
23 | public var imageNode: SKSpriteNode
24 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
25 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
26 |
27 | public init() {
28 | Self.count += 1
29 | id = "NAND" + String(Self.count)
30 |
31 | spriteNode = Self.twoInputGateSprite(wires: &wireNodes, pins: &pinNodes)
32 |
33 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
34 | imageNode.size = CGSize(width: 256, height: 256)
35 | imageNode.name = "MAIN-" + id
36 |
37 | spriteNode.addChild(imageNode)
38 | spriteNode.userData = NSMutableDictionary()
39 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
40 | }
41 |
42 | public func run() {
43 | outputs[0] = inputs.contains(false)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Gate/Nor.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Nor: Gate, Expandable {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "NOR GATE"
7 | public var imageName: String = "NOR"
8 |
9 | public var inputs: [Bool] = [false, false]
10 | public var outputs: [Bool] = [true]
11 |
12 | public var incomingConnections: [Int: Connection?] = [0: nil,
13 | 1: nil]
14 | public var outcomingConnections: [Int: [Connection]] = [0: []]
15 |
16 | public static var count = 0
17 | public var id: String
18 |
19 | public var visited: Bool = false
20 | public var level: Int?
21 |
22 | public var spriteNode: SKSpriteNode = SKSpriteNode()
23 | public var imageNode: SKSpriteNode
24 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
25 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
26 |
27 | init() {
28 | Self.count += 1
29 | id = "NOR" + String(Self.count)
30 |
31 | spriteNode = Self.twoInputGateSprite(wires: &wireNodes, pins: &pinNodes)
32 |
33 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
34 | imageNode.size = CGSize(width: 256, height: 256)
35 | imageNode.name = "MAIN-" + id
36 |
37 | spriteNode.addChild(imageNode)
38 | spriteNode.userData = NSMutableDictionary()
39 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
40 | }
41 |
42 | public func run() {
43 | outputs[0] = inputs.allSatisfy({$0 == false})
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Gate/Not.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Not: Gate {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "NOT GATE"
7 | public var imageName: String = "NOT"
8 |
9 | public var inputs: [Bool] = [false]
10 | public var outputs: [Bool] = [true]
11 |
12 | public var incomingConnections: [Int: Connection?] = [0: nil]
13 | public var outcomingConnections: [Int: [Connection]] = [0: []]
14 |
15 | public static var count = 0
16 | public var id: String
17 |
18 | public var visited: Bool = false
19 | public var level: Int?
20 |
21 | public var spriteNode: SKSpriteNode = SKSpriteNode()
22 | public var imageNode: SKSpriteNode
23 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
24 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
25 |
26 | public init() {
27 | Self.count += 1
28 | id = "NOT" + String(Self.count)
29 |
30 | let wireNode1 = SKShapeNode()
31 | let wirePath1 = CGMutablePath()
32 | wirePath1.move(to: CGPoint(x: -64, y: 0))
33 | wirePath1.addLine(to: CGPoint(x: 0, y: 0))
34 | wireNode1.path = wirePath1
35 | wireNode1.strokeColor = SKColor.darkGray
36 | wireNode1.lineWidth = 8
37 | wireNode1.name = "WIRE1"
38 | wireNode1.userData = NSMutableDictionary()
39 | wireNode1.userData?.setValue(PinType.input, forKey: "type")
40 | wireNode1.userData?.setValue(Int(0), forKey: "terminal")
41 |
42 | wireNodes.append(wireNode1)
43 | spriteNode.addChild(wireNode1)
44 |
45 | let wireNode2 = SKShapeNode()
46 | let wirePath2 = CGMutablePath()
47 | wirePath2.move(to: CGPoint(x: 0, y: 0))
48 | wirePath2.addLine(to: CGPoint(x: 64, y: 0))
49 | wireNode2.path = wirePath2
50 | wireNode2.strokeColor = SKColor.darkGray
51 | wireNode2.lineWidth = 8
52 | wireNode2.name = "WIRE2"
53 | wireNode2.userData = NSMutableDictionary()
54 | wireNode2.userData?.setValue(PinType.output, forKey: "type")
55 | wireNode2.userData?.setValue(Int(0), forKey: "terminal")
56 |
57 | wireNodes.append(wireNode2)
58 | spriteNode.addChild(wireNode2)
59 |
60 | let pinNode1 = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
61 | pinNode1.position = .init(x: -64, y: 0)
62 | pinNode1.name = "PIN1"
63 | pinNode1.userData = NSMutableDictionary()
64 | pinNode1.userData?.setValue(PinType.input, forKey: "type")
65 | pinNode1.userData?.setValue(Int(0), forKey: "terminal")
66 | pinNodes.append(pinNode1)
67 | spriteNode.addChild(pinNode1)
68 |
69 | let pinShapeNode1 = SKShapeNode(rect: .init(origin: .init(x: -8, y: -8), size: .init(width: 16, height: 16)), cornerRadius: 4)
70 | pinShapeNode1.fillColor = SKColor.gray
71 | pinShapeNode1.strokeColor = SKColor.darkGray
72 | pinShapeNode1.lineWidth = 4
73 | pinNode1.addChild(pinShapeNode1)
74 |
75 | let pinNode2 = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
76 | pinNode2.position = .init(x: 64, y: 0)
77 | pinNode2.name = "PIN2"
78 | pinNode2.userData = NSMutableDictionary()
79 | pinNode2.userData?.setValue(PinType.output, forKey: "type")
80 | pinNode2.userData?.setValue(false, forKey: "occupied")
81 | pinNode2.userData?.setValue(Int(0), forKey: "terminal")
82 | pinNodes.append(pinNode2)
83 | spriteNode.addChild(pinNode2)
84 |
85 | let pinShapeNode2 = SKShapeNode(rect: .init(origin: .init(x: -8, y: -8), size: .init(width: 16, height: 16)), cornerRadius: 4)
86 | pinShapeNode2.fillColor = SKColor.gray
87 | pinShapeNode2.strokeColor = SKColor.darkGray
88 | pinShapeNode2.lineWidth = 4
89 | pinShapeNode2.lineJoin = .round
90 | pinNode2.addChild(pinShapeNode2)
91 |
92 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
93 | imageNode.size = CGSize(width: 128, height: 128)
94 | imageNode.name = "MAIN-" + id
95 |
96 | spriteNode.addChild(imageNode)
97 | spriteNode.userData = NSMutableDictionary()
98 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
99 | }
100 |
101 | public func run() {
102 | outputs[0] = !inputs[0]
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Gate/Or.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Or: Gate, Expandable {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "OR GATE"
7 | public var imageName: String = "OR"
8 |
9 | public var inputs: [Bool] = [false, false]
10 | public var outputs: [Bool] = [false]
11 |
12 | public var incomingConnections: [Int: Connection?] = [0: nil,
13 | 1: nil]
14 | public var outcomingConnections: [Int: [Connection]] = [0: []]
15 |
16 | public static var count = 0
17 | public var id: String
18 |
19 | public var visited: Bool = false
20 | public var level: Int?
21 |
22 | public var spriteNode: SKSpriteNode = SKSpriteNode()
23 | public var imageNode: SKSpriteNode
24 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
25 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
26 |
27 | public init() {
28 | Self.count += 1
29 | id = "OR" + String(Self.count)
30 |
31 | spriteNode = Self.twoInputGateSprite(wires: &wireNodes, pins: &pinNodes)
32 |
33 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
34 | imageNode.size = CGSize(width: 256, height: 256)
35 | imageNode.position = .init(x: 4, y: 0)
36 | imageNode.name = "MAIN-" + id
37 |
38 | spriteNode.addChild(imageNode)
39 | spriteNode.userData = NSMutableDictionary()
40 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
41 | }
42 |
43 | public func run() {
44 | outputs[0] = inputs.contains(true)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Gate/Xnor.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Xnor: Gate, Expandable {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "XNOR GATE"
7 | public var imageName: String = "XNOR"
8 |
9 | public var inputs: [Bool] = [false, false]
10 | public var outputs: [Bool] = [true]
11 |
12 | public var incomingConnections: [Int: Connection?] = [0: nil,
13 | 1: nil]
14 | public var outcomingConnections: [Int: [Connection]] = [0: []]
15 |
16 | public static var count = 0
17 | public var id: String
18 |
19 | public var visited: Bool = false
20 | public var level: Int?
21 |
22 | public var spriteNode: SKSpriteNode = SKSpriteNode()
23 | public var imageNode: SKSpriteNode
24 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
25 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
26 |
27 | public init() {
28 | Self.count += 1
29 | id = "XNOR" + String(Self.count)
30 |
31 | spriteNode = Self.twoInputGateSprite(wires: &wireNodes, pins: &pinNodes)
32 |
33 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
34 | imageNode.size = CGSize(width: 256, height: 256)
35 | imageNode.name = "MAIN-" + id
36 |
37 | spriteNode.addChild(imageNode)
38 | spriteNode.userData = NSMutableDictionary()
39 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
40 | }
41 |
42 |
43 | public func run() {
44 | outputs[0] = inputs.filter({$0}).count % 2 == 0
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Gate/Xor.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Xor: Gate, Expandable {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "XOR GATE"
7 | public var imageName: String = "XOR"
8 |
9 | public var inputs: [Bool] = [false, false]
10 | public var outputs: [Bool] = [false]
11 |
12 | public var incomingConnections: [Int: Connection?] = [0: nil,
13 | 1: nil]
14 | public var outcomingConnections: [Int: [Connection]] = [0: []]
15 |
16 | public static var count = 0
17 | public var id: String
18 |
19 | public var visited: Bool = false
20 | public var level: Int?
21 |
22 | public var spriteNode: SKSpriteNode = SKSpriteNode()
23 | public var imageNode: SKSpriteNode
24 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
25 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
26 |
27 | public init() {
28 | Self.count += 1
29 | id = "XOR" + String(Self.count)
30 |
31 | spriteNode = Self.twoInputGateSprite(wires: &wireNodes, pins: &pinNodes)
32 |
33 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
34 | imageNode.size = CGSize(width: 256, height: 256)
35 | imageNode.position = .init(x: 4, y: 0)
36 | imageNode.name = "MAIN-" + id
37 |
38 | spriteNode.addChild(imageNode)
39 | spriteNode.userData = NSMutableDictionary()
40 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
41 | }
42 |
43 | public func run() {
44 | outputs[0] = inputs.filter({$0}).count % 2 == 1
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Input/PushButton.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class PushButton: Input {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "PUSHBUTTON"
7 | public var imageName: String = "InputFalse"
8 |
9 | public var inputs: [Bool] = []
10 | public var outputs: [Bool] = [false] {
11 | didSet {
12 | if oldValue[0] != outputs[0] {
13 | imageNode.texture = outputs[0] ? .onButtonTexture : .offButtonTexture
14 | }
15 | }
16 | }
17 |
18 | public var state: Bool { outputs[0] }
19 |
20 | public var incomingConnections: [Int: Connection?] = [Int: Connection?]()
21 | public var outcomingConnections: [Int: [Connection]] = [0: []]
22 |
23 | public static var count = 0
24 | public var id: String
25 |
26 | public var visited: Bool = false
27 | public var level: Int?
28 |
29 | public var spriteNode: SKSpriteNode = SKSpriteNode()
30 | public var imageNode: SKSpriteNode
31 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
32 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
33 |
34 | public init() {
35 | Self.count += 1
36 | id = "IN" + String(Self.count)
37 |
38 | let wireNode = SKShapeNode()
39 | let wirePath = CGMutablePath()
40 | wirePath.move(to: CGPoint(x: 0, y: 0))
41 | wirePath.addLine(to: CGPoint(x: 64, y: 0))
42 | wireNode.path = wirePath
43 | wireNode.strokeColor = SKColor.darkGray
44 | wireNode.userData = NSMutableDictionary()
45 | wireNode.userData?.setValue(PinType.output, forKey: "type")
46 | wireNode.userData?.setValue(Int(0), forKey: "terminal")
47 | wireNode.lineWidth = 8
48 | wireNode.name = "WIRE1"
49 |
50 | wireNodes.append(wireNode)
51 | spriteNode.addChild(wireNode)
52 |
53 | let pinNode = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
54 | pinNode.position = .init(x: 64, y: 0)
55 | pinNode.name = "PIN1"
56 | pinNode.userData = NSMutableDictionary()
57 | pinNode.userData?.setValue(PinType.output, forKey: "type")
58 | pinNode.userData?.setValue(Int(0), forKey: "terminal")
59 | pinNodes.append(pinNode)
60 | spriteNode.addChild(pinNode)
61 |
62 | let pinShapeNode = SKShapeNode(rect: .init(origin: .init(x: -8, y: -8), size: .init(width: 16, height: 16)), cornerRadius: 4)
63 | pinShapeNode.fillColor = SKColor.gray
64 | pinShapeNode.name = "SHAPE"
65 | pinShapeNode.strokeColor = SKColor.darkGray
66 | pinShapeNode.lineWidth = 4
67 | pinShapeNode.lineJoin = .round
68 | pinNode.addChild(pinShapeNode)
69 |
70 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
71 | imageNode.size = CGSize(width: 128, height: 128)
72 | imageNode.name = "MAIN-" + id
73 |
74 | spriteNode.addChild(imageNode)
75 | spriteNode.userData = NSMutableDictionary()
76 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
77 | }
78 |
79 | public func run() {
80 | }
81 |
82 | public func toggle() {
83 | outputs[0] = !outputs[0]
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Components/Device/Output/Display.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | public class Display: Output {
4 | public var parentCircuit: Circuit?
5 |
6 | public var deviceName: String = "DISPLAY"
7 | public var imageName: String = "OutputFalse"
8 |
9 | public var inputs: [Bool] = [false]
10 | public var outputs: [Bool] = []
11 | public var state: Bool { inputs[0] }
12 |
13 | public var incomingConnections: [Int: Connection?] = [0: nil]
14 | public var outcomingConnections: [Int: [Connection]] = [Int: [Connection]]()
15 |
16 | public static var count = 0
17 | public var id: String
18 |
19 | public var visited: Bool = false
20 | public var level: Int?
21 |
22 | public var spriteNode: SKSpriteNode = SKSpriteNode()
23 | public var imageNode: SKSpriteNode
24 | public var wireNodes: [SKShapeNode] = [SKShapeNode]()
25 | public var pinNodes: [SKSpriteNode] = [SKSpriteNode]()
26 |
27 | public init() {
28 | Self.count += 1
29 | id = "OUT" + String(Self.count)
30 |
31 | let wireNode = SKShapeNode()
32 | let wirePath = CGMutablePath()
33 | wirePath.move(to: CGPoint(x: -64, y: 0))
34 | wirePath.addLine(to: CGPoint(x: 0, y: 0))
35 | wireNode.path = wirePath
36 | wireNode.strokeColor = SKColor.darkGray
37 | wireNode.lineWidth = 8
38 | wireNode.name = "WIRE1"
39 | wireNode.userData = NSMutableDictionary()
40 | wireNode.userData?.setValue(PinType.input, forKey: "type")
41 | wireNode.userData?.setValue(Int(0), forKey: "terminal")
42 | wireNodes.append(wireNode)
43 | spriteNode.addChild(wireNode)
44 |
45 | let pinNode = SKSpriteNode(color: .clear, size: .init(width: 36, height: 36))
46 | pinNode.position = .init(x: -64, y: 0)
47 | pinNode.name = "PIN1"
48 | pinNode.userData = NSMutableDictionary()
49 | pinNode.userData?.setValue(PinType.input, forKey: "type")
50 | pinNode.userData?.setValue(false, forKey: "occupied")
51 | pinNode.userData?.setValue(Int(0), forKey: "terminal")
52 | pinNodes.append(pinNode)
53 | spriteNode.addChild(pinNode)
54 |
55 | let pinShapeNode = SKShapeNode(rect: .init(origin: .init(x: -8, y: -8), size: .init(width: 16, height: 16)), cornerRadius: 4)
56 | pinShapeNode.fillColor = SKColor.gray
57 | pinShapeNode.strokeColor = SKColor.darkGray
58 | pinShapeNode.lineWidth = 4
59 | pinNode.addChild(pinShapeNode)
60 |
61 | imageNode = SKSpriteNode(imageNamed: imageName + ".png")
62 | imageNode.size = CGSize(width: 128, height: 128)
63 | imageNode.name = "MAIN-" + id
64 |
65 | spriteNode.addChild(imageNode)
66 | spriteNode.userData = NSMutableDictionary()
67 | spriteNode.userData?.setValue(self, forKey: "parentDevice")
68 | }
69 |
70 | public func run() {
71 | imageNode.texture = inputs[0] ? .onDisplayTexture : .offDisplayTexture
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/Extensions/Nameable.swift:
--------------------------------------------------------------------------------
1 | protocol Nameable {
2 | var name: String { get set }
3 | }
4 |
5 | extension Nameable {
6 | mutating func changeName(to newName: String) {
7 | name = newName
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/LogicBoard.swift:
--------------------------------------------------------------------------------
1 | public struct LogicBoard {
2 | public var mainCircuit: Circuit
3 | public var circuits: [Circuit] = [Circuit]()
4 | public var selectedCircuit: Circuit
5 | public var state: BoardState = .file {
6 | didSet {
7 | circuits.forEach { circuit in
8 | circuit.state = state
9 | }
10 | }
11 | }
12 |
13 | public init() {
14 | mainCircuit = Circuit.fullAdder()
15 | mainCircuit.isExample = true
16 | circuits.append(mainCircuit)
17 |
18 | let dLatch = Circuit.dLatch()
19 | dLatch.isExample = true
20 | circuits.append(dLatch)
21 | selectedCircuit = mainCircuit
22 | }
23 |
24 | public mutating func addCircuit() {
25 | circuits.append(Circuit())
26 | }
27 |
28 | public mutating func addCircuit(_ circuit: Circuit) {
29 | circuits.append(circuit)
30 | }
31 |
32 | public mutating func removeCircuit(_ circuit: Circuit) {
33 | guard let index = circuits.firstIndex(of: circuit) else {
34 | return
35 | }
36 | circuits.remove(at: index)
37 | }
38 | }
39 |
40 | extension LogicBoard {
41 | subscript(index: Int) -> Circuit {
42 | get {
43 | self.circuits[index]
44 | }
45 | set {
46 | self.circuits[index] = newValue
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/LogicBoard/LogicEnvironment.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | class LogicEnvironment: ObservableObject {
4 | @Published var board: LogicBoard
5 | @Published var showDock: Bool = true
6 |
7 | @Published var selectedCircuit: Circuit {
8 | willSet {
9 | board.selectedCircuit = newValue
10 | }
11 | }
12 |
13 | init() {
14 | let board = LogicBoard()
15 | self.board = board
16 | self.selectedCircuit = board.selectedCircuit
17 | }
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7
2 |
3 | // WARNING:
4 | // This file is automatically generated.
5 | // Do not edit it by hand because the contents will be replaced.
6 |
7 | import PackageDescription
8 | import AppleProductTypes
9 |
10 | let package = Package(
11 | name: "LogicBoard",
12 | platforms: [
13 | .iOS("16.0")
14 | ],
15 | products: [
16 | .iOSApplication(
17 | name: "LogicBoard",
18 | targets: ["AppModule"],
19 | teamIdentifier: "6SK4Y4DTME",
20 | displayVersion: "1.0",
21 | bundleVersion: "1",
22 | appIcon: .asset("AppIcon"),
23 | accentColor: .presetColor(.brown),
24 | supportedDeviceFamilies: [
25 | .pad,
26 | .phone
27 | ],
28 | supportedInterfaceOrientations: [
29 | .portrait,
30 | .landscapeRight,
31 | .landscapeLeft,
32 | .portraitUpsideDown(.when(deviceFamilies: [.pad]))
33 | ],
34 | capabilities: [
35 | .fileAccess(.downloadsFolder, mode: .readWrite),
36 | .fileAccess(.userSelectedFiles, mode: .readWrite)
37 | ],
38 | appCategory: .education
39 | )
40 | ],
41 | targets: [
42 | .executableTarget(
43 | name: "AppModule",
44 | path: ".",
45 | resources: [
46 | .process("Resources")
47 | ]
48 | )
49 | ]
50 | )
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 A. Zheng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Popover+Calculations.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Popover+Calculations.swift
3 | //
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 3/19/23.
6 | // Copyright © 2023 A. Zheng. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | public extension Popover {
12 | /// Updates the popover's frame using its size.
13 | func updateFrame(with size: CGSize?) {
14 | let frame = calculateFrame(from: size)
15 | context.size = size
16 | context.staticFrame = frame
17 | context.frame = frame
18 | }
19 |
20 | /// Calculate the popover's frame based on its size and position.
21 | func calculateFrame(from size: CGSize?) -> CGRect {
22 | guard let window = context.presentedPopoverContainer?.window else { return .zero }
23 |
24 | switch attributes.position {
25 | case let .absolute(originAnchor, popoverAnchor):
26 | var popoverFrame = attributes.position.absoluteFrame(
27 | originAnchor: originAnchor,
28 | popoverAnchor: popoverAnchor,
29 | originFrame: attributes.sourceFrame().inset(by: attributes.sourceFrameInset),
30 | popoverSize: size ?? .zero
31 | )
32 |
33 | let screenEdgePadding = attributes.screenEdgePadding
34 |
35 | let safeWindowFrame = window.safeAreaLayoutGuide.layoutFrame
36 | let maxX = safeWindowFrame.maxX - screenEdgePadding.right
37 | let maxY = safeWindowFrame.maxY - screenEdgePadding.bottom
38 |
39 | /// Popover overflows on left/top side.
40 | if popoverFrame.origin.x < screenEdgePadding.left {
41 | popoverFrame.origin.x = screenEdgePadding.left
42 | }
43 | if popoverFrame.origin.y < screenEdgePadding.top {
44 | popoverFrame.origin.y = screenEdgePadding.top
45 | }
46 |
47 | /// Popover overflows on the right/bottom side.
48 | if popoverFrame.maxX > maxX {
49 | let difference = popoverFrame.maxX - maxX
50 | popoverFrame.origin.x -= difference
51 | }
52 | if popoverFrame.maxY > maxY {
53 | let difference = popoverFrame.maxY - maxY
54 | popoverFrame.origin.y -= difference
55 | }
56 |
57 | return popoverFrame
58 | case let .relative(popoverAnchors):
59 |
60 | /// Set the selected anchor to the first one.
61 | if context.selectedAnchor == nil {
62 | context.selectedAnchor = popoverAnchors.first
63 | }
64 |
65 | let popoverFrame = attributes.position.relativeFrame(
66 | selectedAnchor: context.selectedAnchor ?? popoverAnchors.first ?? .bottom,
67 | containerFrame: attributes.sourceFrame().inset(by: attributes.sourceFrameInset),
68 | popoverSize: size ?? .zero
69 | )
70 |
71 | return popoverFrame
72 | }
73 | }
74 |
75 | /// Calculate if the popover should be dismissed via drag **or** animated to another position (if using `.relative` positioning with multiple anchors). Called when the user stops dragging the popover.
76 | func positionChanged(to point: CGPoint) {
77 | let windowBounds = context.windowBounds
78 |
79 | if
80 | attributes.dismissal.mode.contains(.dragDown),
81 | point.y >= windowBounds.height - windowBounds.height * attributes.dismissal.dragDismissalProximity
82 | {
83 | if attributes.dismissal.dragMovesPopoverOffScreen {
84 | var newFrame = context.staticFrame
85 | newFrame.origin.y = windowBounds.height
86 | context.staticFrame = newFrame
87 | context.frame = newFrame
88 | }
89 | dismiss()
90 | return
91 | }
92 | if
93 | attributes.dismissal.mode.contains(.dragUp),
94 | point.y <= windowBounds.height * attributes.dismissal.dragDismissalProximity
95 | {
96 | if attributes.dismissal.dragMovesPopoverOffScreen {
97 | var newFrame = context.staticFrame
98 | newFrame.origin.y = -newFrame.height
99 | context.staticFrame = newFrame
100 | context.frame = newFrame
101 | }
102 | dismiss()
103 | return
104 | }
105 |
106 | if case let .relative(popoverAnchors) = attributes.position {
107 | let frame = attributes.sourceFrame().inset(by: attributes.sourceFrameInset)
108 | let size = context.size ?? .zero
109 |
110 | let closestAnchor = attributes.position.relativeClosestAnchor(
111 | popoverAnchors: popoverAnchors,
112 | containerFrame: frame,
113 | popoverSize: size,
114 | targetPoint: point
115 | )
116 | let popoverFrame = attributes.position.relativeFrame(
117 | selectedAnchor: closestAnchor,
118 | containerFrame: frame,
119 | popoverSize: size
120 | )
121 |
122 | context.selectedAnchor = closestAnchor
123 | context.staticFrame = popoverFrame
124 | context.frame = popoverFrame
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Popover+Context.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Popover+Context.swift
3 | //
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 3/19/23.
6 | // Copyright © 2023 A. Zheng. All rights reserved.
7 | //
8 |
9 | import Combine
10 | import SwiftUI
11 |
12 | public extension Popover {
13 | /**
14 | The popover's view model (stores attributes, frame, and other visible traits).
15 | */
16 | class Context: Identifiable, ObservableObject {
17 | /// The popover's ID. Must be unique, unless replacing an existing popover.
18 | public var id = UUID()
19 |
20 | /// The popover's customizable properties.
21 | public var attributes = Attributes()
22 |
23 | /// The popover's dynamic size, calculated from SwiftUI. If this is `nil`, the popover is not yet ready to be displayed.
24 | @Published public var size: CGSize?
25 |
26 | /// The frame of the popover, without drag gesture offset applied.
27 | @Published public var staticFrame = CGRect.zero
28 |
29 | /// The current frame of the popover.
30 | @Published public var frame = CGRect.zero
31 |
32 | /// The currently selected anchor, if the popover has a `.relative` position.
33 | @Published public var selectedAnchor: Popover.Attributes.Position.Anchor?
34 |
35 | /// If this is true, the popover is the replacement of another popover.
36 | @Published public var isReplacement = false
37 |
38 | /// For animation syncing. If this is not nil, the popover is in the middle of a frame refresh.
39 | public var transaction: Transaction?
40 |
41 | /// Notify when the context changed.
42 | public var changeSink: AnyCancellable?
43 |
44 | /// Indicates whether the popover can be dragged.
45 | public var isDraggingEnabled: Bool {
46 | get {
47 | popoverModel?.popoversDraggable ?? false
48 | }
49 | set {
50 | popoverModel?.popoversDraggable = newValue
51 | }
52 | }
53 |
54 | public var window: UIWindow {
55 | if let window = presentedPopoverContainer?.window {
56 | return window
57 | } else {
58 | print("[Popovers] - This popover is not tied to a window. Please file a bug report (https://github.com/aheze/Popovers/issues).")
59 | return UIWindow()
60 | }
61 | }
62 |
63 | /**
64 | The bounds of the window in which the `Popover` is being presented, or the `zero` frame if the popover has not been presented yet.
65 | */
66 | public var windowBounds: CGRect {
67 | presentedPopoverContainer?.window?.bounds ?? .zero
68 | }
69 |
70 | /**
71 | For the SwiftUI `.popover` view modifier. This is for internal use only - use `Popover.Attributes.onDismiss` if you want to know when the popover is dismissed.
72 |
73 | This is called just after the popover is removed from the model - inside the view modifier, set `$present` to false when this is called.
74 | */
75 | internal var onAutoDismiss: (() -> Void)?
76 |
77 | /// Invoked by the SwiftUI container view when the view has fully disappeared.
78 | internal var onDisappear: (() -> Void)?
79 |
80 | /// The `UIView` presenting this `Popover`, or `nil` if no popovers are currently being presented.
81 | internal var presentedPopoverContainer: UIView?
82 |
83 | internal var windowSublayersKeyValueObservationToken: NSKeyValueObservation?
84 |
85 | /// The `PopoverModel` managing the `Popover`. Sourced from the `presentedPopoverViewController`.
86 | private var popoverModel: PopoverModel? {
87 | return presentedPopoverContainer?.popoverModel
88 | }
89 |
90 | /// Create a context for the popover. You shouldn't need to use this - it's done automatically when you create a new popover.
91 | public init() {
92 | changeSink = objectWillChange.sink { [weak self] in
93 | guard let self = self else { return }
94 | DispatchQueue.main.async {
95 | self.attributes.onContextChange?(self)
96 | }
97 | }
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Popover+Lifecycle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Popover+Lifecycle.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 1/4/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 | #if os(iOS)
9 | import SwiftUI
10 |
11 | /**
12 | Present a popover.
13 | */
14 | public extension Popover {
15 | /**
16 | Present a popover in a window. It may be easier to use the `UIViewController.present(_:)` convenience method instead.
17 | */
18 | internal func present(in window: UIWindow) {
19 | /// Create a transaction for the presentation animation.
20 | let transaction = Transaction(animation: attributes.presentation.animation)
21 |
22 | /// Inject the transaction into the popover, so following frame calculations are animated smoothly.
23 | context.transaction = transaction
24 |
25 | /// Get the popover model that's tied to the window.
26 | let model = window.popoverModel
27 |
28 | /**
29 | Add the popover to the container view.
30 | */
31 | func displayPopover(in container: PopoverGestureContainer) {
32 | withTransaction(transaction) {
33 | model.add(self)
34 |
35 | /// Stop VoiceOver from reading out background views if `blocksBackgroundTouches` is true.
36 | if attributes.blocksBackgroundTouches {
37 | container.accessibilityViewIsModal = true
38 | }
39 |
40 | /// Shift VoiceOver focus to the popover.
41 | if attributes.accessibility.shiftFocus {
42 | UIAccessibility.post(notification: .screenChanged, argument: nil)
43 | }
44 | }
45 | }
46 |
47 | /// Find the existing container view for popovers in this window. If it does not exist, we need to insert one.
48 | let container: PopoverGestureContainer
49 | if let existingContainer = window.popoverContainerView {
50 | container = existingContainer
51 |
52 | /// The container is already laid out in the window, so we can go ahead and show the popover.
53 | displayPopover(in: container)
54 | } else {
55 | container = PopoverGestureContainer(frame: window.bounds)
56 |
57 | /**
58 | Wait until the container is present in the view hierarchy before showing the popover,
59 | otherwise all the layout math will be working with wonky frames.
60 | */
61 | container.onMovedToWindow = { [weak container] in
62 | if let container = container {
63 | displayPopover(in: container)
64 | }
65 | }
66 |
67 | window.addSubview(container)
68 | }
69 |
70 | if attributes.source == .stayAboveWindows {
71 | context.windowSublayersKeyValueObservationToken = window.layer.observe(\.sublayers) { _, _ in
72 | window.bringSubviewToFront(container)
73 | }
74 | }
75 |
76 | /// Hang on to the container for future dismiss/replace actions.
77 | context.presentedPopoverContainer = container
78 | }
79 |
80 | /**
81 | Dismiss a popover.
82 |
83 | - parameter transaction: An optional transaction that can be applied for the dismissal animation.
84 | */
85 | func dismiss(transaction: Transaction? = nil) {
86 | guard let container = context.presentedPopoverContainer else { return }
87 |
88 | let model = container.popoverModel
89 | let dismissalTransaction = transaction ?? Transaction(animation: attributes.dismissal.animation)
90 |
91 | /// Clean up the container view controller if no more popovers are visible.
92 | context.onDisappear = { [weak context] in
93 | if model.popovers.isEmpty {
94 | context?.presentedPopoverContainer?.removeFromSuperview()
95 | context?.presentedPopoverContainer = nil
96 | }
97 |
98 | /// If at least one popover has `blocksBackgroundTouches` set to true, stop VoiceOver from reading out background views
99 | context?.presentedPopoverContainer?.accessibilityViewIsModal = model.popovers.contains { $0.attributes.blocksBackgroundTouches }
100 | }
101 |
102 | /// Remove this popover from the view model, dismissing it.
103 | withTransaction(dismissalTransaction) {
104 | model.remove(self)
105 | }
106 |
107 | /// Let the internal SwiftUI modifiers know that the popover was automatically dismissed.
108 | context.onAutoDismiss?()
109 |
110 | /// Let the client know that the popover was automatically dismissed.
111 | attributes.onDismiss?()
112 | }
113 |
114 | /**
115 | Replace this popover with another popover smoothly.
116 | */
117 | func replace(with newPopover: Popover) {
118 | guard let popoverContainerViewController = context.presentedPopoverContainer else { return }
119 |
120 | let model = popoverContainerViewController.popoverModel
121 |
122 | /// Get the index of the previous popover.
123 | if let oldPopoverIndex = model.index(of: self) {
124 | /// Get the old popover's context.
125 | let oldContext = model.popovers[oldPopoverIndex].context
126 |
127 | /// Create a new transaction for the replacing animation.
128 | let transaction = Transaction(animation: newPopover.attributes.presentation.animation)
129 |
130 | /// Inject the transaction into the new popover, so following frame calculations are animated smoothly.
131 | newPopover.context.transaction = transaction
132 |
133 | /// Use the same `UIViewController` presenting the previous popover, so we animate the popover in the same container.
134 | newPopover.context.presentedPopoverContainer = oldContext.presentedPopoverContainer
135 |
136 | /// Set the popover as a replacement.
137 | newPopover.context.isReplacement = true
138 |
139 | /// Use same ID so that SwiftUI animates the change.
140 | newPopover.context.id = oldContext.id
141 |
142 | withTransaction(transaction) {
143 | /// Temporarily use the same size for a smooth animation.
144 | newPopover.updateFrame(with: oldContext.size)
145 |
146 | /// Replace the old popover with the new popover.
147 | model.popovers[oldPopoverIndex] = newPopover
148 | }
149 | }
150 | }
151 | }
152 |
153 | public extension UIResponder {
154 | /// Replace a popover with another popover. Convenience method for `Popover.replace(with:)`.
155 | func replace(_ oldPopover: Popover, with newPopover: Popover) {
156 | oldPopover.replace(with: newPopover)
157 | }
158 |
159 | /// Dismiss a popover. Convenience method for `Popover.dismiss(transaction:)`.
160 | func dismiss(_ popover: Popover) {
161 | popover.dismiss()
162 | }
163 |
164 | /**
165 | Get a currently-presented popover with a tag. Returns `nil` if no popover with the tag was found.
166 | - parameter tag: The tag of the popover to look for.
167 | */
168 | func popover(tagged tag: AnyHashable) -> Popover? {
169 | return popoverModel.popover(tagged: tag)
170 | }
171 |
172 | /**
173 | Remove all popovers, or optionally the ones tagged with a `tag` that you supply.
174 | - parameter tag: If this isn't nil, only remove popovers tagged with this.
175 | */
176 | func dismissAllPopovers(with tag: AnyHashable? = nil) {
177 | popoverModel.removeAllPopovers(with: tag)
178 | }
179 | }
180 |
181 | public extension UIViewController {
182 | /// Present a `Popover` using this `UIViewController` as its presentation context.
183 | func present(_ popover: Popover) {
184 | guard let window = view.window else { return }
185 | popover.present(in: window)
186 | }
187 | }
188 |
189 | extension UIView {
190 | var popoverContainerView: PopoverGestureContainer? {
191 | if let container = self as? PopoverGestureContainer {
192 | return container
193 | } else {
194 | for subview in subviews {
195 | if let container = subview.popoverContainerView {
196 | return container
197 | }
198 | }
199 |
200 | return nil
201 | }
202 | }
203 | }
204 | #endif
205 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Popover.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Popover.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 12/23/21.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 | #if os(iOS)
9 | import Combine
10 | import SwiftUI
11 |
12 | /**
13 | A view that is placed over other views.
14 | */
15 | public struct Popover: Identifiable {
16 | /**
17 | Stores information about the popover.
18 | This includes the attributes, frame, and acts like a view model. If using SwiftUI, access it using `PopoverReader`.
19 | */
20 | public var context: Context
21 |
22 | /// The view that the popover presents.
23 | public var view: AnyView
24 |
25 | /// A view that goes behind the popover.
26 | public var background: AnyView
27 |
28 | /**
29 | Convenience accessor for the popover's ID.
30 | */
31 | public var id: UUID {
32 | get {
33 | context.id
34 | } set {
35 | context.id = newValue
36 | }
37 | }
38 |
39 | /// Convenience accessor for the popover's attributes.
40 | public var attributes: Attributes {
41 | get {
42 | context.attributes
43 | } set {
44 | context.attributes = newValue
45 | }
46 | }
47 |
48 | /**
49 | A popover.
50 | - parameter attributes: Customize the popover.
51 | - parameter view: The view to present.
52 | */
53 | public init(
54 | attributes: Attributes = .init(),
55 | @ViewBuilder view: @escaping () -> Content
56 | ) {
57 | let context = Context()
58 | context.attributes = attributes
59 | self.context = context
60 | self.view = AnyView(view().environmentObject(context))
61 | background = AnyView(Color.clear)
62 | }
63 |
64 | /**
65 | A popover with a background.
66 | - parameter attributes: Customize the popover.
67 | - parameter view: The view to present.
68 | - parameter background: The view to present in the background.
69 | */
70 | public init(
71 | attributes: Attributes = .init(),
72 | @ViewBuilder view: @escaping () -> MainContent,
73 | @ViewBuilder background: @escaping () -> BackgroundContent
74 | ) {
75 | let context = Context()
76 | context.attributes = attributes
77 | self.context = context
78 | self.view = AnyView(view().environmentObject(self.context))
79 | self.background = AnyView(background().environmentObject(self.context))
80 | }
81 | }
82 |
83 | extension Popover: Equatable {
84 | /// Conform to equatable.
85 | public static func == (lhs: Popover, rhs: Popover) -> Bool {
86 | return lhs.id == rhs.id
87 | }
88 | }
89 | #endif
90 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/PopoverGestureContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PopoverGestureContainer.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 12/23/21.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | /// A hosting view for `PopoverContainerView` with tap filtering.
13 | class PopoverGestureContainer: UIView {
14 | /// A closure to be invoked when this view is inserted into a window's view hierarchy.
15 | var onMovedToWindow: (() -> Void)?
16 |
17 | /// Create a new `PopoverGestureContainer`.
18 | override init(frame: CGRect) {
19 | super.init(frame: frame)
20 |
21 | /// Allow resizing.
22 | autoresizingMask = [.flexibleWidth, .flexibleHeight]
23 | }
24 |
25 | /// If this is nil, the view hasn't been laid out yet.
26 | var previousBounds: CGRect?
27 |
28 | override func layoutSubviews() {
29 | super.layoutSubviews()
30 |
31 | /// Only update frames on a bounds change.
32 | if let previousBounds = previousBounds, previousBounds != bounds {
33 | /// Orientation or screen bounds changed, so update popover frames.
34 | popoverModel.updateFramesAfterBoundsChange()
35 | }
36 |
37 | /// Store the bounds for later.
38 | previousBounds = bounds
39 | }
40 |
41 | override func didMoveToWindow() {
42 | super.didMoveToWindow()
43 |
44 | /// There might not be a window yet, but that's fine. Just wait until there's actually a window.
45 | guard let window = window else { return }
46 |
47 | /// Create the SwiftUI view that contains all the popovers.
48 | let popoverContainerView = PopoverContainerView(popoverModel: popoverModel)
49 | .environment(\.window, window) /// Inject the window.
50 |
51 | let hostingController = UIHostingController(rootView: popoverContainerView)
52 | hostingController.view.frame = bounds
53 | hostingController.view.backgroundColor = .clear
54 | hostingController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
55 |
56 | addSubview(hostingController.view)
57 |
58 | /// Ensure the view is laid out so that SwiftUI animations don't stutter.
59 | setNeedsLayout()
60 | layoutIfNeeded()
61 |
62 | /// Let the presenter know that its window is available.
63 | onMovedToWindow?()
64 | }
65 |
66 | /**
67 | Determine if touches should land on popovers or pass through to the underlying view.
68 |
69 | The popover container view takes up the entire screen, so normally it would block all touches from going through. This method fixes that.
70 | */
71 | override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
72 | /// Make sure the hit event was actually a touch and not a cursor hover or something else.
73 | guard event.map({ $0.type == .touches }) ?? true else { return nil }
74 |
75 | /// Only loop through the popovers that are in this window.
76 | let popovers = popoverModel.popovers
77 |
78 | /// The current popovers' frames
79 | let popoverFrames = popovers.map { $0.context.frame }
80 |
81 | /// Loop through the popovers and see if the touch hit it.
82 | /// `reversed` to start from the most recently presented popovers, working backwards.
83 | for popover in popovers.reversed() {
84 | /// Check it the popover was hit.
85 | if popover.context.frame.contains(point) {
86 | /// Dismiss other popovers if they have `tapOutsideIncludesOtherPopovers` set to true.
87 | for popoverToDismiss in popovers {
88 | if
89 | popoverToDismiss != popover,
90 | !popoverToDismiss.context.frame.contains(point) /// The popover's frame doesn't contain the touch point.
91 | {
92 | dismissPopoverIfNecessary(popoverFrames: popoverFrames, point: point, popoverToDismiss: popoverToDismiss)
93 | }
94 | }
95 |
96 | /// Receive the touch and block it from going through.
97 | return super.hitTest(point, with: event)
98 | }
99 |
100 | /// The popover was not hit, so let it know that the user tapped outside.
101 | popover.attributes.onTapOutside?()
102 |
103 | /// If the popover has `blocksBackgroundTouches` set to true, stop underlying views from receiving the touch.
104 | if popover.attributes.blocksBackgroundTouches {
105 | let allowedFrames = popover.attributes.blocksBackgroundTouchesAllowedFrames()
106 |
107 | if allowedFrames.contains(where: { $0.contains(point) }) {
108 | dismissPopoverIfNecessary(popoverFrames: popoverFrames, point: point, popoverToDismiss: popover)
109 |
110 | return nil
111 | } else {
112 | /// Receive the touch and block it from going through.
113 | return super.hitTest(point, with: event)
114 | }
115 | }
116 |
117 | /// Check if the touch hit an excluded view. If so, don't dismiss it.
118 | if popover.attributes.dismissal.mode.contains(.tapOutside) {
119 | let excludedFrames = popover.attributes.dismissal.excludedFrames()
120 | if excludedFrames.contains(where: { $0.contains(point) }) {
121 | /**
122 | The touch hit an excluded view, so don't dismiss it.
123 | However, if the touch hit another popover, block it from passing through.
124 | */
125 | if popoverFrames.contains(where: { $0.contains(point) }) {
126 | return super.hitTest(point, with: event)
127 | } else {
128 | return nil
129 | }
130 | }
131 | }
132 |
133 | /// All checks did not pass, which means the touch landed outside the popover. So, dismiss it if necessary.
134 | dismissPopoverIfNecessary(popoverFrames: popoverFrames, point: point, popoverToDismiss: popover)
135 | }
136 |
137 | /// The touch did not hit any popover, so pass it through to the hit testing target.
138 | return nil
139 | }
140 |
141 | /// Dismiss a popover, knowing that its frame does not contain the touch.
142 | func dismissPopoverIfNecessary(popoverFrames: [CGRect], point: CGPoint, popoverToDismiss: Popover) {
143 | if
144 | popoverToDismiss.attributes.dismissal.mode.contains(.tapOutside), /// The popover can be automatically dismissed when tapped outside.
145 | popoverToDismiss.attributes.dismissal.tapOutsideIncludesOtherPopovers || /// The popover can be dismissed even if the touch hit another popover, **or...**
146 | !popoverFrames.contains(where: { $0.contains(point) }) /// ... no other popover frame contains the point (the touch landed outside)
147 | {
148 | popoverToDismiss.dismiss()
149 | }
150 | }
151 |
152 | /// Dismiss all popovers if the accessibility escape gesture was performed.
153 | override func accessibilityPerformEscape() -> Bool {
154 | for popover in popoverModel.popovers {
155 | popover.dismiss()
156 | }
157 |
158 | return true
159 | }
160 |
161 | /// Boilerplate code.
162 | @available(*, unavailable)
163 | required init?(coder _: NSCoder) {
164 | fatalError("[Popovers] - Create this view programmatically.")
165 | }
166 | }
167 | #endif
168 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/PopoverModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PopoverModel.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 12/23/21.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import Combine
11 | import SwiftUI
12 |
13 | /**
14 | The view model for presented popovers within a window.
15 |
16 | Each view model is scoped to a window, which retains the view model.
17 | Presenting or otherwise managing a popover automatically scopes interactions to the window of the current view hierarchy.
18 | */
19 | class PopoverModel: ObservableObject {
20 | /// The currently-presented popovers. The oldest are in front, the newest at the end.
21 | @Published var popovers = [Popover]()
22 |
23 | /// Determines if the popovers can be dragged.
24 | @Published var popoversDraggable = true
25 |
26 | /// Store the frames of views (for excluding popover dismissal or source frames).
27 | @Published var frameTags: [AnyHashable: CGRect] = [:]
28 |
29 | /**
30 | Store frames of popover source views when presented using `.popover(selection:tag:attributes:view:)`. These frames are then used as excluded frames for dismissal.
31 |
32 | To opt out of this behavior, set `attributes.dismissal.excludedFrames` manually. To clear this array (usually when you present another view where the frames don't apply), use a `FrameTagReader` to call `FrameTagProxy.clearSavedFrames()`.
33 | */
34 | @Published var selectionFrameTags: [AnyHashable: CGRect] = [:]
35 |
36 | /// Force the container view to update.
37 | func reload() {
38 | objectWillChange.send()
39 | }
40 |
41 | /**
42 | Refresh the popovers with a new transaction.
43 |
44 | This is called when the screen bounds changes - by setting a transaction for each popover,
45 | the `PopoverContainerView` knows that it needs to animate a change (processed in `sizeReader`).
46 | */
47 | func refresh(with transaction: Transaction?) {
48 | /// Set each popovers's transaction to the new transaction to keep the smooth animation.
49 | for popover in popovers {
50 | popover.context.transaction = transaction
51 | }
52 |
53 | /// Update all popovers.
54 | reload()
55 | }
56 |
57 | /// Adds a `Popover` to this model.
58 | func add(_ popover: Popover) {
59 | popovers.append(popover)
60 | }
61 |
62 | /// Removes a `Popover` from this model.
63 | func remove(_ popover: Popover) {
64 | popovers.removeAll { $0 == popover }
65 | }
66 |
67 | /**
68 | Remove all popovers, or optionally the ones tagged with a `tag` that you supply.
69 | - parameter tag: If this isn't nil, only remove popovers tagged with this.
70 | */
71 | func removeAllPopovers(with tag: AnyHashable? = nil) {
72 | if let tag = tag {
73 | popovers.removeAll(where: { $0.attributes.tag == tag })
74 | } else {
75 | popovers.removeAll()
76 | }
77 | }
78 |
79 | /// Get the index in the for a popover. Returns `nil` if the popover is not in the array.
80 | func index(of popover: Popover) -> Int? {
81 | return popovers.firstIndex(of: popover)
82 | }
83 |
84 | /**
85 | Get a currently-presented popover with a tag. Returns `nil` if no popover with the tag was found.
86 | - parameter tag: The tag of the popover to look for.
87 | */
88 | func popover(tagged tag: AnyHashable) -> Popover? {
89 | let matchingPopovers = popovers.filter { $0.attributes.tag == tag }
90 | if matchingPopovers.count > 1 {
91 | print("[Popovers] - Warning - There are \(matchingPopovers.count) popovers tagged '\(tag)'. Tags should be unique. Try dismissing all existing popovers first.")
92 | }
93 | return matchingPopovers.first
94 | }
95 |
96 | /**
97 | Update all popover frames.
98 |
99 | This is called when the device rotates or has a bounds change.
100 | */
101 | func updateFramesAfterBoundsChange() {
102 | /**
103 | First, update all popovers anyway.
104 |
105 | For some reason, relative positioning + `.center` doesn't need the rotation animation to complete before having a size change.
106 | */
107 | for popover in popovers {
108 | popover.updateFrame(with: popover.context.size)
109 | }
110 |
111 | /// Reload the container view.
112 | reload()
113 |
114 | /// Some other popovers need to wait until the rotation has completed before updating.
115 | DispatchQueue.main.asyncAfter(deadline: .now() + Popovers.frameUpdateDelayAfterBoundsChange) {
116 | self.refresh(with: Transaction(animation: .default))
117 | }
118 | }
119 |
120 | /// Access this with `UIResponder.frameTagged(_:)` if inside a `WindowReader`, or `Popover.Context.frameTagged(_:)` if inside a `PopoverReader.`
121 | func frame(tagged tag: AnyHashable) -> CGRect {
122 | let frame = frameTags[tag]
123 | return frame ?? .zero
124 | }
125 | }
126 | #endif
127 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/PopoverUtilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PopoverUtilities.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 12/23/21.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import Combine
11 | import SwiftUI
12 |
13 | public extension UIView {
14 | /// Convert a view's frame to global coordinates, which are needed for `sourceFrame` and `excludedFrames.`
15 | func windowFrame() -> CGRect {
16 | return convert(bounds, to: nil)
17 | }
18 | }
19 |
20 | public extension Optional where Wrapped: UIView {
21 | /// Convert a view's frame to global coordinates, which are needed for `sourceFrame` and `excludedFrames.` This is a convenience overload for optional `UIView`s.
22 | func windowFrame() -> CGRect {
23 | if let view = self {
24 | return view.windowFrame()
25 | }
26 | return .zero
27 | }
28 | }
29 |
30 | public extension View {
31 | /// Read a view's frame. From https://stackoverflow.com/a/66822461/14351818
32 | func frameReader(in coordinateSpace: CoordinateSpace = .global, rect: @escaping (CGRect) -> Void) -> some View {
33 | return background(
34 | GeometryReader { geometry in
35 | let frame = geometry.frame(in: coordinateSpace)
36 |
37 | Color.clear
38 | .onValueChange(of: frame) { _, newValue in
39 | rect(newValue)
40 | }
41 | .onAppear {
42 | rect(frame)
43 | }
44 | }
45 | .hidden()
46 | )
47 | }
48 |
49 | /**
50 | Read a view's size. The closure is called whenever the size itself changes, or the transaction changes (in the event of a screen rotation.)
51 |
52 | From https://stackoverflow.com/a/66822461/14351818
53 | */
54 | func sizeReader(transaction: Transaction? = nil, size: @escaping (CGSize) -> Void) -> some View {
55 | return background(
56 | GeometryReader { geometry in
57 | Color.clear
58 | .preference(key: ContentSizeReaderPreferenceKey.self, value: geometry.size)
59 | .onPreferenceChange(ContentSizeReaderPreferenceKey.self) { newValue in
60 | DispatchQueue.main.async {
61 | size(newValue)
62 | }
63 | }
64 | .onValueChange(of: transaction?.animation) { _, _ in
65 | DispatchQueue.main.async {
66 | size(geometry.size)
67 | }
68 | }
69 | }
70 | .hidden()
71 | )
72 | }
73 | }
74 |
75 | struct ContentFrameReaderPreferenceKey: PreferenceKey {
76 | static var defaultValue: CGRect { return CGRect() }
77 | static func reduce(value: inout CGRect, nextValue: () -> CGRect) { value = nextValue() }
78 | }
79 |
80 | struct ContentSizeReaderPreferenceKey: PreferenceKey {
81 | static var defaultValue: CGSize { return CGSize() }
82 | static func reduce(value: inout CGSize, nextValue: () -> CGSize) { value = nextValue() }
83 | }
84 |
85 | public extension UIColor {
86 | /**
87 | Create a UIColor from a hex code.
88 |
89 | Example:
90 |
91 | let color = UIColor(hex: 0x00aeef)
92 | */
93 | convenience init(hex: UInt, alpha: CGFloat = 1) {
94 | self.init(
95 | red: CGFloat((hex & 0xFF0000) >> 16) / 255.0,
96 | green: CGFloat((hex & 0x00FF00) >> 8) / 255.0,
97 | blue: CGFloat(hex & 0x0000FF) / 255.0,
98 | alpha: alpha
99 | )
100 | }
101 | }
102 |
103 | /// Position a view using a rectangular frame. Access using `.frame(rect:)`.
104 | struct FrameRectModifier: ViewModifier {
105 | let rect: CGRect
106 | func body(content: Content) -> some View {
107 | content
108 | .frame(width: rect.width, height: rect.height, alignment: .topLeading)
109 | .position(x: rect.origin.x + rect.width / 2, y: rect.origin.y + rect.height / 2)
110 | }
111 | }
112 |
113 | public extension View {
114 | /// Position a view using a rectangular frame.
115 | func frame(rect: CGRect) -> some View {
116 | return modifier(FrameRectModifier(rect: rect))
117 | }
118 | }
119 |
120 | /// For easier CGPoint math
121 | public extension CGPoint {
122 | /// Add 2 CGPoints.
123 | static func + (left: CGPoint, right: CGPoint) -> CGPoint {
124 | return CGPoint(x: left.x + right.x, y: left.y + right.y)
125 | }
126 |
127 | /// Subtract 2 CGPoints.
128 | static func - (left: CGPoint, right: CGPoint) -> CGPoint {
129 | return CGPoint(x: left.x - right.x, y: left.y - right.y)
130 | }
131 | }
132 |
133 | /// Get the distance between 2 CGPoints. From https://www.hackingwithswift.com/example-code/core-graphics/how-to-calculate-the-distance-between-two-cgpoints
134 | public func CGPointDistanceSquared(from: CGPoint, to: CGPoint) -> CGFloat {
135 | return (from.x - to.x) * (from.x - to.x) + (from.y - to.y) * (from.y - to.y)
136 | }
137 |
138 | public extension Shape {
139 | /// Fill and stroke a shape at the same time. https://www.hackingwithswift.com/quick-start/swiftui/how-to-fill-and-stroke-shapes-at-the-same-time
140 | func fill(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: CGFloat = 1) -> some View {
141 | stroke(strokeStyle, lineWidth: lineWidth)
142 | .background(fill(fillStyle))
143 | }
144 | }
145 |
146 | public extension InsettableShape {
147 | /// Fill and stroke a shape at the same time. https://www.hackingwithswift.com/quick-start/swiftui/how-to-fill-and-stroke-shapes-at-the-same-time
148 | func fill(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: CGFloat = 1) -> some View {
149 | strokeBorder(strokeStyle, lineWidth: lineWidth)
150 | .background(fill(fillStyle))
151 | }
152 | }
153 |
154 | public extension UIEdgeInsets {
155 | /// The left + right insets.
156 | var horizontal: CGFloat {
157 | get {
158 | left + right
159 | } set {
160 | left = newValue
161 | right = newValue
162 | }
163 | }
164 |
165 | /// The top + bottom insets.
166 | var vertical: CGFloat {
167 | get {
168 | top + bottom
169 | } set {
170 | top = newValue
171 | bottom = newValue
172 | }
173 | }
174 |
175 | /// Create equal insets on all 4 sides.
176 | init(_ inset: CGFloat) {
177 | self = UIEdgeInsets(top: inset, left: inset, bottom: inset, right: inset)
178 | }
179 | }
180 |
181 | /// Detect changes in bindings (fallback of `.onChange` for iOS 13+). From https://stackoverflow.com/a/64402663/14351818
182 | struct ChangeObserver: View {
183 | let content: Content
184 | let value: Value
185 | let action: (Value, Value) -> Void
186 |
187 | init(value: Value, action: @escaping (Value, Value) -> Void, content: @escaping () -> Content) {
188 | self.value = value
189 | self.action = action
190 | self.content = content()
191 | _oldValue = State(initialValue: value)
192 | }
193 |
194 | @State private var oldValue: Value
195 |
196 | var body: some View {
197 | DispatchQueue.main.async {
198 | if oldValue != value {
199 | action(oldValue, value)
200 | oldValue = value
201 | }
202 | }
203 | return content
204 | }
205 | }
206 |
207 | public extension View {
208 | /// Detect changes in bindings (fallback of `.onChange` for iOS 13+).
209 | func onValueChange(
210 | of value: Value,
211 | perform action: @escaping (_ oldValue: Value, _ newValue: Value) -> Void
212 | ) -> some View {
213 | ChangeObserver(value: value, action: action) {
214 | self
215 | }
216 | }
217 | }
218 |
219 | #endif
220 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/PopoverWindows.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PopoverWindows.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 1/4/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | /**
13 | Popovers supports multiple windows (iOS) by associating each `PopoverModel` with a window.
14 | */
15 |
16 | /// A map of `PopoverModel`s scoped to each window.
17 | class WindowPopoverModels {
18 | /// The singleton `WindowPopoverMap` instance.
19 | static let shared = WindowPopoverModels()
20 |
21 | /**
22 | Aggregates the collection of models applicable to each `UIWindow` in the application.
23 |
24 | `UIWindow` references are weakly retained to avoid us leaking application scenes that have been disposed of by iOS,
25 | e.g. when dismissed from the multitasking UI or explicitly closed by the app.
26 | */
27 | private var windowModels = [Weak: PopoverModel]()
28 |
29 | private init() {
30 | /// Enforcing singleton by marking `init` as private.
31 | }
32 |
33 | /**
34 | Retrieves the `PopoverModel` associated with the given `UIWindow`.
35 |
36 | When a `PopoverModel` already exists for the given `UIWindow`, the same reference will be returned by this function.
37 | Otherwise, a new model is created and associated with the window.
38 |
39 | - parameter window: The `UIWindow` whose `PopoverModel` is being requested, e.g. to present a popover.
40 | - Returns: The `PopoverModel` used to model the visible popovers for the given window.
41 | */
42 | func popoverModel(for window: UIWindow) -> PopoverModel {
43 | /**
44 | Continually remove entries that refer to `UIWindow`s that are no longer about.
45 | The view hierarchies have already been dismantled - this is just for our own book keeping.
46 | */
47 | pruneDeallocatedWindowModels()
48 |
49 | if let existingModel = existingPopoverModel(for: window) {
50 | return existingModel
51 | } else {
52 | return prepareAndRetainModel(for: window)
53 | }
54 | }
55 |
56 | private func pruneDeallocatedWindowModels() {
57 | let keysToRemove = windowModels.keys.filter(\.isPointeeDeallocated)
58 | for key in keysToRemove {
59 | windowModels[key] = nil
60 | }
61 | }
62 |
63 | /// Get an existing popover model for this window if it exists.
64 | private func existingPopoverModel(for window: UIWindow) -> PopoverModel? {
65 | return windowModels.first(where: { holder, _ in holder.pointee === window })?.value
66 | }
67 |
68 | private func prepareAndRetainModel(for window: UIWindow) -> PopoverModel {
69 | let newModel = PopoverModel()
70 | let weakWindowReference = Weak(pointee: window)
71 | windowModels[weakWindowReference] = newModel
72 |
73 | return newModel
74 | }
75 |
76 | /// Container type to enable storage of an object type without incrementing its retain count.
77 | private class Weak: NSObject where T: AnyObject {
78 | private(set) weak var pointee: T?
79 |
80 | var isPointeeDeallocated: Bool {
81 | pointee == nil
82 | }
83 |
84 | init(pointee: T) {
85 | self.pointee = pointee
86 | }
87 | }
88 | }
89 |
90 | extension UIResponder {
91 | /**
92 | The `PopoverModel` in the current responder chain.
93 |
94 | Each responder chain hosts a single `PopoverModel` at the window level.
95 | Each scene containing a separate window will contain its own `PopoverModel`, scoping the layout code to each window.
96 |
97 | - Important: Attempting to request the `PopoverModel` for a responder not present in the chain is programmer error.
98 | */
99 | var popoverModel: PopoverModel {
100 | /// If we're a view, continue to walk up the responder chain until we hit the root view.
101 | if let view = self as? UIView, let superview = view.superview {
102 | return superview.popoverModel
103 | }
104 |
105 | /// If we're a window, we define the scoping for the model - access it.
106 | if let window = self as? UIWindow {
107 | return WindowPopoverModels.shared.popoverModel(for: window)
108 | }
109 |
110 | /// If we're a view controller, begin walking the responder chain up to the root view.
111 | if let viewController = self as? UIViewController {
112 | return viewController.view.popoverModel
113 | }
114 |
115 | print("[Popovers] - No `PopoverModel` present in responder chain (\(self)) - has the source view been installed into a window? Please file a bug report (https://github.com/aheze/Popovers/issues).")
116 |
117 | return PopoverModel()
118 | }
119 | }
120 |
121 | /// For passing the hosting window into the environment.
122 | extension EnvironmentValues {
123 | /// Designates the `UIWindow` hosting the views within the current environment.
124 | var window: UIWindow? {
125 | get {
126 | self[WindowEnvironmentKey.self]
127 | }
128 | set {
129 | self[WindowEnvironmentKey.self] = newValue
130 | }
131 | }
132 |
133 | private struct WindowEnvironmentKey: EnvironmentKey {
134 | typealias Value = UIWindow?
135 |
136 | static var defaultValue: UIWindow? = nil
137 | }
138 | }
139 | #endif
140 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Popovers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Popovers.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 1/17/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | /**
13 | A collection of constants.
14 | */
15 | public enum Popovers {
16 | /// The minimum distance a popover needs to be dragged before it starts getting offset.
17 | public static var minimumDragDistance = CGFloat(2)
18 |
19 | /// The delay after a bounds change before recalculating popover frames.
20 | public static var frameUpdateDelayAfterBoundsChange = CGFloat(0.6)
21 | }
22 | #endif
23 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/SwiftUI/FrameTag.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FrameTag.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 12/23/21.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | /**
13 | Frame tags are used to store the frames.
14 | **Warning:** this won't update the parent view, so is only valid for use inside a popover.
15 | If you need to use frame later, use `frameReader` instead.
16 | */
17 |
18 | /// Store a view's frame for later use.
19 | struct FrameTagModifier: ViewModifier {
20 | /// The name of the frame.
21 | let tag: AnyHashable
22 | @State var frame = CGRect.zero
23 |
24 | func body(content: Content) -> some View {
25 | WindowReader { window in
26 | content
27 | .frameReader { frame in
28 | self.frame = frame
29 |
30 | if let window = window {
31 | window.save(frame, for: tag)
32 | }
33 | }
34 | .onValueChange(of: window) { _, newValue in
35 | if let window = window {
36 | window.save(frame, for: tag)
37 | }
38 | }
39 | }
40 | }
41 | }
42 |
43 | public extension View {
44 | /**
45 | Tag a view and store its frame. Access using `Popovers.frameTagged(_:)`.
46 |
47 | You can use this for supplying source frames or excluded frames. **Do not** use it anywhere else, due to State re-rendering issues.
48 |
49 | - parameter tag: The tag for the frame
50 | */
51 | func frameTag(_ tag: AnyHashable) -> some View {
52 | return modifier(FrameTagModifier(tag: tag))
53 | }
54 | }
55 |
56 | public extension UIResponder {
57 | /**
58 | Get the saved frame of a frame-tagged view inside this window. You must first set the frame using `.frameTag(_:)`.
59 | - parameter tag: The tag that you used for the frame.
60 | - Returns: The frame of a frame-tagged view, or `nil` if no view with the tag exists.
61 | */
62 | func frameTagged(_ tag: AnyHashable) -> CGRect {
63 | return popoverModel.frame(tagged: tag)
64 | }
65 |
66 | /**
67 | Remove all saved frames inside this window for `.popover(selection:tag:attributes:view:)`.
68 | Call this method when you present another view where the frames don't apply.
69 | */
70 | func clearSavedFrames() {
71 | popoverModel.selectionFrameTags.removeAll()
72 | }
73 |
74 | /// Save a frame in this window's `frameTags`.
75 | internal func save(_ frame: CGRect, for tag: AnyHashable) {
76 | popoverModel.frameTags[tag] = frame
77 | }
78 | }
79 |
80 | public extension Optional where Wrapped: UIResponder {
81 | /**
82 | Get the saved frame of a frame-tagged view inside this window. You must first set the frame using `.frameTag(_:)`. This is a convenience overload for optional `UIResponder`s.
83 | - parameter tag: The tag that you used for the frame.
84 | - Returns: The frame of a frame-tagged view, or `nil` if no view with the tag exists.
85 | */
86 | func frameTagged(_ tag: AnyHashable) -> CGRect {
87 | if let responder = self {
88 | return responder.frameTagged(tag)
89 | }
90 | return .zero
91 | }
92 | }
93 | #endif
94 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/SwiftUI/Readers.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Readers.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 12/23/21.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | /**
13 | Read the popover's context from within its `view` or `background`.
14 | Use this just like `GeometryReader`.
15 |
16 | **Warning:** This must be placed inside a popover's `view` or `background`.
17 | */
18 | public struct PopoverReader: View {
19 | /// Read the popover's context from within its `view` or `background`.
20 | public init(@ViewBuilder view: @escaping (Popover.Context) -> Content) {
21 | self.view = view
22 | }
23 |
24 | /// The parent view.
25 | @ViewBuilder var view: (Popover.Context) -> Content
26 |
27 | /// The popover's context (passed down from `Popover.swift`).
28 | @EnvironmentObject var context: Popover.Context
29 |
30 | public var body: some View {
31 | /// Pass the context down.
32 | view(context)
33 | }
34 | }
35 |
36 | /**
37 | Read the current `UIWindow` that hosts the view.
38 | Use this just like `GeometryReader`.
39 |
40 | **Warning:** Do *not* place this inside a popover's `view` or its `background`.
41 | Instead, use the `window` property of the popover's context.
42 | */
43 | public struct WindowReader: View {
44 | /// Your SwiftUI view.
45 | public let view: (UIWindow?) -> Content
46 |
47 | /// The read window.
48 | @StateObject var windowViewModel = WindowViewModel()
49 |
50 | /// Reads the `UIWindow` that hosts some SwiftUI content.
51 | public init(@ViewBuilder view: @escaping (UIWindow?) -> Content) {
52 | self.view = view
53 | }
54 |
55 | public var body: some View {
56 | view(windowViewModel.window)
57 | // .id(windowViewModel.window)
58 | .background(
59 | WindowHandlerRepresentable(windowViewModel: windowViewModel)
60 | )
61 |
62 | }
63 |
64 | /// A wrapper view to read the parent window.
65 | private struct WindowHandlerRepresentable: UIViewRepresentable {
66 | @ObservedObject var windowViewModel: WindowViewModel
67 |
68 | func makeUIView(context _: Context) -> WindowHandler {
69 | return WindowHandler(windowViewModel: self.windowViewModel)
70 | }
71 |
72 | func updateUIView(_: WindowHandler, context _: Context) {}
73 | }
74 |
75 | private class WindowHandler: UIView {
76 | var windowViewModel: WindowViewModel
77 |
78 | init(windowViewModel: WindowViewModel) {
79 | self.windowViewModel = windowViewModel
80 | super.init(frame: .zero)
81 | backgroundColor = .clear
82 | }
83 |
84 | @available(*, unavailable)
85 | required init?(coder _: NSCoder) {
86 | fatalError("[Popovers] - Create this view programmatically.")
87 | }
88 |
89 | override func didMoveToWindow() {
90 | super.didMoveToWindow()
91 |
92 | DispatchQueue.main.async {
93 | /// Set the window.
94 | self.windowViewModel.window = self.window
95 | }
96 | }
97 | }
98 | }
99 |
100 | class WindowViewModel: ObservableObject {
101 | @Published var window: UIWindow?
102 | }
103 |
104 | #endif
105 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Alert.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Alert.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 2/4/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | public extension Templates {
13 | /// A button style to resemble that of a system alert.
14 | struct AlertButtonStyle: ButtonStyle {
15 | /// A button style to resemble that of a system alert.
16 | public init() {}
17 | public func makeBody(configuration: Configuration) -> some View {
18 | configuration.label
19 | .frame(maxWidth: .infinity)
20 | .contentShape(Rectangle())
21 | .padding()
22 | .background(
23 | configuration.isPressed ? Templates.buttonHighlightColor : Color.clear
24 | )
25 | }
26 | }
27 | }
28 | #endif
29 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Blur.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Blur.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 2/4/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | public extension Templates {
13 | /// Use UIKit blurs in SwiftUI.
14 | struct VisualEffectView: UIViewRepresentable {
15 | /// The blur's style.
16 | public var style: UIBlurEffect.Style
17 |
18 | /// Use UIKit blurs in SwiftUI.
19 | public init(_ style: UIBlurEffect.Style) {
20 | self.style = style
21 | }
22 |
23 | public func makeUIView(context: Context) -> UIVisualEffectView {
24 | UIVisualEffectView()
25 | }
26 |
27 | public func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
28 | uiView.effect = UIBlurEffect(style: style)
29 | }
30 | }
31 | }
32 | #endif
33 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Container.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Container.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 2/4/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | public extension Templates {
13 | /**
14 | A standard container for popovers, complete with arrow.
15 | */
16 | struct Container: View {
17 | /// Which side to place the arrow on.
18 | public var arrowSide: ArrowSide?
19 |
20 | /// The container's corner radius.
21 | public var cornerRadius = CGFloat(12)
22 |
23 | /// The container's background/fill color.
24 | public var backgroundColor = Color(.systemBackground)
25 |
26 | /// The shadow around the content view.
27 | public var shadow: Shadow? = .system
28 |
29 | /// The padding around the content view.
30 | public var padding = CGFloat(16)
31 |
32 | /// The content view.
33 | @ViewBuilder public var view: Content
34 |
35 | /**
36 | A standard container for popovers, complete with arrow.
37 | - parameter arrowSide: Which side to place the arrow on.
38 | - parameter cornerRadius: The container's corner radius.
39 | - parameter backgroundColor: The container's background/fill color.
40 | - parameter padding: The padding around the content view.
41 | - parameter view: The content view.
42 | */
43 | public init(
44 | arrowSide: Templates.ArrowSide? = nil,
45 | cornerRadius: CGFloat = CGFloat(12),
46 | backgroundColor: Color = Color(.systemBackground),
47 | shadow: Shadow? = .system,
48 | padding: CGFloat = CGFloat(16),
49 | @ViewBuilder view: () -> Content
50 | ) {
51 | self.arrowSide = arrowSide
52 | self.cornerRadius = cornerRadius
53 | self.backgroundColor = backgroundColor
54 | self.shadow = shadow
55 | self.padding = padding
56 | self.view = view()
57 | }
58 |
59 | public var body: some View {
60 | PopoverReader { context in
61 | view
62 | .padding(padding)
63 | .background(
64 | BackgroundWithArrow(
65 | arrowSide: arrowSide ?? context.attributes.position.getArrowPosition(),
66 | cornerRadius: cornerRadius
67 | )
68 | .fill(backgroundColor)
69 | .popoverShadowIfNeeded(shadow: shadow)
70 | )
71 | }
72 | }
73 | }
74 |
75 | /// The side of the popover that the arrow should be placed on.
76 | /**
77 |
78 | top
79 | X──────────────X──────────────X
80 | | |
81 | | |
82 | left X X right
83 | | |
84 | | |
85 | X──────────────X──────────────X
86 | bottom
87 | */
88 | enum ArrowSide {
89 | case top(ArrowAlignment)
90 | case right(ArrowAlignment)
91 | case bottom(ArrowAlignment)
92 | case left(ArrowAlignment)
93 |
94 | /// Place the arrow on the left, middle, or right on a side.
95 | /**
96 |
97 | mostCounterClockwise centered mostClockwise
98 | ────X──────────────────────X──────────────────────X────
99 | | |
100 | * diagram is for `ArrowSide.top`
101 | */
102 | public enum ArrowAlignment {
103 | case mostCounterClockwise
104 | case centered
105 | case mostClockwise
106 | }
107 | }
108 | }
109 | #endif
110 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Hero.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Hero.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 7/17/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 |
11 | #if os(iOS)
12 |
13 | // MARK: Work in progress, not usable yet
14 |
15 | public extension Templates {
16 | class Hero: ObservableObject {
17 | public enum Selection {
18 | case a
19 | case b
20 | }
21 |
22 | @Published public var selection: Selection?
23 |
24 | public init() {}
25 |
26 | public func transitionForwards() {
27 | guard selection == nil else { return }
28 | selection = .a
29 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.09) {
30 | withAnimation {
31 | self.selection = .b
32 | }
33 | }
34 |
35 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
36 | self.selection = nil
37 | }
38 | }
39 |
40 | public func moveForwards() {
41 | guard selection == nil else { return }
42 | selection = .a
43 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
44 | withAnimation {
45 | self.selection = .b
46 | }
47 | }
48 | }
49 |
50 | public func moveBackwards() {
51 | guard selection == .b else { return }
52 | selection = .a
53 |
54 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
55 | self.selection = nil
56 | }
57 | }
58 |
59 | public func toggleMove() {
60 | if selection == .none {
61 | moveForwards()
62 | } else {
63 | moveBackwards()
64 | }
65 | }
66 | }
67 | }
68 | #endif
69 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Menu/Menu+SwiftUI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Menu+SwiftUI.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 6/14/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | public extension Templates {
13 | /**
14 | A built-from-scratch version of the system menu.
15 | */
16 | @available(iOS 14.0, *)
17 | struct Menu: View {
18 | /// View model for the menu buttons. Should be `StateObject` to avoid getting recreated by SwiftUI, but this works on iOS 13.
19 | @StateObject var model: MenuModel
20 |
21 | /// View model for controlling menu gestures.
22 | @StateObject var gestureModel: MenuGestureModel
23 |
24 | /// Allow presenting from an external view via `$present`.
25 | @Binding var overridePresent: Bool
26 |
27 | /// The menu buttons.
28 | public let content: () -> Content
29 |
30 | /// The origin label.
31 | public let label: (Bool) -> GeneratorLabel
32 |
33 | /// Fade the origin label.
34 | @State var fadeLabel = false
35 |
36 | /**
37 | A built-from-scratch version of the system menu, for SwiftUI.
38 | */
39 | public init(
40 | present: Binding = .constant(false),
41 | configuration buildConfiguration: @escaping ((inout MenuConfiguration) -> Void) = { _ in },
42 | @ViewBuilder content: @escaping () -> Content,
43 | @ViewBuilder label: @escaping (Bool) -> GeneratorLabel
44 | ) {
45 | _overridePresent = present
46 | _model = StateObject(wrappedValue: MenuModel(buildConfiguration: buildConfiguration))
47 | _gestureModel = StateObject(wrappedValue: MenuGestureModel())
48 | self.content = content
49 | self.label = label
50 | }
51 |
52 | public var body: some View {
53 | WindowReader { window in
54 | label(fadeLabel)
55 | .frameTag(model.id)
56 | .contentShape(Rectangle())
57 | .simultaneousGesture(
58 | DragGesture(minimumDistance: 0, coordinateSpace: .global)
59 | .onChanged { value in
60 |
61 | gestureModel.onDragChanged(
62 | newDragLocation: value.location,
63 | model: model,
64 | labelFrame: window.frameTagged(model.id),
65 | window: window
66 | ) { present in
67 | model.present = present
68 | } fadeLabel: { fade in
69 | fadeLabel = fade
70 | }
71 | }
72 | .onEnded { value in
73 | gestureModel.onDragEnded(
74 | newDragLocation: value.location,
75 | model: model,
76 | labelFrame: window.frameTagged(model.id),
77 | window: window
78 | ) { present in
79 | model.present = present
80 | } fadeLabel: { fade in
81 | fadeLabel = fade
82 | }
83 | }
84 | )
85 | .onValueChange(of: model.present) { _, present in
86 | if !present {
87 | withAnimation(model.configuration.labelFadeAnimation) {
88 | fadeLabel = false
89 | model.selectedItemID = nil
90 | model.hoveringItemID = nil
91 | }
92 | overridePresent = present
93 | }
94 | }
95 | .onValueChange(of: overridePresent) { _, present in
96 | if present != model.present {
97 | model.present = present
98 | withAnimation(model.configuration.labelFadeAnimation) {
99 | fadeLabel = present
100 | }
101 | }
102 | }
103 | .popover(
104 | present: $model.present,
105 | attributes: {
106 | $0.position = .absolute(
107 | originAnchor: model.configuration.originAnchor,
108 | popoverAnchor: model.configuration.popoverAnchor
109 | )
110 | $0.rubberBandingMode = .none
111 | $0.dismissal.excludedFrames = {
112 | [
113 | window.frameTagged(model.id),
114 | ]
115 | + model.configuration.excludedFrames()
116 | }
117 | $0.sourceFrameInset = model.configuration.sourceFrameInset
118 | $0.screenEdgePadding = model.configuration.screenEdgePadding
119 | }
120 | ) {
121 | MenuView(
122 | model: model,
123 | content: content
124 | )
125 | } background: {
126 | model.configuration.backgroundColor
127 | }
128 | }
129 | }
130 | }
131 | }
132 | #endif
133 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Menu/Menu+UIKit.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Menu+UIKit.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 2/5/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import Combine
11 | import SwiftUI
12 |
13 | public extension Templates {
14 | /// A built-from-scratch version of the system menu, for UIKit.
15 | class UIKitMenu: NSObject {
16 | // MARK: - Menu properties
17 |
18 | /// View model for the menu buttons.
19 | var model: MenuModel
20 |
21 | /// View model for controlling menu gestures.
22 | var gestureModel: MenuGestureModel
23 |
24 | /// The menu buttons.
25 | public let content: Content
26 |
27 | /// The origin label.
28 | public let sourceView: UIView
29 |
30 | /// Fade the origin label.
31 | var fadeLabel: ((Bool) -> Void)?
32 |
33 | // MARK: - UIKit properties
34 |
35 | var popover: Popover?
36 | var longPressGestureRecognizer: UILongPressGestureRecognizer!
37 | var cancellables = Set()
38 |
39 | /**
40 | A built-from-scratch version of the system menu, for UIKit.
41 | This initializer lets you pass in a multiple menu items.
42 | */
43 | public init(
44 | sourceView: UIView,
45 | configuration buildConfiguration: @escaping ((inout MenuConfiguration) -> Void) = { _ in },
46 | @ViewBuilder content: @escaping () -> Content,
47 | fadeLabel: ((Bool) -> Void)? = nil
48 | ) {
49 | self.sourceView = sourceView
50 | model = MenuModel(buildConfiguration: buildConfiguration)
51 | gestureModel = MenuGestureModel()
52 | self.content = content()
53 | self.fadeLabel = fadeLabel
54 | super.init()
55 |
56 | addGestureRecognizer()
57 | }
58 |
59 | /// Set up the drag gesture recognizer (enable "pull-down" behavior).
60 | func addGestureRecognizer() {
61 | let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(dragged))
62 | longPressGestureRecognizer.minimumPressDuration = 0
63 | sourceView.addGestureRecognizer(longPressGestureRecognizer)
64 | sourceView.isUserInteractionEnabled = true
65 | }
66 |
67 | @objc func dragged(_ gestureRecognizer: UILongPressGestureRecognizer) {
68 | let location = gestureRecognizer.location(in: sourceView.window)
69 |
70 | if gestureRecognizer.state == .began || gestureRecognizer.state == .changed {
71 | gestureModel.onDragChanged(
72 | newDragLocation: location,
73 | model: model,
74 | labelFrame: sourceView.windowFrame(),
75 | window: sourceView.window
76 | ) { [weak self] present in
77 | self?.updatePresent(present)
78 | } fadeLabel: { [weak self] fade in
79 | self?.fadeLabel?(fade)
80 | }
81 | } else {
82 | gestureModel.onDragEnded(
83 | newDragLocation: location,
84 | model: model,
85 | labelFrame: sourceView.windowFrame(),
86 | window: sourceView.window
87 | ) { [weak self] present in
88 | self?.updatePresent(present)
89 | } fadeLabel: { [weak self] fade in
90 | self?.fadeLabel?(fade)
91 | }
92 | }
93 | }
94 |
95 | /**
96 | Set `model.present` and show/hide the popover.
97 |
98 | This is called when a menu button is pressed,
99 | or some other action happened that should hide the menu.
100 | This is **not** called when the user taps outside the menu,
101 | since the menu would already be automatically dismissed.
102 | */
103 | func updatePresent(_ present: Bool) {
104 | model.present = present
105 |
106 | if
107 | present,
108 | let window = sourceView.window
109 | {
110 | presentPopover()
111 | popover?.present(in: window)
112 | fadeLabel?(true)
113 | } else {
114 | popover?.dismiss()
115 | popover = nil
116 | fadeLabel?(false)
117 | }
118 | }
119 |
120 | /// Present the menu popover.
121 | func presentPopover() {
122 | let configuration = model.configuration
123 |
124 | var popover = Popover { [weak self] in
125 | if let self = self {
126 | MenuView(model: self.model) {
127 | self.content
128 | }
129 | }
130 | } background: {
131 | configuration.backgroundColor
132 | }
133 |
134 | popover.attributes.sourceFrame = { [weak sourceView] in sourceView.windowFrame() }
135 | popover.attributes.position = .absolute(
136 | originAnchor: configuration.originAnchor,
137 | popoverAnchor: configuration.popoverAnchor
138 | )
139 | popover.attributes.rubberBandingMode = .none
140 | popover.attributes.dismissal.excludedFrames = { [weak self] in
141 | guard let self = self else { return [] }
142 | return [
143 | self.sourceView.windowFrame(),
144 | ]
145 | + configuration.excludedFrames()
146 | }
147 | popover.attributes.sourceFrameInset = configuration.sourceFrameInset
148 | popover.attributes.screenEdgePadding = configuration.screenEdgePadding
149 |
150 | /**
151 | Make sure to set `model.present` back to false when the menu is dismissed.
152 | Don't call `updatePresent`, since the popover has already been automatically dismissed.
153 | */
154 | popover.context.onAutoDismiss = { [weak self] in
155 | self?.model.present = false
156 | self?.fadeLabel?(false)
157 | }
158 |
159 | self.popover = popover
160 | }
161 | }
162 | }
163 |
164 | /// Control menu state externally.
165 | public extension Templates.UIKitMenu {
166 | /// Whether the menu is currently presented or not.
167 | var isPresented: Bool {
168 | model.present
169 | }
170 |
171 | /// Present the menu.
172 | func present() {
173 | updatePresent(true)
174 | }
175 |
176 | /// Dismiss the menu.
177 | func dismiss() {
178 | updatePresent(false)
179 | }
180 | }
181 | #endif
182 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Menu/Model/MenuGestureModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuGestureModel.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 6/14/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | extension Templates {
13 | /// Model for managing gestures that started on the source label.
14 | /// Gestures that started on the popover itself are handled by `MenuView`.
15 | class MenuGestureModel: ObservableObject {
16 | /// If the user is pressing down on the label, this will be a unique `UUID`.
17 | @Published var labelPressUUID: UUID?
18 |
19 | /**
20 | If the label was pressed/dragged when the menu was already presented.
21 | In this case, dismiss the menu if the user lifts their finger on the label.
22 | */
23 | @Published var labelPressedWhenAlreadyPresented = false
24 |
25 | /// The current position of the user's finger.
26 | @Published var dragLocation: CGPoint?
27 |
28 | /// Process the drag gesture, updating the menu to match.
29 | func onDragChanged(
30 | newDragLocation: CGPoint,
31 | model: MenuModel,
32 | labelFrame: CGRect,
33 | window: UIWindow?,
34 | present: @escaping ((Bool) -> Void),
35 | fadeLabel: @escaping ((Bool) -> Void)
36 | ) {
37 | dragLocation = newDragLocation
38 |
39 | /// Reference this here instead of repeating `model.configuration` over and over again.
40 | let configuration = model.configuration
41 |
42 | if model.present == false {
43 | /// The menu is not yet presented.
44 | if labelPressUUID == nil {
45 | labelPressUUID = UUID()
46 | let currentUUID = labelPressUUID
47 | DispatchQueue.main.asyncAfter(deadline: .now() + configuration.holdDelay) {
48 | if
49 | currentUUID == self.labelPressUUID,
50 | let dragLocation = self.dragLocation /// check the location once again
51 | {
52 | if labelFrame.contains(dragLocation) {
53 | present(true)
54 | }
55 | }
56 | }
57 | }
58 |
59 | withAnimation(configuration.labelFadeAnimation) {
60 | let shouldFade = labelFrame.contains(newDragLocation)
61 | fadeLabel(shouldFade)
62 | }
63 | } else if labelPressUUID == nil {
64 | /// The menu was already presented.
65 | labelPressUUID = UUID()
66 | labelPressedWhenAlreadyPresented = true
67 | } else {
68 | /// Highlight the button that the user's finger is over.
69 | model.hoveringItemID = model.getItemID(from: newDragLocation)
70 |
71 | /// Rubber-band the menu.
72 | withAnimation {
73 | if let distance = model.getDistanceFromMenu(from: newDragLocation) {
74 | if configuration.scaleRange.contains(distance) {
75 | let percentage = (distance - configuration.scaleRange.lowerBound) / (configuration.scaleRange.upperBound - configuration.scaleRange.lowerBound)
76 | let scale = 1 - (1 - configuration.minimumScale) * percentage
77 | model.scale = scale
78 | } else if distance < configuration.scaleRange.lowerBound {
79 | model.scale = 1
80 | } else {
81 | model.scale = configuration.minimumScale
82 | }
83 | }
84 | }
85 | }
86 | }
87 |
88 | /// Process the drag gesture ending, updating the menu to match.
89 | func onDragEnded(
90 | newDragLocation: CGPoint,
91 | model: MenuModel,
92 | labelFrame: CGRect,
93 | window: UIWindow?,
94 | present: @escaping ((Bool) -> Void),
95 | fadeLabel: @escaping ((Bool) -> Void)
96 | ) {
97 | dragLocation = newDragLocation
98 |
99 | withAnimation {
100 | model.scale = 1
101 | }
102 |
103 | labelPressUUID = nil
104 |
105 | /// The user started long pressing when the menu was **already** presented.
106 | if labelPressedWhenAlreadyPresented {
107 | labelPressedWhenAlreadyPresented = false
108 |
109 | let selectedItemID = model.getItemID(from: newDragLocation)
110 | model.selectedItemID = selectedItemID
111 | model.hoveringItemID = nil
112 |
113 | /// The user lifted their finger on the label **and** it did not hit a menu item.
114 | if
115 | selectedItemID == nil,
116 | labelFrame.contains(newDragLocation)
117 | {
118 | present(false)
119 | }
120 | } else {
121 | if !model.present {
122 | if labelFrame.contains(newDragLocation) {
123 | present(true)
124 | } else {
125 | withAnimation(model.configuration.labelFadeAnimation) {
126 | fadeLabel(false)
127 | }
128 | }
129 | } else {
130 | let selectedItemID = model.getItemID(from: newDragLocation)
131 | model.selectedItemID = selectedItemID
132 | model.hoveringItemID = nil
133 |
134 | /// The user lifted their finger outside an item target.
135 | if selectedItemID == nil {
136 | model.configuration.onLiftWithoutSelecting?()
137 | } else if model.configuration.dismissAfterSelecting {
138 | /// Dismiss if the user lifted up their finger on an item.
139 | present(false)
140 | }
141 | }
142 | }
143 | }
144 | }
145 | }
146 | #endif
147 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Menu/Model/MenuModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuModel.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 2/6/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | extension Templates {
13 | typealias MenuItemID = UUID
14 | class MenuModel: ObservableObject {
15 | var buildConfiguration: ((inout MenuConfiguration) -> Void) = { _ in }
16 |
17 | var configuration: MenuConfiguration {
18 | var configuration = MenuConfiguration()
19 | buildConfiguration(&configuration)
20 | return configuration
21 | }
22 |
23 | /// A unique ID for the menu (to support multiple menus in the same screen).
24 | @Published var id = UUID()
25 |
26 | /// Whether to show the popover or not.
27 | @Published var present = false
28 |
29 | /// The popover's scale (for rubber banding).
30 | @Published var scale = CGFloat(1)
31 |
32 | /// The index of the menu button that the user's finger hovers on.
33 | @Published var hoveringItemID: MenuItemID?
34 |
35 | /// The selected menu button if it exists.
36 | @Published var selectedItemID: MenuItemID?
37 |
38 | /// The frames of the menu buttons, relative to the window.
39 | @Published var frames = [MenuItemID: CGRect]()
40 |
41 | /// The frame of the menu in global coordinates.
42 | @Published var menuFrame = CGRect.zero
43 |
44 | init(buildConfiguration: @escaping ((inout MenuConfiguration) -> Void) = { _ in }) {
45 | self.buildConfiguration = buildConfiguration
46 | }
47 |
48 | /// Get the menu button ID that intersects the drag gesture's touch location.
49 | func getItemID(from location: CGPoint) -> MenuItemID? {
50 | let matchingFrames = frames.filter { $0.value.contains(location) }
51 |
52 | if matchingFrames.count > 1 {
53 | print("[Popovers] Multiple menu items have the same frame. Make sure items don't overlay. If you can't resolve this, please file a bug report (https://github.com/aheze/Popovers/issues).")
54 | }
55 |
56 | if let frame = matchingFrames.first {
57 | return frame.key
58 | }
59 |
60 | return nil
61 | }
62 |
63 | func getDistanceFromMenu(from location: CGPoint) -> CGFloat? {
64 | let menuCenter = CGPoint(x: menuFrame.midX, y: menuFrame.midY)
65 |
66 | /// The location relative to the popover menu's center (0, 0)
67 | let normalizedLocation = CGPoint(x: location.x - menuCenter.x, y: location.y - menuCenter.y)
68 |
69 | if abs(normalizedLocation.y) >= menuFrame.height / 2, abs(normalizedLocation.y) >= abs(normalizedLocation.x) {
70 | /// top and bottom
71 | let distance = abs(normalizedLocation.y) - menuFrame.height / 2
72 | return distance
73 | } else {
74 | /// left and right
75 | let distance = abs(normalizedLocation.x) - menuFrame.width / 2
76 | return distance
77 | }
78 | }
79 |
80 | /// Get the anchor point to scale from.
81 | func getScaleAnchor(from context: Popover.Context) -> UnitPoint {
82 | if case let .absolute(_, popoverAnchor) = context.attributes.position {
83 | return popoverAnchor.unitPoint
84 | }
85 |
86 | return .center
87 | }
88 | }
89 | }
90 |
91 | #endif
92 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Shadow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Shadow.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 2/4/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | public extension Templates {
13 | /// A convenient way to apply shadows. Access using the `.popoverShadow()` modifier.
14 | struct Shadow {
15 | /// The shadow color.
16 | public var color = Color.black.opacity(0.25)
17 |
18 | /// The shadow radius.
19 | public var radius = CGFloat(0)
20 |
21 | /// The shadow's x offset.
22 | public var x = CGFloat(0)
23 |
24 | /// The shadow's y offset.
25 | public var y = CGFloat(0)
26 |
27 | public static var system = Self(
28 | color: Color.black.opacity(0.25),
29 | radius: 40,
30 | x: 0,
31 | y: 4
32 | )
33 |
34 | public init(
35 | color: Color = Color.black.opacity(0.25),
36 | radius: CGFloat = CGFloat(0),
37 | x: CGFloat = CGFloat(0),
38 | y: CGFloat = CGFloat(0)
39 | ) {
40 | self.color = color
41 | self.radius = radius
42 | self.x = x
43 | self.y = y
44 | }
45 | }
46 | }
47 | #endif
48 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Templates.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Templates.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 12/23/21.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | /**
13 | Some templates to get started with Popovers.
14 |
15 | The rest of the files in this folder extend `Templates`.
16 | */
17 | public enum Templates {
18 | /// Highlight color for the alert and menu buttons.
19 | public static var buttonHighlightColor = Color.secondary.opacity(0.2)
20 | }
21 | #endif
22 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Popover/Sources/Templates/Views.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Views.swift
3 | // Popovers
4 | //
5 | // Created by A. Zheng (github.com/aheze) on 6/14/22.
6 | // Copyright © 2022 A. Zheng. All rights reserved.
7 | //
8 |
9 | #if os(iOS)
10 | import SwiftUI
11 |
12 | public extension Templates {
13 | /// A vertical stack that adds separators
14 | /// From https://movingparts.io/variadic-views-in-swiftui
15 | struct DividedVStack: View {
16 | var leadingMargin: CGFloat
17 | var trailingMargin: CGFloat
18 | var color: UIColor?
19 | var content: Content
20 |
21 | public init(
22 | leadingMargin: CGFloat = 0,
23 | trailingMargin: CGFloat = 0,
24 | color: UIColor? = nil,
25 | @ViewBuilder content: () -> Content
26 | ) {
27 | self.leadingMargin = leadingMargin
28 | self.trailingMargin = trailingMargin
29 | self.color = color
30 | self.content = content()
31 | }
32 |
33 | public var body: some View {
34 | _VariadicView.Tree(
35 | DividedVStackLayout(
36 | leadingMargin: leadingMargin,
37 | trailingMargin: trailingMargin,
38 | color: color
39 | )
40 | ) {
41 | content
42 | }
43 | }
44 | }
45 |
46 | struct DividedVStackLayout: _VariadicView_UnaryViewRoot {
47 | var leadingMargin: CGFloat
48 | var trailingMargin: CGFloat
49 | var color: UIColor?
50 |
51 | @ViewBuilder
52 | public func body(children: _VariadicView.Children) -> some View {
53 | let last = children.last?.id
54 |
55 | VStack(spacing: 0) {
56 | ForEach(children) { child in
57 | child
58 |
59 | if child.id != last {
60 | Divider()
61 | .opacity(color == nil ? 1 : 0)
62 | .overlay(
63 | color.map { Color($0) } ?? .clear /// If we drop iOS 14 support, we can use `.overlay {}` and add an `if-else` statement here.
64 | )
65 | .padding(.leading, leadingMargin)
66 | .padding(.trailing, trailingMargin)
67 | }
68 | }
69 | }
70 | }
71 | }
72 |
73 | /// A horizontal stack that adds separators
74 | struct DividedHStack: View {
75 | var topMargin: CGFloat
76 | var bottomMargin: CGFloat
77 | var content: Content
78 |
79 | public init(
80 | topMargin: CGFloat = 0,
81 | bottomMargin: CGFloat = 0,
82 | @ViewBuilder content: () -> Content
83 | ) {
84 | self.topMargin = topMargin
85 | self.bottomMargin = bottomMargin
86 | self.content = content()
87 | }
88 |
89 | public var body: some View {
90 | _VariadicView.Tree(
91 | DividedHStackLayout(
92 | topMargin: topMargin,
93 | bottomMargin: bottomMargin
94 | )
95 | ) {
96 | content
97 | }
98 | }
99 | }
100 |
101 | struct DividedHStackLayout: _VariadicView_UnaryViewRoot {
102 | var topMargin: CGFloat
103 | var bottomMargin: CGFloat
104 | @ViewBuilder
105 | public func body(children: _VariadicView.Children) -> some View {
106 | let last = children.last?.id
107 |
108 | HStack(spacing: 0) {
109 | ForEach(children) { child in
110 | child
111 |
112 | if child.id != last {
113 | Divider()
114 | .padding(.top, topMargin)
115 | .padding(.bottom, bottomMargin)
116 | }
117 | }
118 | }
119 | }
120 | }
121 | }
122 | #endif
123 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/BoardView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct BoardView: View {
4 | @EnvironmentObject var envi: LogicEnvironment
5 | var body: some View {
6 | ZStack{
7 | CanvasContainerView()
8 | .ignoresSafeArea()
9 | .animation(.easeInOut(duration: 0.2), value: envi.board.selectedCircuit)
10 | CircuitView(circuit: envi.selectedCircuit)
11 | }.ignoresSafeArea(.keyboard)
12 | }
13 | }
14 |
15 | struct CircuitView: View {
16 | @EnvironmentObject var envi: LogicEnvironment
17 | @ObservedObject var circuit: Circuit
18 |
19 | var body: some View {
20 | ZStack {
21 | VStack {
22 | ActionControlView(circuit: envi.selectedCircuit)
23 | .padding([.top, .horizontal], 20)
24 | .animation(.easeInOut, value: circuit.state)
25 | Spacer()
26 | if envi.showDock {
27 | HStack {
28 | CircuitPickerView()
29 | .padding([.bottom, .horizontal], 24)
30 | .padding(.leading, 84)
31 | .transition(.opacity)
32 | Spacer()
33 | }
34 | }
35 | }
36 | .animation(.easeInOut, value: envi.showDock)
37 | .animation(.easeInOut, value: envi.board.selectedCircuit)
38 | .animation(.easeInOut, value: envi.board.circuits)
39 | .animation(.easeInOut, value: circuit.state)
40 |
41 | HStack {
42 | SystemPickerView(circuit: circuit)
43 | .frame(maxWidth: 64, alignment: .leading)
44 | .padding(20)
45 | Spacer()
46 | if circuit.state == .add {
47 | DevicePickerView()
48 | .padding([.trailing], 20)
49 | .padding([.vertical], 100)
50 | .transition(.opacity)
51 | }
52 | }
53 | .animation(.easeInOut, value: circuit.state)
54 |
55 | }
56 | }
57 | }
58 |
59 | struct BoardView_Previews: PreviewProvider {
60 | static var previews: some View {
61 | BoardView()
62 | .previewInterfaceOrientation(.portrait)
63 | .environmentObject(LogicEnvironment())
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/CanvasScene+Animation.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension SKAction {
4 | static func strokeTransitionColor(of node: SKNode, toColor: UIColor, duration: CGFloat = 0.15) -> SKAction
5 | {
6 | return SKAction.customAction(withDuration: duration, actionBlock: { (node : SKNode!, elapsedTime : CGFloat) -> Void in
7 | guard let node = node as? SKShapeNode else { return }
8 | let fromColor = node.strokeColor
9 | let fraction = CGFloat(elapsedTime / duration)
10 | let startColorComponents = fromColor.toComponents()
11 | let endColorComponents = toColor.toComponents()
12 | let transColor = UIColor(red: lerp(a: startColorComponents.red, b: endColorComponents.red, fraction: fraction),
13 | green: lerp(a: startColorComponents.green, b: endColorComponents.green, fraction: fraction),
14 | blue: lerp(a: startColorComponents.blue, b: endColorComponents.blue, fraction: fraction),
15 | alpha: lerp(a: startColorComponents.alpha, b: endColorComponents.alpha, fraction: fraction))
16 | node.strokeColor = transColor
17 | }
18 | )
19 | }
20 |
21 | static func fillTransitionColor(of node: SKNode, toColor: UIColor, duration: CGFloat = 0.15) -> SKAction
22 | {
23 | return SKAction.customAction(withDuration: duration, actionBlock: { (node : SKNode!, elapsedTime : CGFloat) -> Void in
24 | guard let node = node as? SKShapeNode else { return }
25 | let fromColor = node.fillColor
26 | let fraction = CGFloat(elapsedTime / duration)
27 | let startColorComponents = fromColor.toComponents()
28 | let endColorComponents = toColor.toComponents()
29 | let transColor = UIColor(red: lerp(a: startColorComponents.red, b: endColorComponents.red, fraction: fraction),
30 | green: lerp(a: startColorComponents.green, b: endColorComponents.green, fraction: fraction),
31 | blue: lerp(a: startColorComponents.blue, b: endColorComponents.blue, fraction: fraction),
32 | alpha: lerp(a: startColorComponents.alpha, b: endColorComponents.alpha, fraction: fraction))
33 | node.fillColor = transColor
34 | }
35 | )
36 | }
37 | }
38 |
39 | extension SKNode {
40 | func strokeColorTransition(to color: UIColor, duration: CGFloat = 0.15) {
41 | self.run(SKAction.strokeTransitionColor(of: self, toColor: color, duration: duration))
42 | }
43 |
44 | func fillColorTransition(to color: UIColor, duration: CGFloat = 0.15) {
45 | self.run(SKAction.fillTransitionColor(of: self, toColor: color, duration: duration))
46 | }
47 |
48 | func spriteColorTransition(to color: UIColor, opacity: CGFloat) {
49 | self.run(SKAction.colorize(with: color, colorBlendFactor: opacity, duration: 0.15))
50 | }
51 |
52 | func opacityTransition(_ opacity: CGFloat) {
53 | self.run(SKAction.fadeAlpha(to: opacity, duration: 0.15))
54 | }
55 |
56 | func blueTint() {
57 | self.enumerateChildNodes(withName: ".//*") { node, _ in
58 | if node.name?.contains("PIN") ?? false { return }
59 | node.spriteColorTransition(to: .blue, opacity: 0.5)
60 | node.strokeColorTransition(to: .canvasBlue)
61 | node.fillColorTransition(to: (node.parent?.isOccupied() ?? false) ? .canvasBlue : .canvasLightBlue)
62 | }
63 | }
64 |
65 | func removeTint() {
66 | self.enumerateChildNodes(withName: ".//*") { node, _ in
67 | if node.name?.contains("PIN") ?? false { return }
68 | node.spriteColorTransition(to: .blue, opacity: 0)
69 | node.fillColorTransition(to: (node.parent?.isOccupied() ?? false) ? .darkGray : .gray)
70 | guard let scene = self.scene as? CanvasScene,
71 | scene.state != .simulate else { return }
72 | node.strokeColorTransition(to: .darkGray)
73 |
74 | }
75 | }
76 |
77 | func colorOccupied(_ isOccupied: Bool) {
78 | self.enumerateChildNodes(withName: ".//*") { node, _ in
79 | node.fillColorTransition(to: isOccupied ? .darkGray : .gray)
80 | }
81 | }
82 | }
83 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/CanvasScene+Color.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | // Derived from solutions found in https://stackoverflow.com/questions/35029672/getting-pixel-color-from-an-image-using-cgpoint
4 |
5 | extension CGImage {
6 | func getPixelColor(point: CGPoint) -> CGColor? {
7 |
8 | guard let provider = self.dataProvider, let pixelData = provider.data else { return nil }
9 | let data: UnsafePointer = CFDataGetBytePtr(pixelData)
10 |
11 | let pixelInfo: Int = ((Int(self.width) * Int(point.y)) + Int(point.x)) * 4
12 | let r = CGFloat(data[pixelInfo]) / CGFloat(255.0)
13 | let g = CGFloat(data[pixelInfo+1]) / CGFloat(255.0)
14 | let b = CGFloat(data[pixelInfo+2]) / CGFloat(255.0)
15 | let a = CGFloat(data[pixelInfo+3]) / CGFloat(255.0)
16 |
17 | return CGColor(red: r, green: g, blue: b, alpha: a)
18 | }
19 | }
20 |
21 | func lerp(a : CGFloat, b : CGFloat, fraction : CGFloat) -> CGFloat
22 | {
23 | return (b-a) * fraction + a
24 | }
25 |
26 | // Custom color transition for SKShapeNode derived from solution by OwlOCR and Patrick Collin
27 | // https://stackoverflow.com/questions/20872556/skshapenode-animate-color-change
28 |
29 | struct ColorComponents {
30 | var red = CGFloat.zero
31 | var green = CGFloat.zero
32 | var blue = CGFloat.zero
33 | var alpha = CGFloat.zero
34 | }
35 |
36 | extension UIColor {
37 | func toComponents() -> ColorComponents {
38 | var components = ColorComponents()
39 | getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha)
40 | return components
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/CanvasScene+Device.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension CanvasScene {
4 | public func addDevice(_ device: some Device, at position: CGPoint) {
5 | let deviceNode = device.spriteNode
6 |
7 | guard let scene = scene else { return }
8 | deviceNode.position = CGPoint(x: cameraOffset.x + ((position.x - (frame.width / 2)) * previousCameraScale),
9 | y: cameraOffset.y + (((frame.height / 2) - position.y) * previousCameraScale))
10 |
11 | scene.addChild(deviceNode)
12 | }
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/CanvasScene+Texture.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension SKTexture {
4 | static let offButtonTexture = SKTexture(imageNamed: "InputFalse.png")
5 | static let onButtonTexture = SKTexture(imageNamed: "InputTrue.png")
6 | static let offDisplayTexture = SKTexture(imageNamed: "OutputFalse.png")
7 | static let onDisplayTexture = SKTexture(imageNamed: "OutputTrue.png")
8 | }
9 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/CanvasScene.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 | import UIKit
3 |
4 | public class CanvasScene: SKScene, ObservableObject, UIGestureRecognizerDelegate {
5 | var parentCircuit: Circuit?
6 |
7 | var inited = false
8 |
9 | var previousCameraPoint = CGPoint.zero
10 | var originalCameraPoint = CGPoint.zero
11 | var cameraPoint = CGPoint.zero
12 | var cameraOffset = CGPoint.zero
13 |
14 | var state: BoardState = .add {
15 | didSet {
16 | touchedDeviceNode = nil
17 | touchedWireNode = nil
18 | if state == .simulate {
19 | parentCircuit?.devices.forEach {
20 | $0.pinNodes.forEach {
21 | $0.opacityTransition(0)
22 | }
23 | }
24 | } else if oldValue == .simulate {
25 | parentCircuit?.devices.forEach {
26 | $0.pinNodes.forEach {
27 | $0.removeTint()
28 | $0.opacityTransition(1)
29 | }
30 | }
31 | }
32 | }
33 | }
34 |
35 | var touchedDeviceNode: SKSpriteNode? = nil {
36 | didSet {
37 | if touchedDeviceNode == nil {
38 | parentCircuit?.selectedDevice = nil
39 | guard oldValue != draggedDeviceNode else { return }
40 | oldValue?.removeTint()
41 | } else if oldValue != touchedDeviceNode {
42 | guard let parentDevice = touchedDeviceNode?.parentDevice else { return }
43 | parentCircuit?.selectedDevice = parentDevice
44 | oldValue?.removeTint()
45 | touchedDeviceNode?.blueTint()
46 | }
47 | }
48 | }
49 |
50 | var draggedDeviceNode: SKSpriteNode? = nil {
51 | didSet {
52 | touchedDeviceNode = nil
53 | if draggedDeviceNode == nil {
54 | oldValue?.removeTint()
55 | } else {
56 | oldValue?.removeTint()
57 | draggedDeviceNode?.blueTint()
58 | }
59 | }
60 | }
61 | var draggedNodePosition: CGPoint? = nil
62 |
63 |
64 | var touchedWireNode: SKShapeNode? = nil {
65 | didSet {
66 | if touchedWireNode == nil {
67 | parentCircuit?.selectedConnection = nil
68 | guard oldValue != touchedWireNode, state != .simulate else { return }
69 | oldValue?.strokeColorTransition(to: .darkGray)
70 | } else if oldValue != touchedWireNode {
71 | guard let parentConnection = touchedWireNode?.parentConnection else {
72 | return
73 | }
74 | parentCircuit?.selectedConnection = parentConnection
75 | oldValue?.strokeColorTransition(to: .darkGray)
76 | touchedWireNode?.strokeColorTransition(to: .canvasBlue)
77 | }
78 | }
79 | }
80 |
81 | var tempLine: SKShapeNode?
82 | var tempLineLocations: [CGPoint] = [CGPoint]()
83 | var tempLinePointCount: Int = 0
84 | var tempStartPin: SKSpriteNode?
85 | var newPointCreated: Bool = false
86 |
87 | var previousCameraScale: CGFloat = 1.5
88 |
89 | let oneFingerTapRecognizer = UITapGestureRecognizer()
90 | let oneFingerPanRecognizer = UIPanGestureRecognizer()
91 | let pencilPanRecognizer = PencilPanGestureRecognizer()
92 | let twoFingerPanRecognizer = UIPanGestureRecognizer()
93 | let twoFingerPinchRecognizer = UIPinchGestureRecognizer()
94 |
95 | public override func didMove(to view: SKView) {
96 | guard let scene = scene else { return }
97 |
98 | oneFingerTapRecognizer.addTarget(self, action: #selector(tap))
99 | oneFingerTapRecognizer.delegate = self
100 | oneFingerTapRecognizer.allowedTouchTypes = [0,2,3]
101 | oneFingerTapRecognizer.numberOfTouchesRequired = 1
102 | view.addGestureRecognizer(oneFingerTapRecognizer)
103 |
104 | oneFingerPanRecognizer.addTarget(self, action: #selector(oneFingerPan))
105 | oneFingerPanRecognizer.delegate = self
106 | oneFingerPanRecognizer.allowedTouchTypes = [0,3]
107 | oneFingerPanRecognizer.minimumNumberOfTouches = 1
108 | oneFingerPanRecognizer.maximumNumberOfTouches = 1
109 | view.addGestureRecognizer(oneFingerPanRecognizer)
110 |
111 | pencilPanRecognizer.canvasScene = self
112 | pencilPanRecognizer.delegate = self
113 | pencilPanRecognizer.allowedTouchTypes = [2]
114 | pencilPanRecognizer.minimumNumberOfTouches = 1
115 | pencilPanRecognizer.maximumNumberOfTouches = 1
116 | pencilPanRecognizer.delaysTouchesBegan = true
117 | view.addGestureRecognizer(pencilPanRecognizer)
118 |
119 | twoFingerPanRecognizer.addTarget(self, action: #selector(twoFingerPan))
120 | twoFingerPanRecognizer.allowedScrollTypesMask = .continuous
121 | twoFingerPanRecognizer.minimumNumberOfTouches = 2
122 | twoFingerPanRecognizer.maximumNumberOfTouches = 2
123 | twoFingerPanRecognizer.delegate = self
124 | view.addGestureRecognizer(twoFingerPanRecognizer)
125 |
126 | twoFingerPinchRecognizer.addTarget(self, action: #selector(pinch))
127 | twoFingerPinchRecognizer.delegate = self
128 | view.addGestureRecognizer(twoFingerPinchRecognizer)
129 |
130 | let camera = SKCameraNode()
131 |
132 | if !inited {
133 | camera.position = CGPoint(x: 0, y: 0)
134 | cameraPoint = camera.position
135 | originalCameraPoint = camera.position
136 | camera.setScale(1.5)
137 | inited = true
138 | } else {
139 | camera.position = cameraPoint
140 | }
141 |
142 | scene.camera = camera
143 | self.addChild(camera)
144 | }
145 |
146 | public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
147 | if (gestureRecognizer == twoFingerPanRecognizer && otherGestureRecognizer == twoFingerPinchRecognizer)
148 | || (gestureRecognizer == twoFingerPinchRecognizer && otherGestureRecognizer == twoFingerPanRecognizer) {
149 | return true
150 | } else {
151 | return false
152 | }
153 | }
154 | }
155 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/CanvasView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import SpriteKit
3 |
4 | public struct CanvasView: View {
5 | @EnvironmentObject var envi: LogicEnvironment
6 | @ObservedObject var canvasScene: CanvasScene
7 |
8 | public var body: some View {
9 | SpriteView(scene: canvasScene)
10 | .dropDestination(for: String.self) { items, location in
11 | guard let item = items.first else { return false }
12 |
13 | let deviceName = DeviceName.deviceStringToDeviceName(string: item)
14 | var device = deviceName.deviceNameToDevice()
15 | envi.board.selectedCircuit.add(&device, at: location)
16 | return true
17 | }
18 | }
19 | }
20 |
21 | public struct CanvasContainerView: View {
22 | @EnvironmentObject var envi: LogicEnvironment
23 |
24 | public var body: some View {
25 | ZStack {
26 | Rectangle()
27 | .fill(Color(uiColor: UIColor.lightGray))
28 | if envi.board.circuits.count == 0 {
29 | Text("NO CIRCUIT IS SELECTED")
30 | .font(.system(.body, design: .monospaced, weight: .bold))
31 | .foregroundColor(.white)
32 | .padding(8)
33 | .padding(.horizontal, 4)
34 | .background(.thinMaterial, in: Capsule())
35 | }
36 | ForEach(envi.board.circuits) { circuit in
37 | CanvasView(canvasScene: circuit.canvasScene)
38 | .opacity(envi.selectedCircuit == circuit ? 1 : 0)
39 | }
40 | }
41 | .background(Color(uiColor: UIColor.canvasBackground))
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/Gestures/Pan+TwoFinger.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension CanvasScene {
4 | @objc func twoFingerPan(recognizer: UIPanGestureRecognizer) {
5 | guard let camera = scene?.camera else {
6 | return
7 | }
8 |
9 | if recognizer.state == .began {
10 | previousCameraPoint = camera.position
11 | }
12 |
13 | let translation = recognizer.translation(in: self.view)
14 | let newPosition = CGPoint(
15 | x: previousCameraPoint.x + translation.x * -1 * previousCameraScale,
16 | y: previousCameraPoint.y + translation.y * previousCameraScale
17 | )
18 | camera.position = newPosition
19 | cameraPoint = newPosition
20 | cameraOffset = CGPoint(
21 | x: newPosition.x - originalCameraPoint.x,
22 | y: newPosition.y - originalCameraPoint.y
23 | )
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/Gestures/Pinch.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension CanvasScene {
4 | @objc func pinch(recognizer: UIPinchGestureRecognizer) {
5 | guard let camera = self.camera else {
6 | return
7 | }
8 | if recognizer.state == .began {
9 | previousCameraScale = camera.xScale
10 | }
11 | let newScale = previousCameraScale * 1 / recognizer.scale
12 | guard newScale < 5, newScale > 0.5 else { return }
13 | camera.setScale(newScale)
14 | if recognizer.state == .ended {
15 | previousCameraScale = camera.xScale
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Canvas/Gestures/Tap.swift:
--------------------------------------------------------------------------------
1 | import SpriteKit
2 |
3 | extension CanvasScene {
4 | @objc func tap(recognizer: UITapGestureRecognizer) {
5 | guard let scene = scene else { return }
6 | let touchLocation = recognizer.location(in: self.view)
7 | let location = CGPoint(x: cameraOffset.x + ((touchLocation.x - (frame.width / 2)) * previousCameraScale),
8 | y: cameraOffset.y + (((frame.height / 2) - touchLocation.y) * previousCameraScale))
9 | if state == .add {
10 | let nodes = self.nodes(at: location)
11 | let imageNodes = nodes
12 | .filter { $0.name?.contains("MAIN") ?? false }
13 | .map { $0 as? SKSpriteNode }
14 | for imageNode in imageNodes {
15 | guard let imageNode = imageNode,
16 | let node = imageNode.parent as? SKSpriteNode,
17 | let image = imageNode.texture?.cgImage() else { continue }
18 |
19 | var nodeTouchedLocation = scene.convert(location, to: imageNode)
20 | nodeTouchedLocation = CGPoint(x: (nodeTouchedLocation.x + imageNode.size.width/2) * 4,
21 | y: (nodeTouchedLocation.y + imageNode.size.height/2) * 4)
22 |
23 | guard let color = image.getPixelColor(point: nodeTouchedLocation),
24 | color.alpha > 0.01 else { continue }
25 |
26 | touchedDeviceNode = touchedDeviceNode == node ? nil : node
27 | return
28 | }
29 | touchedDeviceNode = nil
30 | }
31 | if state == .wire {
32 | let nodes = self.nodes(at: location)
33 | let wireNodes = nodes
34 | .filter { $0.name?.contains("CONNECTION") ?? false }
35 | .map { $0 as? SKShapeNode }
36 | for wireNode in wireNodes {
37 | guard let wireNode = wireNode,
38 | let image = view?.texture(from: wireNode)?.cgImage() else { continue }
39 | let wireFrame = wireNode.frame
40 | let wireTouchedLocation = CGPoint(x: location.x - wireFrame.minX,
41 | y: location.y - wireFrame.minY)
42 | let pixelPosition = CGPoint(x: wireTouchedLocation.x * CGFloat(image.width) / wireFrame.width,
43 | y: CGFloat(image.height) - (wireTouchedLocation.y * CGFloat(image.height) / wireFrame.height))
44 |
45 |
46 | guard let color = image.getPixelColor(point: pixelPosition),
47 | color.alpha > 0.05 else { continue }
48 |
49 | touchedWireNode = touchedWireNode == wireNode ? nil : wireNode
50 |
51 | return
52 | }
53 | touchedWireNode = nil
54 | }
55 | if state == .simulate {
56 | let nodes = self.nodes(at: location)
57 |
58 | let buttonNode = nodes
59 | .filter { $0.parentDevice is PushButton }
60 | .first
61 |
62 | guard let button = buttonNode?.parentDevice as? PushButton,
63 | let circuit = parentCircuit else { return }
64 | circuit.toggleAndSolve(button)
65 | }
66 | }
67 |
68 | func changeTouchedDeviceNode(to node: SKSpriteNode?) {
69 | touchedDeviceNode = node
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Color/Color.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | extension UIColor {
3 | static let canvasGreen = UIColor(red: 6/255, green: 151/255, blue: 2/255, alpha: 1.00)
4 | static let canvasYellow = UIColor(red: 183/255, green: 138/255, blue: 2/255, alpha: 1.00)
5 | static let canvasBlue = UIColor(red: 0.1, green: 0.1, blue: 0.7, alpha: 1)
6 | static let canvasLightBlue = UIColor(red: 0.4, green: 0.4, blue: 0.8, alpha: 1)
7 | static let canvasBackground = UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1.00)
8 | }
9 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Controls/ActionControlView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UniformTypeIdentifiers
3 |
4 | public struct ActionControlView: View {
5 | @EnvironmentObject var envi: LogicEnvironment
6 | @ObservedObject var circuit: Circuit
7 | @State var showInfo: Bool = false
8 | let wireHelp: String = "To draw a wire, drag from one pin to another pin. To make a turn in the wire, pause momentarily while dragging or, if using an Apple Pencil, press hard on the screen."
9 | let addHelp: String = "To add a device, drag your chosen device from the right panel and drop it to the desire location on the circuit."
10 |
11 | @State var importing = false
12 | @State var exporting = false
13 | @State var showImportErrorAlert = false
14 | @State var showExportErrorAlert = false
15 | @State var showExportSuccessAlert = false
16 |
17 | public var body: some View {
18 | HStack(spacing: 0) {
19 | if circuit.state == .file {
20 | Button {
21 | importing = true
22 | } label: {
23 | Label("Import", systemImage: "square.and.arrow.down")
24 | .labelStyle(ActionLabelStyle())
25 | }
26 | .buttonStyle(ActionButtonStyle())
27 |
28 | Divider()
29 | .frame(height: 20)
30 |
31 | Button {
32 | exporting = true
33 | } label: {
34 | Label("Export", systemImage: "square.and.arrow.up")
35 | .labelStyle(ActionLabelStyle())
36 | }.buttonStyle(ActionButtonStyle())
37 |
38 | }
39 | if circuit.state == .wire || circuit.state == .add {
40 | if let device = circuit.selectedDevice {
41 | Button {
42 | let position: CGPoint = .init(x: device.spriteNode.position.x + (device.imageNode.size.width * 1.25),
43 | y: device.spriteNode.position.y)
44 |
45 | var newDevice: any Device = DeviceName.deviceStringToDeviceName(string: device.deviceName).deviceNameToDevice()
46 | circuit.add(&newDevice, at: position)
47 |
48 | newDevice.spriteNode.position = position
49 | } label: {
50 | Label("Duplicate", systemImage: "plus.square.on.square")
51 | .labelStyle(ActionLabelStyle())
52 | }
53 | .buttonStyle(ActionButtonStyle())
54 | Divider()
55 | .frame(height: 20)
56 | Button {
57 | circuit.remove(device)
58 | circuit.selectedDevice = nil
59 | circuit.canvasScene.touchedDeviceNode = nil
60 | } label: {
61 | Label("Delete", systemImage: "trash")
62 | .labelStyle(ActionLabelStyle())
63 | }
64 | .buttonStyle(ActionButtonStyle())
65 | } else if let connection = circuit.selectedConnection {
66 | Button {
67 | circuit.remove(connection)
68 | circuit.selectedConnection = nil
69 | circuit.canvasScene.touchedWireNode = nil
70 | } label: {
71 | Label("Delete", systemImage: "trash")
72 | .labelStyle(ActionLabelStyle())
73 | }
74 | .buttonStyle(ActionButtonStyle())
75 | } else {
76 | Button {
77 | showInfo = !showInfo
78 | } label: {
79 | Label("Help", systemImage: "questionmark.circle")
80 | .labelStyle(ActionLabelStyle())
81 | }
82 | .buttonStyle(ActionButtonStyle())
83 | .popover(present: $showInfo)
84 | {
85 | Text(circuit.state == .wire ? wireHelp : addHelp)
86 | .frame(maxWidth: 300)
87 | .fixedSize(horizontal: false, vertical: true)
88 | .lineLimit(4)
89 | .font(.system(size: 13, weight: .regular, design: .default))
90 | .multilineTextAlignment(.leading)
91 | .padding(20)
92 | .background(.thinMaterial , in: RoundedRectangle(cornerRadius: 12))
93 | .padding(.top, 20)
94 | }
95 | }
96 | }
97 |
98 | if circuit.state == .simulate {
99 | Button {
100 | circuit.perLevelSolve()
101 | } label: {
102 | Label("Next Step", systemImage: "forward")
103 | .labelStyle(ActionLabelStyle())
104 | }
105 | .buttonStyle(ActionButtonStyle())
106 | Divider()
107 | .frame(height: 20)
108 | Button {
109 | circuit.hardReset()
110 | } label: {
111 | Label("Reset", systemImage: "gobackward")
112 | .labelStyle(ActionLabelStyle())
113 | }
114 | .buttonStyle(ActionButtonStyle())
115 | }
116 |
117 | }
118 |
119 | .background(.thinMaterial, in: Capsule())
120 | .clipShape(Capsule())
121 | .transaction { transaction in
122 | transaction.animation = .easeInOut
123 | }
124 | .fileImporter(
125 | isPresented: $importing,
126 | allowedContentTypes: [.circ, .json, .init(filenameExtension: "circ")!]
127 | ) { result in
128 | do {
129 | let url: URL = try result.get()
130 | let _ = url.startAccessingSecurityScopedResource()
131 | let jsonData = try Data(contentsOf: url)
132 | url.stopAccessingSecurityScopedResource()
133 | let decoder = JSONDecoder()
134 | let circuitData = try decoder.decode(CircuitData.self, from: jsonData)
135 | let newCircuit = circuitData.makeCircuit()
136 | envi.board.addCircuit(newCircuit)
137 | envi.selectedCircuit = newCircuit
138 | } catch {
139 | showImportErrorAlert = true
140 | }
141 | }
142 | .fileExporter(isPresented: $exporting,
143 | document: CircuitDocument(circuitData: circuit.makeCircuitData()),
144 | contentType: .circ,
145 | defaultFilename: circuit.name + ".circ",
146 | onCompletion: { result in
147 | switch result {
148 | case .success:
149 | showExportSuccessAlert = true
150 | case .failure:
151 | showExportErrorAlert = true
152 | }
153 | })
154 | .alert("Unable to import .circ file.", isPresented: $showImportErrorAlert) {
155 | Button("OK", role: .cancel) {
156 | showImportErrorAlert = false
157 | }
158 | }
159 | .alert("Unable to export .circ file.", isPresented: $showExportErrorAlert) {
160 | Button("OK", role: .cancel) {
161 | showExportErrorAlert = false
162 | }
163 | }
164 | .alert("The .circ file was exported successfully.", isPresented: $showExportSuccessAlert) {
165 | Button("OK", role: .cancel) {
166 | showExportSuccessAlert = false
167 | }
168 | }
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Pickers/CircuitPickerView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct CircuitPickerView: View {
4 | @EnvironmentObject var envi: LogicEnvironment
5 |
6 | public var body: some View {
7 | ScrollView(.horizontal, showsIndicators: false) {
8 | HStack(spacing: 8) {
9 | ForEach($envi.board.circuits) { $circuit in
10 | CircuitPickerUnitView(circuit: $circuit)
11 | }
12 | Button {
13 | envi.board.addCircuit()
14 | } label: {
15 | Label("Add Circuit", systemImage: "plus")
16 | .labelStyle(.iconOnly)
17 | .font(.system(size: 20, weight: .bold))
18 | }
19 | .buttonStyle(CircleButtonStyle())
20 |
21 | }
22 | .padding(12)
23 | .background(.ultraThinMaterial)
24 | .clipShape(RoundedRectangle(cornerRadius: 40))
25 | }
26 | .clipShape(RoundedRectangle(cornerRadius: 40))
27 | }
28 | }
29 |
30 |
31 | struct CircuitPickerUnitView: View {
32 | @EnvironmentObject var envi: LogicEnvironment
33 | @Binding var circuit: Circuit
34 | @State var showRename: Bool = false
35 | @State var newName: String = ""
36 | @State var showDelete: Bool = false
37 |
38 | var body: some View {
39 | Button {
40 | envi.selectedCircuit = circuit
41 | } label: {
42 | Text(circuit.name.uppercased())
43 | .font(.system(size: 16, weight: .bold, design: .monospaced))
44 | .lineLimit(1)
45 | .fixedSize()
46 |
47 | }
48 | .buttonStyle(CircuitButtonStyle(isChosen: envi.selectedCircuit == circuit))
49 | .contextMenu(menuItems: {
50 | Button {
51 | showRename = true
52 | newName = circuit.name
53 | } label: {
54 | Label("Rename", systemImage: "text.cursor")
55 | }.disabled(circuit.isExample)
56 | Button(role: .destructive) {
57 | showDelete = true
58 | } label: {
59 | Label("Delete", systemImage: "trash")
60 | }.disabled(circuit.isExample)
61 | })
62 | .shadow(color: Color(red: 0.0, green: 0.4, blue: 1.0).opacity(envi.selectedCircuit == circuit ? 0.3 : 0),
63 | radius: 10,
64 | x: 5,
65 | y: -5)
66 | .shadow(color: .black.opacity(envi.selectedCircuit == circuit ? 0.5 : 0), radius: envi.selectedCircuit == circuit ? 8 : 0, x: -4, y: 4)
67 | .alert("Rename Circuit", isPresented: $showRename, actions: {
68 | TextField(circuit.name, text: $newName)
69 | Button("Cancel", role: .cancel) {
70 | showRename = false
71 | }
72 | Button("Rename") {
73 | circuit.name = newName
74 | newName = ""
75 | showRename = false
76 | }
77 | }, message: {
78 | Text("Enter the new name for " + circuit.name)
79 | })
80 | .alert("Delete Circuit", isPresented: $showDelete, actions: {
81 | Button("Cancel", role: .cancel) {
82 | showDelete = false
83 | }
84 | Button("Delete", role: .destructive) {
85 | envi.board.removeCircuit(circuit)
86 | envi.selectedCircuit = envi.board.mainCircuit
87 | showDelete = false
88 | }
89 | }, message: {
90 | Text("Are you sure you want to delete " + circuit.name + "?")
91 | })
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Pickers/DevicePickerView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct DevicePickerView: View {
4 | var deviceNames: [DeviceName] = [.PUSHBUTTON, .DISPLAY, .AND, .NAND, .OR, .NOR, .XOR, .XNOR, .NOT, .BUFFER]
5 |
6 | public var body: some View {
7 | ScrollView(.vertical ,showsIndicators: false) {
8 | VStack{
9 | ForEach(deviceNames) { deviceName in
10 | DeviceView(deviceName: deviceName)
11 | }
12 | }
13 | .frame(width: 100)
14 | .padding(10)
15 | }
16 | .background(.ultraThinMaterial)
17 | .clipShape(RoundedRectangle(cornerRadius: 20))
18 | .frame(maxHeight: 800)
19 | }
20 | }
21 |
22 | struct DeviceView: View {
23 | var deviceName: DeviceName
24 |
25 | var body: some View {
26 | VStack {
27 | Image(deviceName.imageName)
28 | .resizable()
29 | .aspectRatio(contentMode: .fit)
30 | Text(deviceName.deviceName)
31 | .lineLimit(1)
32 | .foregroundColor(.white)
33 | .font(.system(size: 12, weight: .bold, design: .monospaced))
34 | .padding(4)
35 | .padding(.horizontal, 4)
36 | .background(Color(red: 0.4, green: 0.4, blue: 0.4), in: RoundedRectangle(cornerRadius: 10))
37 | }
38 | .padding(8)
39 | .background(Color(red: 0.25, green: 0.25, blue: 0.25), in: RoundedRectangle(cornerRadius: 12))
40 | .draggable(deviceName.deviceName) {
41 | Image(deviceName.imageName)
42 | .resizable()
43 | .aspectRatio(contentMode: .fit)
44 | .frame(maxWidth: 100, maxHeight: 100)
45 | }
46 | .hoverEffect(.lift)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/Pickers/SystemPickerView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct SystemPickerView: View {
4 | @EnvironmentObject var envi: LogicEnvironment
5 | @ObservedObject var circuit: Circuit
6 | var body: some View {
7 | VStack() {
8 | Spacer()
9 | VStack() {
10 | Spacer()
11 | VStack {
12 | Button {
13 | circuit.state = .file
14 | } label: {
15 | Label("File Picker", systemImage: "doc.fill")
16 | .labelStyle(.iconOnly)
17 | .imageScale(.large)
18 | .font(.system(size: 20))
19 | }
20 | .buttonStyle(SystemButtonStyle(isChosen: circuit.state == .file))
21 | .keyboardShortcut("s", modifiers: .command)
22 |
23 | Button {
24 | circuit.state = .add
25 | } label: {
26 | Label("Add", systemImage: "memorychip")
27 | .rotationEffect(.degrees(90))
28 | .labelStyle(.iconOnly)
29 | .imageScale(.large)
30 | .font(.system(size: 20, weight: .bold))
31 |
32 | }
33 | .buttonStyle(SystemButtonStyle(isChosen: circuit.state == .add))
34 | .keyboardShortcut("a", modifiers: .command)
35 |
36 | Button {
37 | circuit.state = .wire
38 | } label: {
39 | Label("Wire", systemImage: "app.connected.to.app.below.fill")
40 | .rotationEffect(.degrees(90))
41 | .labelStyle(.iconOnly)
42 | .imageScale(.large)
43 | .font(.system(size: 20, weight: .bold))
44 | }
45 | .keyboardShortcut("w", modifiers: .command)
46 | .buttonStyle(SystemButtonStyle(isChosen: circuit.state == .wire))
47 |
48 | Button {
49 | circuit.state = .simulate
50 | } label: {
51 | Label("Simulate", systemImage: "play.fill")
52 | .labelStyle(.iconOnly)
53 | .imageScale(.large)
54 | .font(.system(size: 20))
55 | }
56 | .buttonStyle(SystemButtonStyle(isChosen: circuit.state == .simulate))
57 | .keyboardShortcut(.defaultAction)
58 |
59 | }
60 | .padding(10)
61 | .background(.ultraThinMaterial)
62 | .clipShape(RoundedRectangle(cornerSize: .init(width: 100, height: 100)))
63 | .frame(alignment: .leading)
64 |
65 |
66 | Spacer()
67 |
68 | Button {
69 | envi.showDock.toggle()
70 | } label: {
71 | Label("", systemImage: "rectangle.stack.fill")
72 | .labelStyle(.iconOnly)
73 | .imageScale(.large)
74 | .font(.system(size: 20))
75 | }
76 | .buttonStyle(SystemButtonStyle(isChosen: envi.showDock))
77 | .keyboardShortcut("b")
78 | .padding(10)
79 | .background(.ultraThinMaterial)
80 | .clipShape(RoundedRectangle(cornerSize: .init(width: 100, height: 100)))
81 | .frame(alignment: .bottomLeading)
82 |
83 | }
84 | }
85 | }
86 | }
87 |
88 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/ViewStyles/ButtonStyles/ActionButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct ActionButtonStyle: ButtonStyle {
4 | public func makeBody(configuration: Configuration) -> some View {
5 | configuration.label
6 | .foregroundColor(.primary)
7 | .hoverEffect(.highlight)
8 | .padding(4)
9 | .padding(.horizontal, 4)
10 | .background(
11 | Capsule()
12 | .fill(configuration.isPressed ? Color.primary.opacity(0.3) : Color.clear)
13 | )
14 | .padding(4)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/ViewStyles/ButtonStyles/CircleButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct CircleButtonStyle: ButtonStyle {
4 | public func makeBody(configuration: Configuration) -> some View {
5 | configuration.label
6 | .foregroundColor(.white)
7 | .frame(width: 36, height: 36)
8 |
9 | .foregroundColor(.white)
10 | .background(
11 | Circle()
12 | .fill(configuration.isPressed ? Color(red: 0.4, green: 0.4, blue: 0.4) : Color.clear)
13 | .offset(x: 12, y:-12)
14 | .blur(radius: 12)
15 |
16 | )
17 | .background(Color(red: 0.25, green: 0.25, blue: 0.25))
18 | .clipShape(Circle())
19 | .hoverEffect(.lift)
20 | .shadow(color: .black.opacity(configuration.isPressed ? 0.5 : 0), radius: configuration.isPressed ? 6 : 0, x: -4, y: 4)
21 | .transaction { transaction in
22 | transaction.animation = nil
23 | }
24 | .animation(.default, value: configuration.isPressed)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/ViewStyles/ButtonStyles/CircuitButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct CircuitButtonStyle: ButtonStyle {
4 | public var isChosen: Bool
5 | public func makeBody(configuration: Configuration) -> some View {
6 | configuration.label
7 | .padding([.horizontal], 8)
8 | .padding(8)
9 | .foregroundColor(.white)
10 | .background(
11 | RoundedRectangle(cornerRadius: 12)
12 | .fill(isChosen ? Color(red: 0.0, green: 0.52, blue: 1.0) : Color.clear)
13 | .offset(x: 12, y:-12)
14 | .blur(radius: 16)
15 | )
16 | .background(isChosen ? Color(red: 0.0, green: 0.14, blue: 1.0) : Color(red: 0.25, green: 0.25, blue: 0.25))
17 | .clipShape(RoundedRectangle(cornerRadius: 32))
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/ViewStyles/ButtonStyles/NoFlickerButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct NoFlickerButtonStyle: ButtonStyle {
4 | public func makeBody(configuration: Configuration) -> some View {
5 | configuration.label
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/ViewStyles/ButtonStyles/SystemButtonStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct SystemButtonStyle: ButtonStyle {
4 | public var isChosen: Bool
5 |
6 | public func makeBody(configuration: Configuration) -> some View {
7 | configuration.label
8 | .frame(width: 32, height: 32, alignment: .center)
9 | .aspectRatio(1.0, contentMode: .fit)
10 | .padding(8)
11 | .foregroundColor(.white)
12 | .background(
13 | Circle()
14 | .fill(isChosen ? Color(red: 0.0, green: 0.52, blue: 1.0) : Color.clear)
15 | .offset(x: 12, y:-12)
16 | .blur(radius: 12)
17 |
18 | )
19 | .background(isChosen ? Color(red: 0.0, green: 0.14, blue: 1.0) : Color(red: 0.25, green: 0.25, blue: 0.25))
20 | .clipShape(Circle())
21 | .animation(.easeIn, value: isChosen)
22 | .shadow(color: Color(red: 0.0, green: 0.4, blue: 1.0).opacity(isChosen ? 0.3 : 0),
23 | radius: 10,
24 | x: 5,
25 | y: -5)
26 | .shadow(color: .black.opacity(isChosen ? 0.5 : 0),
27 | radius: 8,
28 | x: -4,
29 | y: 4)
30 |
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LogicBoard.swiftpm/Views/ViewStyles/LabelStyles/ActionLabelStyle.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public struct ActionLabelStyle: LabelStyle {
4 | public func makeBody(configuration: Configuration) -> some View {
5 | HStack {
6 | configuration.icon
7 | .font(.system(size: 16, weight: .regular, design: .default))
8 | .frame(width: 12, height: 12)
9 | .padding(4)
10 | .padding(.leading, 2)
11 |
12 | configuration.title
13 | .fixedSize()
14 | .lineLimit(1)
15 | .font(.system(size: 13, weight: .regular, design: .default))
16 | .padding(.trailing, 4)
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LogicBoard
2 |
3 | 
4 |
5 | ## Introduction
6 |
7 | Hello, welcome to Jose Adolfo Talactac's submission for WWDC23 Swift Student Challenge, LogicBoard! LogicBoard is an interactive sandbox experience that allows you to design and simulate digital logic circuit. LogicBoard is written entirely in Swift and built with SwiftUI, UIKit, and SpriteKit.
8 |
9 | ## Credits
10 |
11 | - [IBM Plex font](https://github.com/IBM/plex) designed by Mike Abbink, Paul van der Laan, and Pieter van Rosmalen for IBM is used in the Playground App's thumbnail and the logic gate image assets. The font is used under the [SIL Open Font License 1.1](https://github.com/IBM/plex/blob/master/LICENSE.txt).
12 | - [Popovers](https://github.com/aheze/Popovers) by Andrew Zheng was used to make a popover modal consistent for all view sizes. The package is used under the [MIT license](https://github.com/aheze/Popovers/blob/main/LICENSE).
13 | - San Francisco font, SF Symbols icons, and the Apple platforms development frameworks used in this Playground App is accessed and used fairly within the license set by Apple Inc. for the development of software for Apple platforms.
14 | - All assets that aren’t attributed to another party are made by the creator of this Playground App, Jose Adolfo Talactac, including app icon, Playground thumbnail, and the app image assets.
15 |
16 | ## Screenshots
17 | ### Adding Devices
18 | 
19 | ### Drawing Wires
20 | 
21 | ### Simulating the Circuit
22 | 
23 |
--------------------------------------------------------------------------------
/Screenshots/Screenshot1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/Screenshots/Screenshot1.gif
--------------------------------------------------------------------------------
/Screenshots/Screenshot2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/Screenshots/Screenshot2.gif
--------------------------------------------------------------------------------
/Screenshots/Screenshot3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/devjoseadolfo/LogicBoard/aa048e2df544cc75a287c67276ddf87e7fa08cfd/Screenshots/Screenshot3.gif
--------------------------------------------------------------------------------