├── GitHub
├── banner.jpg
├── Design.sketch
├── IMG_0881.jpeg
├── IMG_0882.jpeg
├── IMG_0884.jpeg
├── IMG_0885.jpeg
├── IMG_0887.jpeg
├── IMG_0888.jpeg
└── IMG_0889.jpeg
├── NodeEditor
├── Assets.xcassets
│ ├── Contents.json
│ ├── base.imageset
│ │ ├── base.png
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── 100.png
│ │ ├── 114.png
│ │ ├── 120.png
│ │ ├── 144.png
│ │ ├── 152.png
│ │ ├── 167.png
│ │ ├── 180.png
│ │ ├── 20.png
│ │ ├── 29.png
│ │ ├── 40.png
│ │ ├── 50.png
│ │ ├── 57.png
│ │ ├── 58.png
│ │ ├── 60.png
│ │ ├── 72.png
│ │ ├── 76.png
│ │ ├── 80.png
│ │ ├── 87.png
│ │ ├── 1024.png
│ │ └── Contents.json
│ ├── pipe-green.imageset
│ │ ├── pipe-green.png
│ │ └── Contents.json
│ ├── background-day.imageset
│ │ ├── background-day.png
│ │ └── Contents.json
│ ├── background-night.imageset
│ │ ├── background-night.png
│ │ └── Contents.json
│ ├── yellowbird-upflap.imageset
│ │ ├── yellowbird-upflap.png
│ │ └── Contents.json
│ ├── yellowbird-midflap.imageset
│ │ ├── yellowbird-midflap.png
│ │ └── Contents.json
│ ├── yellowbird-downflap.imageset
│ │ ├── yellowbird-downflap.png
│ │ └── Contents.json
│ └── AccentColor.colorset
│ │ └── Contents.json
├── Resources
│ └── FlappyBird.sks
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Helper
│ ├── String+Identifiable.swift
│ ├── Collection+Nil.swift
│ ├── Any+Equatable.swift
│ ├── Bundle+Version.swift
│ ├── SwiftUI+Conditional.swift
│ ├── CoreGraphics+Hashable.swift
│ ├── UserDefaults+SwiftUI.swift
│ ├── Class+Runtime.swift
│ └── CoreGraphics+Math.swift
├── NodeEditor.entitlements
├── Data
│ ├── NodePorts
│ │ ├── IntNodeDataPort.swift
│ │ ├── SKNodeNodeDataPort.swift
│ │ ├── CGFloatNodeDataPort.swift
│ │ └── CGVectorNodeDataPort.swift
│ ├── Nodes
│ │ ├── StartNode.swift
│ │ ├── UpdateNode.swift
│ │ ├── BirdNode.swift
│ │ ├── PipeNode.swift
│ │ ├── GetTouchNode.swift
│ │ ├── NewFrameNode.swift
│ │ ├── TriggerNode.swift
│ │ ├── RandomNode.swift
│ │ ├── SetFloatNode.swift
│ │ ├── ApplyImpulseNode.swift
│ │ ├── SetPositionNode.swift
│ │ ├── PrintNode.swift
│ │ ├── GetPositionNode.swift
│ │ ├── AddFloatNode.swift
│ │ ├── ComparsionNode.swift
│ │ └── LoopFloatNode.swift
│ ├── Environment.swift
│ ├── NodeCanvasData.swift
│ ├── NodePageData.swift
│ ├── NodePortConnectionData.swift
│ └── NodePages
│ │ └── NodePageDataChapterZero.swift
├── Manager
│ ├── EnvironmentManager.swift
│ ├── PageManager.swift
│ ├── PreferenceManager.swift
│ ├── RenderManager.swift
│ └── BaseManager.swift
├── View
│ ├── NodeCanvas
│ │ ├── NodeCanvasDocView.swift
│ │ ├── NodeCanvasLiveView.swift
│ │ ├── NodeCanvasMinimapView.swift
│ │ ├── NodeCanvasTitleIndicatorView.swift
│ │ ├── NodeCanvasNavigationView.swift
│ │ ├── NodeAddSelectionView.swift
│ │ ├── NodeCanvasToolbarView.swift
│ │ └── NodePortView.swift
│ ├── Control
│ │ └── ToggleButtonView.swift
│ ├── Wrapper
│ │ └── SpriteViewWrapper.swift
│ └── More
│ │ └── MoreNavigationView.swift
└── App
│ └── NodeEditorApp.swift
├── Playgrounds
├── Pegboard.swiftpm.zip
└── Pegboard.swiftpm
│ ├── App
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── base.imageset
│ │ │ ├── base.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── 100.png
│ │ │ ├── 1024.png
│ │ │ ├── 114.png
│ │ │ ├── 120.png
│ │ │ ├── 144.png
│ │ │ ├── 152.png
│ │ │ ├── 167.png
│ │ │ ├── 180.png
│ │ │ ├── 20.png
│ │ │ ├── 29.png
│ │ │ ├── 40.png
│ │ │ ├── 50.png
│ │ │ ├── 57.png
│ │ │ ├── 58.png
│ │ │ ├── 60.png
│ │ │ ├── 72.png
│ │ │ ├── 76.png
│ │ │ ├── 80.png
│ │ │ ├── 87.png
│ │ │ └── Contents.json
│ │ ├── pipe-green.imageset
│ │ │ ├── pipe-green.png
│ │ │ └── Contents.json
│ │ ├── background-day.imageset
│ │ │ ├── background-day.png
│ │ │ └── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── background-night.imageset
│ │ │ ├── background-night.png
│ │ │ └── Contents.json
│ │ ├── yellowbird-upflap.imageset
│ │ │ ├── yellowbird-upflap.png
│ │ │ └── Contents.json
│ │ ├── yellowbird-downflap.imageset
│ │ │ ├── yellowbird-downflap.png
│ │ │ └── Contents.json
│ │ └── yellowbird-midflap.imageset
│ │ │ ├── yellowbird-midflap.png
│ │ │ └── Contents.json
│ ├── Resources
│ │ └── FlappyBird.sks
│ ├── Helper
│ │ ├── String+Identifiable.swift
│ │ ├── Collection+Nil.swift
│ │ ├── Any+Equatable.swift
│ │ ├── Bundle+Version.swift
│ │ ├── SwiftUI+Conditional.swift
│ │ ├── CoreGraphics+Hashable.swift
│ │ ├── UserDefaults+SwiftUI.swift
│ │ ├── Class+Runtime.swift
│ │ └── CoreGraphics+Math.swift
│ ├── Data
│ │ ├── NodePorts
│ │ │ ├── IntNodeDataPort.swift
│ │ │ ├── SKNodeNodeDataPort.swift
│ │ │ ├── CGFloatNodeDataPort.swift
│ │ │ └── CGVectorNodeDataPort.swift
│ │ ├── Nodes
│ │ │ ├── StartNode.swift
│ │ │ ├── UpdateNode.swift
│ │ │ ├── BirdNode.swift
│ │ │ ├── PipeNode.swift
│ │ │ ├── GetTouchNode.swift
│ │ │ ├── NewFrameNode.swift
│ │ │ ├── TriggerNode.swift
│ │ │ ├── RandomNode.swift
│ │ │ ├── SetFloatNode.swift
│ │ │ ├── ApplyImpulseNode.swift
│ │ │ ├── SetPositionNode.swift
│ │ │ ├── PrintNode.swift
│ │ │ ├── GetPositionNode.swift
│ │ │ ├── AddFloatNode.swift
│ │ │ ├── ComparsionNode.swift
│ │ │ └── LoopFloatNode.swift
│ │ ├── Environment.swift
│ │ ├── NodeCanvasData.swift
│ │ ├── NodePageData.swift
│ │ ├── NodePortConnectionData.swift
│ │ └── NodePages
│ │ │ └── NodePageDataChapterZero.swift
│ ├── Manager
│ │ ├── PageManager.swift
│ │ ├── EnvironmentManager.swift
│ │ ├── PreferenceManager.swift
│ │ ├── RenderManager.swift
│ │ └── BaseManager.swift
│ ├── View
│ │ ├── NodeCanvas
│ │ │ ├── NodeCanvasDocView.swift
│ │ │ ├── NodeCanvasLiveView.swift
│ │ │ ├── NodeCanvasMinimapView.swift
│ │ │ ├── NodeCanvasTitleIndicatorView.swift
│ │ │ ├── NodeCanvasNavigationView.swift
│ │ │ ├── NodeAddSelectionView.swift
│ │ │ ├── NodeCanvasToolbarView.swift
│ │ │ └── NodePortView.swift
│ │ ├── Control
│ │ │ └── ToggleButtonView.swift
│ │ ├── Wrapper
│ │ │ └── SpriteViewWrapper.swift
│ │ └── More
│ │ │ └── MoreNavigationView.swift
│ └── App
│ │ └── NodeEditorApp.swift
│ ├── Guide
│ ├── Resources
│ │ └── intro.png
│ └── Walkthrough.tutorial
│ ├── .swiftpm
│ └── playgrounds
│ │ ├── DocumentThumbnail.png
│ │ ├── Workspace.plist
│ │ ├── DocumentThumbnail.plist
│ │ └── CachedManifest.plist
│ └── Package.swift
├── generate_playground.sh
├── ScriptNode.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── xcuserdata
│ └── fincher.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── xcshareddata
│ └── xcschemes
│ └── ScriptNode.xcscheme
└── README.md
/GitHub/banner.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/banner.jpg
--------------------------------------------------------------------------------
/GitHub/Design.sketch:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/Design.sketch
--------------------------------------------------------------------------------
/GitHub/IMG_0881.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/IMG_0881.jpeg
--------------------------------------------------------------------------------
/GitHub/IMG_0882.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/IMG_0882.jpeg
--------------------------------------------------------------------------------
/GitHub/IMG_0884.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/IMG_0884.jpeg
--------------------------------------------------------------------------------
/GitHub/IMG_0885.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/IMG_0885.jpeg
--------------------------------------------------------------------------------
/GitHub/IMG_0887.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/IMG_0887.jpeg
--------------------------------------------------------------------------------
/GitHub/IMG_0888.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/IMG_0888.jpeg
--------------------------------------------------------------------------------
/GitHub/IMG_0889.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/GitHub/IMG_0889.jpeg
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm.zip
--------------------------------------------------------------------------------
/NodeEditor/Resources/FlappyBird.sks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Resources/FlappyBird.sks
--------------------------------------------------------------------------------
/NodeEditor/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/base.imageset/base.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/base.imageset/base.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/Guide/Resources/intro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/Guide/Resources/intro.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Resources/FlappyBird.sks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Resources/FlappyBird.sks
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/pipe-green.imageset/pipe-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/pipe-green.imageset/pipe-green.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/background-day.imageset/background-day.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/background-day.imageset/background-day.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/base.imageset/base.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/base.imageset/base.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/background-night.imageset/background-night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/background-night.imageset/background-night.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/yellowbird-upflap.imageset/yellowbird-upflap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/yellowbird-upflap.imageset/yellowbird-upflap.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/yellowbird-midflap.imageset/yellowbird-midflap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/yellowbird-midflap.imageset/yellowbird-midflap.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/yellowbird-downflap.imageset/yellowbird-downflap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/NodeEditor/Assets.xcassets/yellowbird-downflap.imageset/yellowbird-downflap.png
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/pipe-green.imageset/pipe-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/pipe-green.imageset/pipe-green.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/background-day.imageset/background-day.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/background-day.imageset/background-day.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/background-night.imageset/background-night.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/background-night.imageset/background-night.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-upflap.imageset/yellowbird-upflap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-upflap.imageset/yellowbird-upflap.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-downflap.imageset/yellowbird-downflap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-downflap.imageset/yellowbird-downflap.png
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-midflap.imageset/yellowbird-midflap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JustinFincher/WWDC2022-SwiftUINodeEditor/HEAD/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-midflap.imageset/yellowbird-midflap.png
--------------------------------------------------------------------------------
/generate_playground.sh:
--------------------------------------------------------------------------------
1 | cd Playgrounds/Pegboard.swiftpm/App
2 | ls -1 | egrep -v "^(Package.swift)$" # | xargs rm -r
3 | ls -1 | egrep -v "^(Package.swift)$" | xargs rm -r
4 | rsync -av --exclude='NodeEditor.entitlements' --exclude='Preview Content' ../../../NodeEditor/ ./
5 | cd ../../..
--------------------------------------------------------------------------------
/ScriptNode.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/String+Identifiable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Identifiable.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String: Identifiable {
11 | public typealias ID = Int
12 | public var id: Int {
13 | return hash
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/ScriptNode.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/Collection+Nil.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection+Nil.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/22/22.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Collection {
11 | subscript (safe index: Index) -> Element? {
12 | return indices.contains(index) ? self[index] : nil
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/String+Identifiable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+Identifiable.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 |
10 | extension String: Identifiable {
11 | public typealias ID = Int
12 | public var id: Int {
13 | return hash
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/Collection+Nil.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Collection+Nil.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/22/22.
6 | //
7 |
8 | import Foundation
9 |
10 | extension Collection {
11 | subscript (safe index: Index) -> Element? {
12 | return indices.contains(index) ? self[index] : nil
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/NodeEditor/NodeEditor.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/Any+Equatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Any+Equatable.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 |
10 | func equals(_ x : Any, _ y : Any) -> Bool {
11 | guard x is AnyHashable else { return false }
12 | guard y is AnyHashable else { return false }
13 | return (x as! AnyHashable) == (y as! AnyHashable)
14 | }
15 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/Any+Equatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Any+Equatable.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 |
10 | func equals(_ x : Any, _ y : Any) -> Bool {
11 | guard x is AnyHashable else { return false }
12 | guard y is AnyHashable else { return false }
13 | return (x as! AnyHashable) == (y as! AnyHashable)
14 | }
15 |
--------------------------------------------------------------------------------
/NodeEditor/Data/NodePorts/IntNodeDataPort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntNodeDataPort.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class IntNodeDataPort: NodeDataPortData {
11 | override class func getDefaultValue() -> Any? {
12 | return 0
13 | }
14 |
15 | override class func getDefaultValueType() -> Any.Type {
16 | Int.self
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/base.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "base.png",
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 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/Bundle+Version.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | extension Bundle {
3 | var versionString: String? {
4 | return infoDictionary?["CFBundleShortVersionString"] as? String
5 | }
6 | var buildString: String? {
7 | return infoDictionary?["CFBundleVersion"] as? String
8 | }
9 | var displayName: String? {
10 | return object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/NodePorts/IntNodeDataPort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // IntNodeDataPort.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class IntNodeDataPort: NodeDataPortData {
11 | override class func getDefaultValue() -> Any? {
12 | return 0
13 | }
14 |
15 | override class func getDefaultValueType() -> Any.Type {
16 | Int.self
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/pipe-green.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "pipe-green.png",
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 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/background-day.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "background-day.png",
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 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/.swiftpm/playgrounds/Workspace.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AppSettings
6 |
7 | appIconPlaceholderGlyphName
8 | earth
9 | appSettingsVersion
10 | 1
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/background-night.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "background-night.png",
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 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/yellowbird-upflap.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "yellowbird-upflap.png",
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 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/base.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "base.png",
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 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/Bundle+Version.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | extension Bundle {
3 | var versionString: String? {
4 | return infoDictionary?["CFBundleShortVersionString"] as? String
5 | }
6 | var buildString: String? {
7 | return infoDictionary?["CFBundleVersion"] as? String
8 | }
9 | var displayName: String? {
10 | return object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/yellowbird-downflap.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "yellowbird-downflap.png",
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 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/yellowbird-midflap.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "yellowbird-midflap.png",
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 |
--------------------------------------------------------------------------------
/NodeEditor/Data/NodePorts/SKNodeNodeDataPort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SKSpriteNodeNodeDataPort.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 |
11 | class SKNodeNodeDataPort: NodeDataPortData {
12 | override class func getDefaultValue() -> Any? {
13 | return SKNode()
14 | }
15 |
16 | override class func getDefaultValueType() -> Any.Type {
17 | SKNode.self
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/pipe-green.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "pipe-green.png",
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 |
--------------------------------------------------------------------------------
/NodeEditor/Data/NodePorts/CGFloatNodeDataPort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGFloatNodeDataPort.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import CoreGraphics
10 |
11 | class CGFloatNodeDataPort: NodeDataPortData {
12 | override class func getDefaultValue() -> Any? {
13 | return CGFloat(0.0)
14 | }
15 |
16 | override class func getDefaultValueType() -> Any.Type {
17 | CGFloat.self
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/background-day.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "background-day.png",
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 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/background-night.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "background-night.png",
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 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-midflap.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "yellowbird-midflap.png",
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 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-upflap.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "yellowbird-upflap.png",
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 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/yellowbird-downflap.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "yellowbird-downflap.png",
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 |
--------------------------------------------------------------------------------
/NodeEditor/Data/NodePorts/CGVectorNodeDataPort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GKVectorNodeDataPort.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 |
11 |
12 | class CGVectorNodeDataPort: NodeDataPortData {
13 |
14 | override class func getDefaultValue() -> Any? {
15 | return CGVector.zero
16 | }
17 |
18 | override class func getDefaultValueType() -> Any.Type {
19 | CGVector.self
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/NodePorts/SKNodeNodeDataPort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SKSpriteNodeNodeDataPort.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 |
11 | class SKNodeNodeDataPort: NodeDataPortData {
12 | override class func getDefaultValue() -> Any? {
13 | return SKNode()
14 | }
15 |
16 | override class func getDefaultValueType() -> Any.Type {
17 | SKNode.self
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/NodePorts/CGFloatNodeDataPort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGFloatNodeDataPort.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import CoreGraphics
10 |
11 | class CGFloatNodeDataPort: NodeDataPortData {
12 | override class func getDefaultValue() -> Any? {
13 | return CGFloat(0.0)
14 | }
15 |
16 | override class func getDefaultValueType() -> Any.Type {
17 | CGFloat.self
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/SwiftUI+Conditional.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUI+Conditional.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | extension View {
12 |
13 | @ViewBuilder func conditionalModifier(_ condition: Bool,
14 | transform: (Self) -> Content) -> some View {
15 | if condition {
16 | transform(self)
17 | } else {
18 | self
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/NodePorts/CGVectorNodeDataPort.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GKVectorNodeDataPort.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 |
11 |
12 | class CGVectorNodeDataPort: NodeDataPortData {
13 |
14 | override class func getDefaultValue() -> Any? {
15 | return CGVector.zero
16 | }
17 |
18 | override class func getDefaultValueType() -> Any.Type {
19 | CGVector.self
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/SwiftUI+Conditional.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUI+Conditional.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | extension View {
12 |
13 | @ViewBuilder func conditionalModifier(_ condition: Bool,
14 | transform: (Self) -> Content) -> some View {
15 | if condition {
16 | transform(self)
17 | } else {
18 | self
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/NodeEditor/Manager/EnvironmentManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnvironmentManager.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/17/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class EnvironmentManager : BaseManager {
11 |
12 | static let instance = EnvironmentManager()
13 |
14 | override class var shared: EnvironmentManager {
15 | return instance
16 | }
17 |
18 | let environment : Environment = Environment()
19 |
20 | override func setup() {
21 |
22 | }
23 |
24 | override func destroy() {
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/NodeEditor/Manager/PageManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageManager.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | class PageManager : BaseManager {
12 |
13 | static let instance = PageManager()
14 |
15 | override class var shared: PageManager {
16 | return instance
17 | }
18 |
19 | @ObservedObject var nodePageData : NodePageData = NodePageData()
20 |
21 | override func setup() {
22 |
23 | }
24 |
25 | override func destroy() {
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/NodeEditor/Manager/PreferenceManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreferenceManager.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class PreferenceManager : BaseManager {
11 |
12 | static let instance = PreferenceManager()
13 |
14 | override class var shared: PreferenceManager {
15 | return instance
16 | }
17 |
18 | let userDefaults : UserDefaults = UserDefaults.standard
19 |
20 | override func setup() {
21 |
22 | }
23 |
24 | override func destroy() {
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Manager/PageManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PageManager.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | class PageManager : BaseManager {
12 |
13 | static let instance = PageManager()
14 |
15 | override class var shared: PageManager {
16 | return instance
17 | }
18 |
19 | @ObservedObject var nodePageData : NodePageData = NodePageData()
20 |
21 | override func setup() {
22 |
23 | }
24 |
25 | override func destroy() {
26 |
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/StartNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StartNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class StartNode : NodeData {
11 |
12 | class override func getDefaultExposedToUser() -> Bool {
13 | false
14 | }
15 |
16 | class override func getDefaultTitle() -> String {
17 | "Start"
18 | }
19 |
20 | class override func getDefaultControlOutPorts() -> [NodeControlPortData] {
21 | return [
22 | NodeControlPortData(portID: 0, name: "", direction: .output)
23 | ]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/UpdateNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UpdateNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class UpdateNode : NodeData {
11 |
12 | class override func getDefaultExposedToUser() -> Bool {
13 | false
14 | }
15 |
16 | class override func getDefaultTitle() -> String {
17 | "Update"
18 | }
19 |
20 | class override func getDefaultControlOutPorts() -> [NodeControlPortData] {
21 | return [
22 | NodeControlPortData(portID: 0, name: "", direction: .output)
23 | ]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DocumentThumbnailConfiguration
6 |
7 | accentColorHash
8 |
9 | MFe13OMFARMJ8HDqD5bDNSWxDg9LDdv8oq4TvGw4ZwM=
10 |
11 | appIconHash
12 |
13 | e3S0GKNS1nEIFzwgwbFrS3JrrYYGvmVxH/kk2/mkBnA=
14 |
15 | thumbnailIsPrerendered
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Manager/EnvironmentManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnvironmentManager.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/17/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class EnvironmentManager : BaseManager {
11 |
12 | static let instance = EnvironmentManager()
13 |
14 | override class var shared: EnvironmentManager {
15 | return instance
16 | }
17 |
18 | let environment : Environment = Environment()
19 |
20 | override func setup() {
21 |
22 | }
23 |
24 | override func destroy() {
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Manager/PreferenceManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PreferenceManager.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class PreferenceManager : BaseManager {
11 |
12 | static let instance = PreferenceManager()
13 |
14 | override class var shared: PreferenceManager {
15 | return instance
16 | }
17 |
18 | let userDefaults : UserDefaults = UserDefaults.standard
19 |
20 | override func setup() {
21 |
22 | }
23 |
24 | override func destroy() {
25 |
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/StartNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StartNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class StartNode : NodeData {
11 |
12 | class override func getDefaultExposedToUser() -> Bool {
13 | false
14 | }
15 |
16 | class override func getDefaultTitle() -> String {
17 | "Start"
18 | }
19 |
20 | class override func getDefaultControlOutPorts() -> [NodeControlPortData] {
21 | return [
22 | NodeControlPortData(portID: 0, name: "", direction: .output)
23 | ]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/UpdateNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UpdateNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class UpdateNode : NodeData {
11 |
12 | class override func getDefaultExposedToUser() -> Bool {
13 | false
14 | }
15 |
16 | class override func getDefaultTitle() -> String {
17 | "Update"
18 | }
19 |
20 | class override func getDefaultControlOutPorts() -> [NodeControlPortData] {
21 | return [
22 | NodeControlPortData(portID: 0, name: "", direction: .output)
23 | ]
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Pegboard
4 |
5 | > Pegboard is like Shortcuts from iOS and Blueprints from Unreal Engine combined into one.
6 |
7 | ---
8 |
9 | A creative workspace with node-based editor built-in. Purely written in SwiftUI.
10 |
11 | [Download](Playgrounds/Pegboard.swiftpm.zip) | [Dev Log](https://twitter.com/JustZht/status/1516384029636849665) | [Demo Video](https://youtu.be/B6D3y49WOEQ)
12 |
13 | ---
14 |
15 | Screenshots:
16 |
17 | 
18 | 
19 | 
20 | 
21 | 
22 | 
23 | 
24 |
25 |
--------------------------------------------------------------------------------
/NodeEditor/Manager/RenderManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RenderManager.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 |
10 | import Foundation
11 | import SwiftUI
12 | import SpriteKit
13 |
14 | class RenderManager : BaseManager, SKViewDelegate {
15 |
16 | static let instance = RenderManager()
17 |
18 | override class var shared: RenderManager {
19 | return instance
20 | }
21 |
22 | func view(_ view: SKView, shouldRenderAtTime time: TimeInterval) -> Bool {
23 | NotificationCenter.default.post(name: Notification.Name(rawValue: "newFrameRendered"), object: nil)
24 | return true
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Manager/RenderManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RenderManager.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 |
10 | import Foundation
11 | import SwiftUI
12 | import SpriteKit
13 |
14 | class RenderManager : BaseManager, SKViewDelegate {
15 |
16 | static let instance = RenderManager()
17 |
18 | override class var shared: RenderManager {
19 | return instance
20 | }
21 |
22 | func view(_ view: SKView, shouldRenderAtTime time: TimeInterval) -> Bool {
23 | NotificationCenter.default.post(name: Notification.Name(rawValue: "newFrameRendered"), object: nil)
24 | return true
25 | }
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/CoreGraphics+Hashable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CoreGraphics+Hashable.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | extension CGPoint: Hashable {
12 | public func hash(into hasher: inout Hasher) {
13 | hasher.combine(x)
14 | hasher.combine(y)
15 | }
16 | }
17 |
18 | extension CGSize : Hashable {
19 | public func hash(into hasher: inout Hasher) {
20 | hasher.combine(width)
21 | hasher.combine(height)
22 | }
23 | }
24 |
25 |
26 | extension CGRect: Hashable {
27 | public func hash(into hasher: inout Hasher) {
28 | hasher.combine(origin)
29 | hasher.combine(size)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/BirdNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BirdNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class BirdNode : NodeData {
11 |
12 | override class func getDefaultCategory() -> String {
13 | "Actor"
14 | }
15 |
16 | class override func getDefaultTitle() -> String {
17 | "Bird 🐦"
18 | }
19 |
20 | override class func getDefaultDataOutPorts() -> [NodeDataPortData] {
21 | return [
22 | SKNodeNodeDataPort(portID: 0, direction: .output, name: "", defaultValueGetter: {
23 | return PageManager.shared.nodePageData.bird
24 | }, defaultValueSetter: { _ in })
25 | ]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/CoreGraphics+Hashable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CoreGraphics+Hashable.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | extension CGPoint: Hashable {
12 | public func hash(into hasher: inout Hasher) {
13 | hasher.combine(x)
14 | hasher.combine(y)
15 | }
16 | }
17 |
18 | extension CGSize : Hashable {
19 | public func hash(into hasher: inout Hasher) {
20 | hasher.combine(width)
21 | hasher.combine(height)
22 | }
23 | }
24 |
25 |
26 | extension CGRect: Hashable {
27 | public func hash(into hasher: inout Hasher) {
28 | hasher.combine(origin)
29 | hasher.combine(size)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/NodeEditor/View/NodeCanvas/NodeCanvasDocView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasDocView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasDocView: View {
11 | @EnvironmentObject var nodePageData : NodePageData
12 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
13 |
14 | var body: some View {
15 | nodePageData.docView
16 | .frame(minWidth: 220,
17 | idealWidth: 320,
18 | maxWidth: .infinity,
19 | alignment: .top)
20 |
21 | }
22 | }
23 |
24 | struct NodeCanvasDocView_Previews: PreviewProvider {
25 | static var previews: some View {
26 | NodeCanvasDocView()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/BirdNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BirdNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 |
10 | class BirdNode : NodeData {
11 |
12 | override class func getDefaultCategory() -> String {
13 | "Actor"
14 | }
15 |
16 | class override func getDefaultTitle() -> String {
17 | "Bird 🐦"
18 | }
19 |
20 | override class func getDefaultDataOutPorts() -> [NodeDataPortData] {
21 | return [
22 | SKNodeNodeDataPort(portID: 0, direction: .output, name: "", defaultValueGetter: {
23 | return PageManager.shared.nodePageData.bird
24 | }, defaultValueSetter: { _ in })
25 | ]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/PipeNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PipeNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 |
10 | import Foundation
11 |
12 | class PipeNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Actor"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Pipe 🏙"
20 | }
21 |
22 | override class func getDefaultDataOutPorts() -> [NodeDataPortData] {
23 | return [
24 | SKNodeNodeDataPort(portID: 0, direction: .output, name: "", defaultValueGetter: {
25 | return PageManager.shared.nodePageData.pipe
26 | }, defaultValueSetter: { _ in })
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/NodeEditor/App/NodeEditorApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShaderNodeEditorApp.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/16/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Welcome to the Pegboard app.
11 | // Pegboard is like the Shortcuts App, but empowered by node-based visual scripting capabilities, so that it can even support real-time logic execution in game development.
12 |
13 | // TODO: Please execute the app using the run button rather than the sideview, as this app is designed to run best on full-screen mode.
14 |
15 | @main
16 | struct NodeEditorApp: App {
17 | var body: some Scene {
18 | WindowGroup {
19 | NodeCanvasNavigationView()
20 | .environmentObject(EnvironmentManager.shared.environment)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/NodeCanvas/NodeCanvasDocView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasDocView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasDocView: View {
11 | @EnvironmentObject var nodePageData : NodePageData
12 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
13 |
14 | var body: some View {
15 | nodePageData.docView
16 | .frame(minWidth: 220,
17 | idealWidth: 320,
18 | maxWidth: .infinity,
19 | alignment: .top)
20 |
21 | }
22 | }
23 |
24 | struct NodeCanvasDocView_Previews: PreviewProvider {
25 | static var previews: some View {
26 | NodeCanvasDocView()
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/PipeNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PipeNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 |
10 | import Foundation
11 |
12 | class PipeNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Actor"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Pipe 🏙"
20 | }
21 |
22 | override class func getDefaultDataOutPorts() -> [NodeDataPortData] {
23 | return [
24 | SKNodeNodeDataPort(portID: 0, direction: .output, name: "", defaultValueGetter: {
25 | return PageManager.shared.nodePageData.pipe
26 | }, defaultValueSetter: { _ in })
27 | ]
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/App/NodeEditorApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShaderNodeEditorApp.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/16/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Welcome to the Pegboard app.
11 | // Pegboard is like the Shortcuts App, but empowered by node-based visual scripting capabilities, so that it can even support real-time logic execution in game development.
12 |
13 | // TODO: Please execute the app using the run button rather than the sideview, as this app is designed to run best on full-screen mode.
14 |
15 | @main
16 | struct NodeEditorApp: App {
17 | var body: some Scene {
18 | WindowGroup {
19 | NodeCanvasNavigationView()
20 | .environmentObject(EnvironmentManager.shared.environment)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/Guide/Walkthrough.tutorial:
--------------------------------------------------------------------------------
1 | @GuideBook(title: "Pegboard", firstFile: NodeEditorApp.swift) {
2 | @WelcomeMessage(title: "Pegboard") {
3 | Please execute the app using the run button rather sideview, as this app is designed to run best on full-screen mode.
4 | }
5 | @Guide {
6 | @Step(title: "Pegboard") {
7 | @ContentAndMedia {
8 | 
9 |
10 | Welcome to the Pegboard app. Pegboard is like the Shortcuts App, but empowered by node-based visual scripting capabilities, so that it can even support real-time logic execution in game development. Please execute the app using the run button rather sideview, as this app is designed to run best on full-screen mode.
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/NodeEditor/Manager/BaseManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseManager.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/17/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public class BaseManager : NSObject {
11 | class var shared : BaseManager {
12 | return BaseManager()
13 | }
14 |
15 | func setup() -> Void {
16 |
17 | }
18 |
19 | func destroy() -> Void {
20 |
21 | }
22 | }
23 |
24 | public func setupAllBaseManagers() -> Void {
25 | let list = subclasses(of: BaseManager.self)
26 | list.forEach { (manager) in
27 | manager.shared.setup()
28 | }
29 | }
30 |
31 |
32 | public func destroyAllBaseManagers() -> Void {
33 | let list = subclasses(of: BaseManager.self)
34 | list.forEach { (manager) in
35 | manager.shared.destroy()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Manager/BaseManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // BaseManager.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/17/22.
6 | //
7 |
8 | import Foundation
9 |
10 | public class BaseManager : NSObject {
11 | class var shared : BaseManager {
12 | return BaseManager()
13 | }
14 |
15 | func setup() -> Void {
16 |
17 | }
18 |
19 | func destroy() -> Void {
20 |
21 | }
22 | }
23 |
24 | public func setupAllBaseManagers() -> Void {
25 | let list = subclasses(of: BaseManager.self)
26 | list.forEach { (manager) in
27 | manager.shared.setup()
28 | }
29 | }
30 |
31 |
32 | public func destroyAllBaseManagers() -> Void {
33 | let list = subclasses(of: BaseManager.self)
34 | list.forEach { (manager) in
35 | manager.shared.destroy()
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/UserDefaults+SwiftUI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults+SwiftUI.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import Foundation
9 |
10 | @propertyWrapper
11 | struct UserDefault {
12 | let key: String
13 | let defaultValue: T
14 | var postSetHandler : ((T,T) -> Void)?
15 |
16 | var wrappedValue: T {
17 | get {
18 | PreferenceManager.shared.userDefaults.value(forKey: key) as? T ?? defaultValue
19 | } set {
20 | if let postSetHandler = postSetHandler {
21 | let old = wrappedValue
22 | PreferenceManager.shared.userDefaults.set(newValue, forKey: key)
23 | postSetHandler(old, newValue)
24 | } else {
25 | PreferenceManager.shared.userDefaults.set(newValue, forKey: key)
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/UserDefaults+SwiftUI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaults+SwiftUI.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import Foundation
9 |
10 | @propertyWrapper
11 | struct UserDefault {
12 | let key: String
13 | let defaultValue: T
14 | var postSetHandler : ((T,T) -> Void)?
15 |
16 | var wrappedValue: T {
17 | get {
18 | PreferenceManager.shared.userDefaults.value(forKey: key) as? T ?? defaultValue
19 | } set {
20 | if let postSetHandler = postSetHandler {
21 | let old = wrappedValue
22 | PreferenceManager.shared.userDefaults.set(newValue, forKey: key)
23 | postSetHandler(old, newValue)
24 | } else {
25 | PreferenceManager.shared.userDefaults.set(newValue, forKey: key)
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ScriptNode.xcodeproj/xcuserdata/fincher.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | NodeEditor.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 | ScriptNode.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 | ShaderNodeEditor.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 0
21 |
22 |
23 | SuppressBuildableAutocreation
24 |
25 | 0D8E6A5D280AAC100071A4D5
26 |
27 | primary
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/Class+Runtime.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public func subclasses(of theClass: T) -> [T] {
4 | let classPtr = address(of: theClass)
5 |
6 | var result: [T] = []
7 | let classCount = objc_getClassList(nil, 0)
8 | let classes = UnsafeMutablePointer.allocate(capacity: Int(classCount))
9 |
10 | let releasingClasses = AutoreleasingUnsafeMutablePointer(classes)
11 | let numClasses: Int32 = objc_getClassList(releasingClasses, classCount)
12 | for n : Int in 0 ..< Int(numClasses) {
13 | if let someClass: AnyClass = classes[n]
14 | {
15 | guard let someSuperClass = class_getSuperclass(someClass), address(of: someSuperClass) == classPtr else { continue }
16 | result.append(someClass as! T)
17 | }
18 | }
19 |
20 | return result
21 | }
22 |
23 | public func address(of object: Any?) -> UnsafeMutableRawPointer{
24 | return Unmanaged.passUnretained(object as AnyObject).toOpaque()
25 | }
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/Class+Runtime.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public func subclasses(of theClass: T) -> [T] {
4 | let classPtr = address(of: theClass)
5 |
6 | var result: [T] = []
7 | let classCount = objc_getClassList(nil, 0)
8 | let classes = UnsafeMutablePointer.allocate(capacity: Int(classCount))
9 |
10 | let releasingClasses = AutoreleasingUnsafeMutablePointer(classes)
11 | let numClasses: Int32 = objc_getClassList(releasingClasses, classCount)
12 | for n : Int in 0 ..< Int(numClasses) {
13 | if let someClass: AnyClass = classes[n]
14 | {
15 | guard let someSuperClass = class_getSuperclass(someClass), address(of: someSuperClass) == classPtr else { continue }
16 | result.append(someClass as! T)
17 | }
18 | }
19 |
20 | return result
21 | }
22 |
23 | public func address(of object: Any?) -> UnsafeMutableRawPointer{
24 | return Unmanaged.passUnretained(object as AnyObject).toOpaque()
25 | }
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/NodeEditor/Helper/CoreGraphics+Math.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGPoint+Math.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/18/22.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | extension CGPoint {
12 | static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
13 | return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
14 | }
15 | static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
16 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
17 | }
18 | static func +(lhs: CGPoint, rhs: CGSize) -> CGPoint {
19 | return CGPoint(x: lhs.x + rhs.width, y: lhs.y + rhs.height)
20 | }
21 | static func *(lhs: CGPoint, rhs: CGFloat) -> CGPoint {
22 | return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
23 | }
24 | }
25 |
26 | extension CGSize {
27 | func toPoint() -> CGPoint {
28 | return CGPoint(x: self.width, y: self.height)
29 | }
30 | }
31 |
32 | extension CGRect {
33 | func toCenter() -> CGPoint {
34 | return CGPoint(x: self.midX, y: self.midY)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/NodeEditor/View/Control/ToggleButtonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToggleButton.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ToggleButtonView: View {
11 |
12 | @State var icon : Image
13 | @Binding var state : Bool
14 |
15 | var body: some View {
16 | Button {
17 | self.state.toggle()
18 | } label: {
19 | icon
20 | .foregroundColor(state ? .init(UIColor.systemBackground) : .accentColor)
21 | .padding(.all, 8)
22 | .background(
23 | RoundedRectangle(cornerRadius: 8)
24 | .foregroundColor(state ? .accentColor : .clear)
25 | )
26 | .animation(.easeInOut, value: state)
27 | }
28 |
29 | }
30 | }
31 |
32 | struct ToggleButtonView_Previews: PreviewProvider {
33 | static var previews: some View {
34 | ToggleButtonView(icon: .init(systemName: "rectangle.stack.fill"), state: .constant(true))
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Helper/CoreGraphics+Math.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGPoint+Math.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/18/22.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | extension CGPoint {
12 | static func -(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
13 | return CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
14 | }
15 | static func +(lhs: CGPoint, rhs: CGPoint) -> CGPoint {
16 | return CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
17 | }
18 | static func +(lhs: CGPoint, rhs: CGSize) -> CGPoint {
19 | return CGPoint(x: lhs.x + rhs.width, y: lhs.y + rhs.height)
20 | }
21 | static func *(lhs: CGPoint, rhs: CGFloat) -> CGPoint {
22 | return CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
23 | }
24 | }
25 |
26 | extension CGSize {
27 | func toPoint() -> CGPoint {
28 | return CGPoint(x: self.width, y: self.height)
29 | }
30 | }
31 |
32 | extension CGRect {
33 | func toCenter() -> CGPoint {
34 | return CGPoint(x: self.midX, y: self.midY)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/Control/ToggleButtonView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ToggleButton.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ToggleButtonView: View {
11 |
12 | @State var icon : Image
13 | @Binding var state : Bool
14 |
15 | var body: some View {
16 | Button {
17 | self.state.toggle()
18 | } label: {
19 | icon
20 | .foregroundColor(state ? .init(UIColor.systemBackground) : .accentColor)
21 | .padding(.all, 8)
22 | .background(
23 | RoundedRectangle(cornerRadius: 8)
24 | .foregroundColor(state ? .accentColor : .clear)
25 | )
26 | .animation(.easeInOut, value: state)
27 | }
28 |
29 | }
30 | }
31 |
32 | struct ToggleButtonView_Previews: PreviewProvider {
33 | static var previews: some View {
34 | ToggleButtonView(icon: .init(systemName: "rectangle.stack.fill"), state: .constant(true))
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/NodeEditor/View/NodeCanvas/NodeCanvasLiveView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasLiveView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import SwiftUI
9 | import SpriteKit
10 |
11 | struct NodeCanvasLiveView: View {
12 | @EnvironmentObject var nodePageData : NodePageData
13 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
14 |
15 | var body: some View {
16 | SpriteViewWrapper(scene: $nodePageData.liveScene, paused: .init(get: {
17 | !nodePageData.playing
18 | }, set: { newValue in
19 | nodePageData.playing = !newValue
20 | }))
21 | .onTapGesture {
22 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "liveViewTapped"), object: nil)
23 | }
24 | .frame(minWidth: 280,
25 | idealWidth: 360,
26 | maxWidth: .infinity,
27 | alignment: .top)
28 |
29 | }
30 | }
31 |
32 | struct NodeCanvasLiveView_Previews: PreviewProvider {
33 | static var previews: some View {
34 | NodeCanvasLiveView()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/NodeCanvas/NodeCanvasLiveView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasLiveView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import SwiftUI
9 | import SpriteKit
10 |
11 | struct NodeCanvasLiveView: View {
12 | @EnvironmentObject var nodePageData : NodePageData
13 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
14 |
15 | var body: some View {
16 | SpriteViewWrapper(scene: $nodePageData.liveScene, paused: .init(get: {
17 | !nodePageData.playing
18 | }, set: { newValue in
19 | nodePageData.playing = !newValue
20 | }))
21 | .onTapGesture {
22 | NotificationCenter.default.post(name: NSNotification.Name(rawValue: "liveViewTapped"), object: nil)
23 | }
24 | .frame(minWidth: 280,
25 | idealWidth: 360,
26 | maxWidth: .infinity,
27 | alignment: .top)
28 |
29 | }
30 | }
31 |
32 | struct NodeCanvasLiveView_Previews: PreviewProvider {
33 | static var previews: some View {
34 | NodeCanvasLiveView()
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/NodeEditor/View/Wrapper/SpriteViewWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpriteViewWrapper.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 | import SwiftUI
11 |
12 | struct SpriteViewWrapper : UIViewRepresentable {
13 |
14 | @Binding var scene: SKScene
15 | @Binding var paused: Bool
16 |
17 | func updateUIView(_ uiView: SKView, context: Context) {
18 | uiView.presentScene(nil)
19 | uiView.setNeedsDisplay()
20 | uiView.setNeedsLayout()
21 | uiView.presentScene(scene)
22 | uiView.setNeedsDisplay()
23 | uiView.setNeedsLayout()
24 | uiView.isPaused = paused
25 | }
26 |
27 | func makeUIView(context: Context) -> SKView{
28 | let view = SKView()
29 | view.isAsynchronous = true
30 | view.preferredFramesPerSecond = 30
31 | view.showsFPS = true
32 | view.showsDrawCount = true
33 | view.showsPhysics = true
34 | view.showsFields = true
35 | view.showsLargeContentViewer = true
36 | view.delegate = RenderManager.shared
37 |
38 | return view
39 | }
40 |
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/Wrapper/SpriteViewWrapper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SpriteViewWrapper.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 | import SwiftUI
11 |
12 | struct SpriteViewWrapper : UIViewRepresentable {
13 |
14 | @Binding var scene: SKScene
15 | @Binding var paused: Bool
16 |
17 | func updateUIView(_ uiView: SKView, context: Context) {
18 | uiView.presentScene(nil)
19 | uiView.setNeedsDisplay()
20 | uiView.setNeedsLayout()
21 | uiView.presentScene(scene)
22 | uiView.setNeedsDisplay()
23 | uiView.setNeedsLayout()
24 | uiView.isPaused = paused
25 | }
26 |
27 | func makeUIView(context: Context) -> SKView{
28 | let view = SKView()
29 | view.isAsynchronous = true
30 | view.preferredFramesPerSecond = 30
31 | view.showsFPS = true
32 | view.showsDrawCount = true
33 | view.showsPhysics = true
34 | view.showsFields = true
35 | view.showsLargeContentViewer = true
36 | view.delegate = RenderManager.shared
37 |
38 | return view
39 | }
40 |
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/NodeEditor/View/NodeCanvas/NodeCanvasMinimapView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasMinimapView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasMinimapView: View {
11 |
12 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
13 |
14 | var body: some View {
15 |
16 | ZStack(alignment: .topTrailing) {
17 | VStack {
18 | Color.clear
19 | .aspectRatio(nodeCanvasData.canvasSize, contentMode: .fit)
20 | Divider()
21 | Text("MINIMAP")
22 | .font(.caption2.monospaced())
23 | }
24 | .background(
25 | Material.thin
26 | )
27 | .mask(RoundedRectangle(cornerRadius: 12))
28 | .frame(width: 100)
29 | .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 0)
30 | .padding()
31 | }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
32 | }
33 | }
34 |
35 | struct NodeCanvasMinimapView_Previews: PreviewProvider {
36 | static var previews: some View {
37 | NodeCanvasMinimapView()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/NodeCanvas/NodeCanvasMinimapView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasMinimapView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasMinimapView: View {
11 |
12 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
13 |
14 | var body: some View {
15 |
16 | ZStack(alignment: .topTrailing) {
17 | VStack {
18 | Color.clear
19 | .aspectRatio(nodeCanvasData.canvasSize, contentMode: .fit)
20 | Divider()
21 | Text("MINIMAP")
22 | .font(.caption2.monospaced())
23 | }
24 | .background(
25 | Material.thin
26 | )
27 | .mask(RoundedRectangle(cornerRadius: 12))
28 | .frame(width: 100)
29 | .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 0)
30 | .padding()
31 | }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topTrailing)
32 | }
33 | }
34 |
35 | struct NodeCanvasMinimapView_Previews: PreviewProvider {
36 | static var previews: some View {
37 | NodeCanvasMinimapView()
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Environment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Environment.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/18/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import SwiftUI
11 |
12 | class Environment : ObservableObject {
13 |
14 |
15 | @UserDefault(key: "useContextMenuOnNodes", defaultValue: true)
16 | var useContextMenuOnNodes: Bool
17 | @UserDefault(key: "enableBlurEffectOnNodes", defaultValue: false)
18 | var enableBlurEffectOnNodes: Bool
19 | @UserDefault(key: "debugMode", defaultValue: false)
20 | var debugMode: Bool
21 | @UserDefault(key: "toggleLivePanel", defaultValue: true)
22 | var toggleLivePanel: Bool
23 | @UserDefault(key: "toggleDocPanel", defaultValue: true)
24 | var toggleDocPanel: Bool
25 | @UserDefault(key: "provideConnectionHint", defaultValue: true)
26 | var provideConnectionHint: Bool
27 |
28 | private var notificationSubscription: AnyCancellable?
29 | init() {
30 | notificationSubscription = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { notif in
31 | DispatchQueue.main.async {
32 | self.objectWillChange.send()
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Environment.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Environment.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/18/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import SwiftUI
11 |
12 | class Environment : ObservableObject {
13 |
14 |
15 | @UserDefault(key: "useContextMenuOnNodes", defaultValue: true)
16 | var useContextMenuOnNodes: Bool
17 | @UserDefault(key: "enableBlurEffectOnNodes", defaultValue: false)
18 | var enableBlurEffectOnNodes: Bool
19 | @UserDefault(key: "debugMode", defaultValue: false)
20 | var debugMode: Bool
21 | @UserDefault(key: "toggleLivePanel", defaultValue: true)
22 | var toggleLivePanel: Bool
23 | @UserDefault(key: "toggleDocPanel", defaultValue: true)
24 | var toggleDocPanel: Bool
25 | @UserDefault(key: "provideConnectionHint", defaultValue: true)
26 | var provideConnectionHint: Bool
27 |
28 | private var notificationSubscription: AnyCancellable?
29 | init() {
30 | notificationSubscription = NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification).sink { notif in
31 | DispatchQueue.main.async {
32 | self.objectWillChange.send()
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/NodeEditor/View/NodeCanvas/NodeCanvasTitleIndicatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasTitleIndicatorView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasTitleIndicatorView: View {
11 |
12 | @State var title : String = ""
13 | @Binding var indicating : Bool
14 |
15 | var childView: ChildView
16 | var body: some View {
17 | ZStack(alignment: .top) {
18 | childView
19 | .overlay(RoundedRectangle(cornerRadius: 16).stroke(indicating ? Color.init(UIColor.quaternaryLabel) : Color.clear, lineWidth: indicating ? 8 : 0))
20 | .mask(RoundedRectangle(cornerRadius: 16))
21 |
22 | ZStack{
23 | Text("\(title)")
24 | .font(.body.monospaced())
25 | .padding()
26 | }
27 | .background(Material.thin)
28 | .frame(height: 32)
29 | .mask(RoundedRectangle(cornerRadius: 16))
30 | .shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 0)
31 | .opacity(indicating ? 1 : 0)
32 | .padding(.top, indicating ? 32 : -32)
33 | }
34 | .animation(.easeInOut, value: indicating)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.5
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: "Pegboard",
12 | platforms: [
13 | .iOS("15.2")
14 | ],
15 | products: [
16 | .iOSApplication(
17 | name: "Pegboard",
18 | targets: ["App"],
19 | displayVersion: "1.0",
20 | bundleVersion: "1",
21 | iconAssetName: "AppIcon",
22 | accentColorAssetName: "AccentColor",
23 | supportedDeviceFamilies: [
24 | .pad,
25 | .phone
26 | ],
27 | supportedInterfaceOrientations: [
28 | .portrait,
29 | .landscapeRight,
30 | .landscapeLeft,
31 | .portraitUpsideDown(.when(deviceFamilies: [.pad]))
32 | ]
33 | )
34 | ],
35 | targets: [
36 | .executableTarget(
37 | name: "App",
38 | path: "App",
39 | resources: [
40 | .process("Resources")
41 | ]
42 | )
43 | ]
44 | )
45 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/NodeCanvas/NodeCanvasTitleIndicatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasTitleIndicatorView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasTitleIndicatorView: View {
11 |
12 | @State var title : String = ""
13 | @Binding var indicating : Bool
14 |
15 | var childView: ChildView
16 | var body: some View {
17 | ZStack(alignment: .top) {
18 | childView
19 | .overlay(RoundedRectangle(cornerRadius: 16).stroke(indicating ? Color.init(UIColor.quaternaryLabel) : Color.clear, lineWidth: indicating ? 8 : 0))
20 | .mask(RoundedRectangle(cornerRadius: 16))
21 |
22 | ZStack{
23 | Text("\(title)")
24 | .font(.body.monospaced())
25 | .padding()
26 | }
27 | .background(Material.thin)
28 | .frame(height: 32)
29 | .mask(RoundedRectangle(cornerRadius: 16))
30 | .shadow(color: .black.opacity(0.1), radius: 12, x: 0, y: 0)
31 | .opacity(indicating ? 1 : 0)
32 | .padding(.top, indicating ? 32 : -32)
33 | }
34 | .animation(.easeInOut, value: indicating)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/GetTouchNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GetTouchNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | class GetTouchNode : NodeData {
12 |
13 | var anyCancellable : AnyCancellable?
14 |
15 | override func postInit() {
16 | super.postInit()
17 | anyCancellable = NotificationCenter.default.publisher(for: Notification.Name(rawValue: "liveViewTapped"))
18 | .sink { notification in
19 | self.perform()
20 | }
21 | }
22 |
23 | override class func getDefaultCategory() -> String {
24 | "Event"
25 | }
26 |
27 | class override func getDefaultTitle() -> String {
28 | "Get Touch ☝️"
29 | }
30 |
31 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
32 | return { nodeData in
33 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
34 | }
35 | }
36 |
37 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
38 | return [
39 | NodeControlPortData(portID: 0, name: "", direction: .output)
40 | ]
41 | }
42 |
43 | override func destroy() {
44 | super.destroy()
45 | anyCancellable?.cancel()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/NewFrameNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewFrameNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | class NewFrameNode : NodeData {
12 |
13 | var anyCancellable : AnyCancellable?
14 |
15 | override func postInit() {
16 | super.postInit()
17 | anyCancellable = NotificationCenter.default.publisher(for: Notification.Name(rawValue: "newFrameRendered"))
18 | .sink { notification in
19 | self.perform()
20 | }
21 | }
22 |
23 | override class func getDefaultCategory() -> String {
24 | "Event"
25 | }
26 |
27 | class override func getDefaultTitle() -> String {
28 | "Rendered Frame 🎞"
29 | }
30 |
31 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
32 | return { nodeData in
33 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
34 | }
35 | }
36 |
37 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
38 | return [
39 | NodeControlPortData(portID: 0, name: "", direction: .output)
40 | ]
41 | }
42 |
43 | override func destroy() {
44 | super.destroy()
45 | anyCancellable?.cancel()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/GetTouchNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GetTouchNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | class GetTouchNode : NodeData {
12 |
13 | var anyCancellable : AnyCancellable?
14 |
15 | override func postInit() {
16 | super.postInit()
17 | anyCancellable = NotificationCenter.default.publisher(for: Notification.Name(rawValue: "liveViewTapped"))
18 | .sink { notification in
19 | self.perform()
20 | }
21 | }
22 |
23 | override class func getDefaultCategory() -> String {
24 | "Event"
25 | }
26 |
27 | class override func getDefaultTitle() -> String {
28 | "Get Touch ☝️"
29 | }
30 |
31 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
32 | return { nodeData in
33 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
34 | }
35 | }
36 |
37 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
38 | return [
39 | NodeControlPortData(portID: 0, name: "", direction: .output)
40 | ]
41 | }
42 |
43 | override func destroy() {
44 | super.destroy()
45 | anyCancellable?.cancel()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/NewFrameNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NewFrameNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | class NewFrameNode : NodeData {
12 |
13 | var anyCancellable : AnyCancellable?
14 |
15 | override func postInit() {
16 | super.postInit()
17 | anyCancellable = NotificationCenter.default.publisher(for: Notification.Name(rawValue: "newFrameRendered"))
18 | .sink { notification in
19 | self.perform()
20 | }
21 | }
22 |
23 | override class func getDefaultCategory() -> String {
24 | "Event"
25 | }
26 |
27 | class override func getDefaultTitle() -> String {
28 | "Rendered Frame 🎞"
29 | }
30 |
31 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
32 | return { nodeData in
33 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
34 | }
35 | }
36 |
37 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
38 | return [
39 | NodeControlPortData(portID: 0, name: "", direction: .output)
40 | ]
41 | }
42 |
43 | override func destroy() {
44 | super.destroy()
45 | anyCancellable?.cancel()
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/TriggerNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TriggerNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import SwiftUI
9 | import Foundation
10 |
11 | class TriggerNode : NodeData {
12 |
13 | override class func getDefaultCategory() -> String {
14 | "Event"
15 | }
16 |
17 | class override func getDefaultTitle() -> String {
18 | "Trigger 🕹"
19 | }
20 |
21 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
22 | return [
23 | NodeControlPortData(portID: 0, name: "", direction: .output)
24 | ]
25 | }
26 |
27 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
28 | return { nodeData in
29 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
30 | }
31 | }
32 |
33 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
34 | AnyView(
35 | ZStack {
36 | Button {
37 | if let node = node as? TriggerNode {
38 | node.perform()
39 | }
40 | } label: {
41 | Text("Click To Trigger")
42 | .font(.body.monospaced())
43 | }
44 | .buttonStyle(BorderedButtonStyle())
45 | }.frame(minWidth: 100, alignment: .center)
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/TriggerNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TriggerNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import SwiftUI
9 | import Foundation
10 |
11 | class TriggerNode : NodeData {
12 |
13 | override class func getDefaultCategory() -> String {
14 | "Event"
15 | }
16 |
17 | class override func getDefaultTitle() -> String {
18 | "Trigger 🕹"
19 | }
20 |
21 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
22 | return [
23 | NodeControlPortData(portID: 0, name: "", direction: .output)
24 | ]
25 | }
26 |
27 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
28 | return { nodeData in
29 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
30 | }
31 | }
32 |
33 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
34 | AnyView(
35 | ZStack {
36 | Button {
37 | if let node = node as? TriggerNode {
38 | node.perform()
39 | }
40 | } label: {
41 | Text("Click To Trigger")
42 | .font(.body.monospaced())
43 | }
44 | .buttonStyle(BorderedButtonStyle())
45 | }.frame(minWidth: 100, alignment: .center)
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/RandomNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RandomNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 |
9 | import Foundation
10 | import SpriteKit
11 |
12 | class RandomNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Variable"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Generate Random 🎲"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let port1 = nodeData.outDataPorts[safe: 0] as? CGFloatNodeDataPort
25 | {
26 | port1.value = CGFloat.random(in: -90...90)
27 | }
28 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
29 | }
30 | }
31 |
32 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
33 | return [
34 | NodeControlPortData(portID: 0, name: "", direction: .input)
35 | ]
36 | }
37 |
38 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
39 | return [
40 | NodeControlPortData(portID: 0, name: "", direction: .output)
41 | ]
42 | }
43 |
44 | class override func getDefaultDataOutPorts() -> [NodeDataPortData] {
45 | return [
46 | CGFloatNodeDataPort(portID: 0, name: "Random", direction: .output)
47 | ]
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/RandomNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RandomNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 |
9 | import Foundation
10 | import SpriteKit
11 |
12 | class RandomNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Variable"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Generate Random 🎲"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let port1 = nodeData.outDataPorts[safe: 0] as? CGFloatNodeDataPort
25 | {
26 | port1.value = CGFloat.random(in: -90...90)
27 | }
28 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
29 | }
30 | }
31 |
32 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
33 | return [
34 | NodeControlPortData(portID: 0, name: "", direction: .input)
35 | ]
36 | }
37 |
38 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
39 | return [
40 | NodeControlPortData(portID: 0, name: "", direction: .output)
41 | ]
42 | }
43 |
44 | class override func getDefaultDataOutPorts() -> [NodeDataPortData] {
45 | return [
46 | CGFloatNodeDataPort(portID: 0, name: "Random", direction: .output)
47 | ]
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/SetFloatNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetFloatNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 | import SwiftUI
11 |
12 | class SetFloatNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Set Float 🔗"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let nodeData = nodeData as? SetFloatNode,
25 | let port1 = nodeData.inDataPorts[safe: 0] as? CGFloatNodeDataPort,
26 | let port2 = nodeData.inDataPorts[safe: 1] as? CGFloatNodeDataPort
27 | {
28 | port1.value = port2.value
29 | }
30 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
31 | }
32 | }
33 |
34 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
35 | return [
36 | NodeControlPortData(portID: 0, name: "", direction: .input)
37 | ]
38 | }
39 |
40 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
41 | return [
42 | NodeControlPortData(portID: 0, name: "", direction: .output)
43 | ]
44 | }
45 |
46 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
47 | return [
48 | CGFloatNodeDataPort(portID: 0, name: "Reference", direction: .input),
49 | CGFloatNodeDataPort(portID: 1, name: "New Value", direction: .input)
50 | ]
51 | }
52 |
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/SetFloatNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetFloatNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 | import SwiftUI
11 |
12 | class SetFloatNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Set Float 🔗"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let nodeData = nodeData as? SetFloatNode,
25 | let port1 = nodeData.inDataPorts[safe: 0] as? CGFloatNodeDataPort,
26 | let port2 = nodeData.inDataPorts[safe: 1] as? CGFloatNodeDataPort
27 | {
28 | port1.value = port2.value
29 | }
30 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
31 | }
32 | }
33 |
34 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
35 | return [
36 | NodeControlPortData(portID: 0, name: "", direction: .input)
37 | ]
38 | }
39 |
40 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
41 | return [
42 | NodeControlPortData(portID: 0, name: "", direction: .output)
43 | ]
44 | }
45 |
46 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
47 | return [
48 | CGFloatNodeDataPort(portID: 0, name: "Reference", direction: .input),
49 | CGFloatNodeDataPort(portID: 1, name: "New Value", direction: .input)
50 | ]
51 | }
52 |
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/.swiftpm/playgrounds/CachedManifest.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CachedManifest
6 |
7 | manifestData
8 |
9 | eyJkZXBlbmRlbmNpZXMiOltdLCJuYW1lIjoiTm9kZSBTY3JpcHQiLCJwYWNr
10 | YWdlS2luZCI6InJvb3QiLCJwbGF0Zm9ybXMiOlt7Im9wdGlvbnMiOltdLCJw
11 | bGF0Zm9ybU5hbWUiOiJpb3MiLCJ2ZXJzaW9uIjoiMTUuMiJ9XSwicHJvZHVj
12 | dHMiOlt7Im5hbWUiOiJOb2RlIFNjcmlwdCIsInNldHRpbmdzIjpbeyJkaXNw
13 | bGF5VmVyc2lvbiI6WyIxLjAiXX0seyJidW5kbGVWZXJzaW9uIjpbIjEiXX0s
14 | eyJpT1NBcHBJbmZvIjpbeyJhY2NlbnRDb2xvckFzc2V0TmFtZSI6IkFjY2Vu
15 | dENvbG9yIiwiY2FwYWJpbGl0aWVzIjpbXSwiaWNvbkFzc2V0TmFtZSI6IkFw
16 | cEljb24iLCJzdXBwb3J0ZWREZXZpY2VGYW1pbGllcyI6WyJwYWQiLCJwaG9u
17 | ZSJdLCJzdXBwb3J0ZWRJbnRlcmZhY2VPcmllbnRhdGlvbnMiOlt7InBvcnRy
18 | YWl0Ijp7fX0seyJsYW5kc2NhcGVSaWdodCI6e319LHsibGFuZHNjYXBlTGVm
19 | dCI6e319LHsicG9ydHJhaXRVcHNpZGVEb3duIjp7ImNvbmRpdGlvbiI6eyJk
20 | ZXZpY2VGYW1pbGllcyI6WyJwYWQiXX19fV19XX1dLCJ0YXJnZXRzIjpbIkFw
21 | cE1vZHVsZSJdLCJ0eXBlIjp7ImV4ZWN1dGFibGUiOm51bGx9fV0sInRhcmdl
22 | dE1hcCI6eyJBcHBNb2R1bGUiOnsiZGVwZW5kZW5jaWVzIjpbXSwiZXhjbHVk
23 | ZSI6W10sIm5hbWUiOiJBcHBNb2R1bGUiLCJwYXRoIjoiLiIsInJlc291cmNl
24 | cyI6W10sInNldHRpbmdzIjpbXSwidHlwZSI6ImV4ZWN1dGFibGUifX0sInRh
25 | cmdldHMiOlt7ImRlcGVuZGVuY2llcyI6W10sImV4Y2x1ZGUiOltdLCJuYW1l
26 | IjoiQXBwTW9kdWxlIiwicGF0aCI6Ii4iLCJyZXNvdXJjZXMiOltdLCJzZXR0
27 | aW5ncyI6W10sInR5cGUiOiJleGVjdXRhYmxlIn1dLCJ0b29sc1ZlcnNpb24i
28 | OnsiX3ZlcnNpb24iOiI1LjUuMCJ9fQ==
29 |
30 | manifestHash
31 |
32 | +uvrnvmTtlz1opbdF1lxpyNaqirZi4z/tNjxDZUp9vE=
33 |
34 | schemaVersion
35 | 3
36 | swiftPMVersionString
37 | 5.5.0
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/NodeEditor/Data/NodeCanvasData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasData.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/19/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import SwiftUI
11 |
12 | class NodeCanvasData : ObservableObject {
13 |
14 | @Published var canvasSize : CGSize = .init(width: 1600, height: 1600)
15 | @Published var nodes : [NodeData] = [] {
16 | willSet {
17 | newValue.forEach({ node in
18 | node.objectWillChange.assign(to: &$childWillChange)
19 | })
20 | }
21 | }
22 | @Published var pendingConnections : [NodePortConnectionData] = [] {
23 | willSet {
24 | newValue.forEach({ node in
25 | node.objectWillChange.assign(to: &$childWillChange)
26 | })
27 | }
28 | }
29 | @Published private var childWillChange: Void = ()
30 |
31 | init() {
32 | }
33 |
34 | convenience init(nodes : [NodeData]) {
35 | self.init()
36 | self.nodes = nodes
37 | }
38 |
39 | func addNode(newNodeType : NodeData.Type, position: CGPoint) -> NodeData {
40 | let newNode = newNodeType.init(nodeID: getNextNodeID())
41 | .withCanvasPosition(canvasPosition: position)
42 | .withCanvas(canvasData: self)
43 | nodes.append(newNode)
44 | return newNode
45 | }
46 |
47 | func getNextNodeID () -> Int {
48 | return (nodes.map { node in
49 | node.nodeID
50 | }.max() ?? -1) + 1
51 | }
52 |
53 | func deleteNode(node : NodeData) {
54 | node.destroy()
55 | nodes.removeAll { nodeData in
56 | nodeData == node
57 | }
58 | }
59 |
60 | func destroy() {
61 | nodes.forEach { nodeData in
62 | deleteNode(node: nodeData)
63 | }
64 | pendingConnections.forEach { connectionData in
65 | connectionData.destroy()
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/NodeEditor/View/NodeCanvas/NodeCanvasNavigationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasNavigationView.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/17/22.
6 | //
7 |
8 | import SwiftUI
9 | import SpriteKit
10 |
11 | struct NodeCanvasNavigationView: View {
12 | @ObservedObject var nodePageData : NodePageData = PageManager.shared.nodePageData
13 | @EnvironmentObject var environment : Environment
14 | var indicating : Binding = .init {
15 | let environment = EnvironmentManager.shared.environment
16 | return environment.toggleDocPanel || environment.toggleLivePanel
17 | } set: { _, _ in
18 |
19 | }
20 |
21 | var body: some View {
22 | ZStack {
23 | HStack(alignment: .center, spacing: 8) {
24 | if environment.toggleDocPanel {
25 | NodeCanvasTitleIndicatorView(title: "Documentation", indicating: indicating, childView:NodeCanvasDocView())
26 | .layoutPriority(1)
27 | }
28 | NodeCanvasTitleIndicatorView(title: "Editor", indicating: indicating, childView: NodeCanvasView())
29 | .layoutPriority(1)
30 | if environment.toggleLivePanel {
31 | NodeCanvasTitleIndicatorView(title: "Live", indicating: indicating, childView:NodeCanvasLiveView())
32 | .layoutPriority(0)
33 | }
34 | }
35 | .padding(.all, 8)
36 | NodeCanvasToolbarView()
37 | }
38 | .animation(.easeInOut, value: environment.toggleDocPanel)
39 | .animation(.easeInOut, value: environment.toggleLivePanel)
40 | .environmentObject(nodePageData)
41 | .environmentObject(nodePageData.nodeCanvasData)
42 | .navigationViewStyle(.stack)
43 | }
44 | }
45 |
46 | struct NodeCanvasNavigationView_Previews: PreviewProvider {
47 | static var previews: some View {
48 | NodeCanvasNavigationView()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/NodeCanvasData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasData.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/19/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import SwiftUI
11 |
12 | class NodeCanvasData : ObservableObject {
13 |
14 | @Published var canvasSize : CGSize = .init(width: 1600, height: 1600)
15 | @Published var nodes : [NodeData] = [] {
16 | willSet {
17 | newValue.forEach({ node in
18 | node.objectWillChange.assign(to: &$childWillChange)
19 | })
20 | }
21 | }
22 | @Published var pendingConnections : [NodePortConnectionData] = [] {
23 | willSet {
24 | newValue.forEach({ node in
25 | node.objectWillChange.assign(to: &$childWillChange)
26 | })
27 | }
28 | }
29 | @Published private var childWillChange: Void = ()
30 |
31 | init() {
32 | }
33 |
34 | convenience init(nodes : [NodeData]) {
35 | self.init()
36 | self.nodes = nodes
37 | }
38 |
39 | func addNode(newNodeType : NodeData.Type, position: CGPoint) -> NodeData {
40 | let newNode = newNodeType.init(nodeID: getNextNodeID())
41 | .withCanvasPosition(canvasPosition: position)
42 | .withCanvas(canvasData: self)
43 | nodes.append(newNode)
44 | return newNode
45 | }
46 |
47 | func getNextNodeID () -> Int {
48 | return (nodes.map { node in
49 | node.nodeID
50 | }.max() ?? -1) + 1
51 | }
52 |
53 | func deleteNode(node : NodeData) {
54 | node.destroy()
55 | nodes.removeAll { nodeData in
56 | nodeData == node
57 | }
58 | }
59 |
60 | func destroy() {
61 | nodes.forEach { nodeData in
62 | deleteNode(node: nodeData)
63 | }
64 | pendingConnections.forEach { connectionData in
65 | connectionData.destroy()
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/NodeCanvas/NodeCanvasNavigationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasNavigationView.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/17/22.
6 | //
7 |
8 | import SwiftUI
9 | import SpriteKit
10 |
11 | struct NodeCanvasNavigationView: View {
12 | @ObservedObject var nodePageData : NodePageData = PageManager.shared.nodePageData
13 | @EnvironmentObject var environment : Environment
14 | var indicating : Binding = .init {
15 | let environment = EnvironmentManager.shared.environment
16 | return environment.toggleDocPanel || environment.toggleLivePanel
17 | } set: { _, _ in
18 |
19 | }
20 |
21 | var body: some View {
22 | ZStack {
23 | HStack(alignment: .center, spacing: 8) {
24 | if environment.toggleDocPanel {
25 | NodeCanvasTitleIndicatorView(title: "Documentation", indicating: indicating, childView:NodeCanvasDocView())
26 | .layoutPriority(1)
27 | }
28 | NodeCanvasTitleIndicatorView(title: "Editor", indicating: indicating, childView: NodeCanvasView())
29 | .layoutPriority(1)
30 | if environment.toggleLivePanel {
31 | NodeCanvasTitleIndicatorView(title: "Live", indicating: indicating, childView:NodeCanvasLiveView())
32 | .layoutPriority(0)
33 | }
34 | }
35 | .padding(.all, 8)
36 | NodeCanvasToolbarView()
37 | }
38 | .animation(.easeInOut, value: environment.toggleDocPanel)
39 | .animation(.easeInOut, value: environment.toggleLivePanel)
40 | .environmentObject(nodePageData)
41 | .environmentObject(nodePageData.nodeCanvasData)
42 | .navigationViewStyle(.stack)
43 | }
44 | }
45 |
46 | struct NodeCanvasNavigationView_Previews: PreviewProvider {
47 | static var previews: some View {
48 | NodeCanvasNavigationView()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/ApplyImpulseNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplyForceNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import SwiftUI
9 | import Foundation
10 | import SpriteKit
11 |
12 | class ApplyImpulseNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Physics"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Apply Force ☄️"
20 | }
21 |
22 | override class func getDefaultUsage() -> String {
23 | "Apply Force node adds impulse force to object"
24 | }
25 |
26 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
27 | return { nodeData in
28 | guard let inDataPort1 = nodeData.inDataPorts[safe: 0],
29 | let inDataPort2 = nodeData.inDataPorts[safe: 1],
30 | let outControlPort1 = nodeData.outControlPorts[safe: 0] else {
31 | return
32 | }
33 | if let spriteNode = inDataPort1.value as? SKSpriteNode, let vector = inDataPort2.value as? CGVector {
34 | print("applyImpulse")
35 | spriteNode.physicsBody?.applyImpulse(vector)
36 | }
37 |
38 | outControlPort1.connections[safe: 0]?.endPort?.nodeData?.perform()
39 | }
40 | }
41 |
42 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
43 | return [
44 | SKNodeNodeDataPort(portID: 0, name: "Object", direction: .input),
45 | CGVectorNodeDataPort(portID: 1, name: "Vector", direction: .input)
46 | ]
47 | }
48 |
49 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
50 | return [
51 | NodeControlPortData(portID: 0, name: "", direction: .input)
52 | ]
53 | }
54 |
55 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
56 | return [
57 | NodeControlPortData(portID: 0, name: "", direction: .output)
58 | ]
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/NodeEditor/Data/NodePageData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePageData.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import Combine
11 | import SpriteKit
12 |
13 | protocol NodePageDataProvider {
14 | func modifyCanvas(nodePageData : NodePageData)
15 | func modifyDocView(nodePageData : NodePageData)
16 | func modifyLiveScene(nodePageData : NodePageData)
17 | func cheat(nodePageData : NodePageData)
18 | func destroy(nodePageData : NodePageData)
19 | }
20 |
21 | class NodePageData : ObservableObject
22 | {
23 | @ObservedObject var nodeCanvasData : NodeCanvasData
24 | @Published var docView : AnyView
25 | @Published var liveScene : SKScene
26 | @Published var playing : Bool = true
27 | var bird : SKSpriteNode = SKSpriteNode()
28 | var pipe : SKNode = SKNode()
29 |
30 | var modifier : NodePageDataProvider = NodePageDataProviderChapterZero()
31 |
32 | required init() {
33 | nodeCanvasData = NodeCanvasData()
34 | docView = AnyView(ZStack{})
35 | liveScene = SKScene()
36 |
37 | switchTo(index: 0)
38 | }
39 |
40 | func cheat() {
41 |
42 | }
43 |
44 | func reset() {
45 | modifier.modifyCanvas(nodePageData: self)
46 | modifier.modifyDocView(nodePageData: self)
47 | modifier.modifyLiveScene(nodePageData: self)
48 | }
49 |
50 | func switchTo(index : Int) {
51 | modifier.destroy(nodePageData: self)
52 | // switch modifer
53 | switch index {
54 | case 0:
55 | modifier = NodePageDataProviderChapterZero()
56 | break
57 | case 1:
58 | modifier = NodePageDataProviderChapterOne()
59 | break
60 | case 2:
61 | modifier = NodePageDataProviderChapterTwo()
62 | break
63 | case 3:
64 | modifier = NodePageDataProviderChapterThree()
65 | break
66 | default:
67 | break
68 | }
69 | reset()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/ApplyImpulseNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApplyForceNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import SwiftUI
9 | import Foundation
10 | import SpriteKit
11 |
12 | class ApplyImpulseNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Physics"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Apply Force ☄️"
20 | }
21 |
22 | override class func getDefaultUsage() -> String {
23 | "Apply Force node adds impulse force to object"
24 | }
25 |
26 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
27 | return { nodeData in
28 | guard let inDataPort1 = nodeData.inDataPorts[safe: 0],
29 | let inDataPort2 = nodeData.inDataPorts[safe: 1],
30 | let outControlPort1 = nodeData.outControlPorts[safe: 0] else {
31 | return
32 | }
33 | if let spriteNode = inDataPort1.value as? SKSpriteNode, let vector = inDataPort2.value as? CGVector {
34 | print("applyImpulse")
35 | spriteNode.physicsBody?.applyImpulse(vector)
36 | }
37 |
38 | outControlPort1.connections[safe: 0]?.endPort?.nodeData?.perform()
39 | }
40 | }
41 |
42 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
43 | return [
44 | SKNodeNodeDataPort(portID: 0, name: "Object", direction: .input),
45 | CGVectorNodeDataPort(portID: 1, name: "Vector", direction: .input)
46 | ]
47 | }
48 |
49 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
50 | return [
51 | NodeControlPortData(portID: 0, name: "", direction: .input)
52 | ]
53 | }
54 |
55 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
56 | return [
57 | NodeControlPortData(portID: 0, name: "", direction: .output)
58 | ]
59 | }
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/NodePageData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePageData.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/23/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import Combine
11 | import SpriteKit
12 |
13 | protocol NodePageDataProvider {
14 | func modifyCanvas(nodePageData : NodePageData)
15 | func modifyDocView(nodePageData : NodePageData)
16 | func modifyLiveScene(nodePageData : NodePageData)
17 | func cheat(nodePageData : NodePageData)
18 | func destroy(nodePageData : NodePageData)
19 | }
20 |
21 | class NodePageData : ObservableObject
22 | {
23 | @ObservedObject var nodeCanvasData : NodeCanvasData
24 | @Published var docView : AnyView
25 | @Published var liveScene : SKScene
26 | @Published var playing : Bool = true
27 | var bird : SKSpriteNode = SKSpriteNode()
28 | var pipe : SKNode = SKNode()
29 |
30 | var modifier : NodePageDataProvider = NodePageDataProviderChapterZero()
31 |
32 | required init() {
33 | nodeCanvasData = NodeCanvasData()
34 | docView = AnyView(ZStack{})
35 | liveScene = SKScene()
36 |
37 | switchTo(index: 0)
38 | }
39 |
40 | func cheat() {
41 |
42 | }
43 |
44 | func reset() {
45 | modifier.modifyCanvas(nodePageData: self)
46 | modifier.modifyDocView(nodePageData: self)
47 | modifier.modifyLiveScene(nodePageData: self)
48 | }
49 |
50 | func switchTo(index : Int) {
51 | modifier.destroy(nodePageData: self)
52 | // switch modifer
53 | switch index {
54 | case 0:
55 | modifier = NodePageDataProviderChapterZero()
56 | break
57 | case 1:
58 | modifier = NodePageDataProviderChapterOne()
59 | break
60 | case 2:
61 | modifier = NodePageDataProviderChapterTwo()
62 | break
63 | case 3:
64 | modifier = NodePageDataProviderChapterThree()
65 | break
66 | default:
67 | break
68 | }
69 | reset()
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/SetPositionNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetPositionNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 |
11 | class SetPositionNode : NodeData {
12 |
13 | override class func getDefaultCategory() -> String {
14 | "Variable"
15 | }
16 |
17 | class override func getDefaultTitle() -> String {
18 | "Set Position 📍"
19 | }
20 |
21 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
22 | return { nodeData in
23 | if let port1 = nodeData.inDataPorts[safe: 0] as? SKNodeNodeDataPort,
24 | let port1Node = port1.value as? SKNode,
25 | let port2 = nodeData.inDataPorts[safe: 1] as? CGFloatNodeDataPort,
26 | let port3 = nodeData.inDataPorts[safe: 2] as? CGFloatNodeDataPort,
27 | let port2Value = port2.value as? CGFloat,
28 | let port3Value = port3.value as? CGFloat
29 | {
30 | port1Node.position = .init(x: port2Value, y: port3Value)
31 | // print("port1Node.position \(port1Node.position ) = (x: \(port2Value), y: \(port3Value))")
32 | }
33 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
34 | }
35 | }
36 |
37 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
38 | return [
39 | NodeControlPortData(portID: 0, name: "", direction: .input)
40 | ]
41 | }
42 |
43 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
44 | return [
45 | NodeControlPortData(portID: 0, name: "", direction: .output)
46 | ]
47 | }
48 |
49 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
50 | return [
51 | SKNodeNodeDataPort(portID: 0, name: "Object", direction: .input),
52 | CGFloatNodeDataPort(portID: 1, name: "X", direction: .input),
53 | CGFloatNodeDataPort(portID: 2, name: "Y", direction: .input)
54 | ]
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/PrintNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrintNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 | import SwiftUI
11 |
12 | class PrintNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Print String 📝"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let nodeData = nodeData as? PrintNode
25 | {
26 | print("\(nodeData.content)")
27 | }
28 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
29 | }
30 | }
31 |
32 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
33 | return [
34 | NodeControlPortData(portID: 0, name: "", direction: .input)
35 | ]
36 | }
37 |
38 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
39 | return [
40 | NodeControlPortData(portID: 0, name: "", direction: .output)
41 | ]
42 | }
43 |
44 |
45 | var content : String = ""
46 |
47 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
48 | return AnyView (
49 | HStack {
50 | Text("Content")
51 | TextField("Content", text: .init(get: { () -> String in
52 | if let node = node as? PrintNode {
53 | return node.content
54 | } else { return "" }
55 | }, set: { newValue in
56 | if let node = node as? PrintNode {
57 | node.content = newValue
58 | }
59 | }),prompt: Text("Content"))
60 | .textFieldStyle(.roundedBorder)
61 | }
62 | .font(.caption.monospaced())
63 | .frame(minWidth: 120, maxWidth: 180, alignment: .center)
64 | )
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/SetPositionNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SetPositionNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 |
11 | class SetPositionNode : NodeData {
12 |
13 | override class func getDefaultCategory() -> String {
14 | "Variable"
15 | }
16 |
17 | class override func getDefaultTitle() -> String {
18 | "Set Position 📍"
19 | }
20 |
21 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
22 | return { nodeData in
23 | if let port1 = nodeData.inDataPorts[safe: 0] as? SKNodeNodeDataPort,
24 | let port1Node = port1.value as? SKNode,
25 | let port2 = nodeData.inDataPorts[safe: 1] as? CGFloatNodeDataPort,
26 | let port3 = nodeData.inDataPorts[safe: 2] as? CGFloatNodeDataPort,
27 | let port2Value = port2.value as? CGFloat,
28 | let port3Value = port3.value as? CGFloat
29 | {
30 | port1Node.position = .init(x: port2Value, y: port3Value)
31 | // print("port1Node.position \(port1Node.position ) = (x: \(port2Value), y: \(port3Value))")
32 | }
33 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
34 | }
35 | }
36 |
37 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
38 | return [
39 | NodeControlPortData(portID: 0, name: "", direction: .input)
40 | ]
41 | }
42 |
43 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
44 | return [
45 | NodeControlPortData(portID: 0, name: "", direction: .output)
46 | ]
47 | }
48 |
49 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
50 | return [
51 | SKNodeNodeDataPort(portID: 0, name: "Object", direction: .input),
52 | CGFloatNodeDataPort(portID: 1, name: "X", direction: .input),
53 | CGFloatNodeDataPort(portID: 2, name: "Y", direction: .input)
54 | ]
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/PrintNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PrintNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 | import SwiftUI
11 |
12 | class PrintNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Print String 📝"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let nodeData = nodeData as? PrintNode
25 | {
26 | print("\(nodeData.content)")
27 | }
28 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
29 | }
30 | }
31 |
32 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
33 | return [
34 | NodeControlPortData(portID: 0, name: "", direction: .input)
35 | ]
36 | }
37 |
38 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
39 | return [
40 | NodeControlPortData(portID: 0, name: "", direction: .output)
41 | ]
42 | }
43 |
44 |
45 | var content : String = ""
46 |
47 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
48 | return AnyView (
49 | HStack {
50 | Text("Content")
51 | TextField("Content", text: .init(get: { () -> String in
52 | if let node = node as? PrintNode {
53 | return node.content
54 | } else { return "" }
55 | }, set: { newValue in
56 | if let node = node as? PrintNode {
57 | node.content = newValue
58 | }
59 | }),prompt: Text("Content"))
60 | .textFieldStyle(.roundedBorder)
61 | }
62 | .font(.caption.monospaced())
63 | .frame(minWidth: 120, maxWidth: 180, alignment: .center)
64 | )
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/GetPositionNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GetPositionNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 |
11 | class GetPositionNode : NodeData {
12 |
13 | override class func getDefaultCategory() -> String {
14 | "Variable"
15 | }
16 |
17 | class override func getDefaultTitle() -> String {
18 | "Get Position 📍"
19 | }
20 |
21 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
22 | return { nodeData in
23 | if let port1 = nodeData.inDataPorts[safe: 0] as? SKNodeNodeDataPort,
24 | let port1Node = port1.value as? SKNode,
25 | let port2 = nodeData.outDataPorts[safe: 0] as? CGFloatNodeDataPort,
26 | let port3 = nodeData.outDataPorts[safe: 1] as? CGFloatNodeDataPort
27 | {
28 | port2.value = port1Node.position.x
29 | port3.value = port1Node.position.y
30 | // print("port2.value = port1Node.position.x \(port1Node.position.x)")
31 | // print("port3.value = port1Node.position.y \(port1Node.position.y)")
32 | }
33 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
34 | }
35 | }
36 |
37 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
38 | return [
39 | NodeControlPortData(portID: 0, name: "", direction: .input)
40 | ]
41 | }
42 |
43 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
44 | return [
45 | NodeControlPortData(portID: 0, name: "", direction: .output)
46 | ]
47 | }
48 |
49 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
50 | return [
51 | SKNodeNodeDataPort(portID: 0, name: "Object", direction: .input)
52 | ]
53 | }
54 |
55 | class override func getDefaultDataOutPorts() -> [NodeDataPortData] {
56 | return [
57 | CGFloatNodeDataPort(portID: 0, name: "X", direction: .output),
58 | CGFloatNodeDataPort(portID: 0, name: "Y", direction: .output)
59 | ]
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/GetPositionNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GetPositionNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 |
11 | class GetPositionNode : NodeData {
12 |
13 | override class func getDefaultCategory() -> String {
14 | "Variable"
15 | }
16 |
17 | class override func getDefaultTitle() -> String {
18 | "Get Position 📍"
19 | }
20 |
21 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
22 | return { nodeData in
23 | if let port1 = nodeData.inDataPorts[safe: 0] as? SKNodeNodeDataPort,
24 | let port1Node = port1.value as? SKNode,
25 | let port2 = nodeData.outDataPorts[safe: 0] as? CGFloatNodeDataPort,
26 | let port3 = nodeData.outDataPorts[safe: 1] as? CGFloatNodeDataPort
27 | {
28 | port2.value = port1Node.position.x
29 | port3.value = port1Node.position.y
30 | // print("port2.value = port1Node.position.x \(port1Node.position.x)")
31 | // print("port3.value = port1Node.position.y \(port1Node.position.y)")
32 | }
33 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
34 | }
35 | }
36 |
37 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
38 | return [
39 | NodeControlPortData(portID: 0, name: "", direction: .input)
40 | ]
41 | }
42 |
43 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
44 | return [
45 | NodeControlPortData(portID: 0, name: "", direction: .output)
46 | ]
47 | }
48 |
49 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
50 | return [
51 | SKNodeNodeDataPort(portID: 0, name: "Object", direction: .input)
52 | ]
53 | }
54 |
55 | class override func getDefaultDataOutPorts() -> [NodeDataPortData] {
56 | return [
57 | CGFloatNodeDataPort(portID: 0, name: "X", direction: .output),
58 | CGFloatNodeDataPort(portID: 0, name: "Y", direction: .output)
59 | ]
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/AddFloatNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 | import SwiftUI
11 |
12 | class AddFloatNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Add Float ➕"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let nodeData = nodeData as? AddFloatNode,
25 | let port1 = nodeData.inDataPorts[safe: 0] as? CGFloatNodeDataPort,
26 | let port1Float = port1.value as? CGFloat
27 | {
28 | port1.value = CGFloat(port1Float + nodeData.addition)
29 | }
30 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
31 | }
32 | }
33 |
34 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
35 | return [
36 | NodeControlPortData(portID: 0, name: "", direction: .input)
37 | ]
38 | }
39 |
40 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
41 | return [
42 | NodeControlPortData(portID: 0, name: "", direction: .output)
43 | ]
44 | }
45 |
46 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
47 | return [
48 | CGFloatNodeDataPort(portID: 0, name: "Value", direction: .input)
49 | ]
50 | }
51 |
52 | var addition : CGFloat = 0
53 |
54 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
55 | return AnyView (
56 | HStack {
57 | Text("Add")
58 | TextField("Add", value: .init(get: { () -> CGFloat in
59 | if let node = node as? AddFloatNode {
60 | return node.addition
61 | } else { return CGFloat(0) }
62 | }, set: { newValue in
63 | if let node = node as? AddFloatNode {
64 | node.addition = newValue
65 | }
66 | }), formatter: NumberFormatter(), prompt: Text("Add"))
67 | .textFieldStyle(.roundedBorder)
68 | }
69 | .font(.caption.monospaced())
70 | .frame(minWidth: 100, maxWidth: 180, alignment: .center)
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/AddFloatNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SpriteKit
10 | import SwiftUI
11 |
12 | class AddFloatNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Add Float ➕"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let nodeData = nodeData as? AddFloatNode,
25 | let port1 = nodeData.inDataPorts[safe: 0] as? CGFloatNodeDataPort,
26 | let port1Float = port1.value as? CGFloat
27 | {
28 | port1.value = CGFloat(port1Float + nodeData.addition)
29 | }
30 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
31 | }
32 | }
33 |
34 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
35 | return [
36 | NodeControlPortData(portID: 0, name: "", direction: .input)
37 | ]
38 | }
39 |
40 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
41 | return [
42 | NodeControlPortData(portID: 0, name: "", direction: .output)
43 | ]
44 | }
45 |
46 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
47 | return [
48 | CGFloatNodeDataPort(portID: 0, name: "Value", direction: .input)
49 | ]
50 | }
51 |
52 | var addition : CGFloat = 0
53 |
54 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
55 | return AnyView (
56 | HStack {
57 | Text("Add")
58 | TextField("Add", value: .init(get: { () -> CGFloat in
59 | if let node = node as? AddFloatNode {
60 | return node.addition
61 | } else { return CGFloat(0) }
62 | }, set: { newValue in
63 | if let node = node as? AddFloatNode {
64 | node.addition = newValue
65 | }
66 | }), formatter: NumberFormatter(), prompt: Text("Add"))
67 | .textFieldStyle(.roundedBorder)
68 | }
69 | .font(.caption.monospaced())
70 | .frame(minWidth: 100, maxWidth: 180, alignment: .center)
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/ScriptNode.xcodeproj/xcshareddata/xcschemes/ScriptNode.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/NodeEditor/View/More/MoreNavigationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsNavigationView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MoreNavigationView: View {
11 |
12 | @EnvironmentObject var environment : Environment
13 |
14 | var body: some View {
15 | NavigationView {
16 | List {
17 | Section(content: {
18 | Toggle("Use Context Menu On Nodes", isOn: $environment.useContextMenuOnNodes)
19 | Toggle("Provide Hint On Node Connections", isOn: $environment.provideConnectionHint)
20 | Toggle("Blurry Node Background", isOn: $environment.enableBlurEffectOnNodes)
21 | Toggle("Debug Mode", isOn: $environment.debugMode)
22 | }, header: {
23 | Text("Settings")
24 | })
25 |
26 | Section {
27 | Link("Haotian Zheng's Website", destination: URL(string: "https://haotianzheng.com")!)
28 | Text("Haotian Zheng is currently a student in Carnegie Mellon University. He likes coding, photography, video-gaming, and driving.")
29 | } header: {
30 | Text("About")
31 | }
32 |
33 | Section {
34 | Text("My app, Pegboard, is an interactive canvas with dynamic execution. You can see it as the Shortcuts app or the Workflow app from Apple but packs a whole new node-based user interface that is more flexible and intuitive to use.")
35 | .lineLimit(nil)
36 | Text("You will see the power of Pegboard as in the app I created several node graphs to drive a simple Flappy Bird game. However, it is worth mentioning that, It is not strictly an editor for developers, rather, the underlying node-based UI framework I wrote with pure SwiftUI can support nearly any type of creative work, like music production, interactive story-telling, automation, educational programming learning experience, etc.")
37 | .lineLimit(nil)
38 | Text("As mentioned, Pegboard is a pure SwiftUI app, and heavily uses Combine for state management. Pegboard is designed with openness in mind, as you can extend one of the base classes to create a new node with your own desired logic and custom drawing. I fully believe an app like Pegboard will make users feel like at home with their iPads, as pro users can use it to do creative work, while daily users can use it to automate workflows, and students can use it to learn the basis of software / games development.")
39 | .lineLimit(nil)
40 | } header: {
41 | Text("Submission")
42 | }
43 |
44 | }
45 | .font(.body.monospaced())
46 | .navigationTitle("More")
47 | }
48 | .frame(minWidth: 360, idealWidth: 500, maxWidth: nil,
49 | minHeight: 360, idealHeight: 540, maxHeight: nil,
50 | alignment: .top)
51 | }
52 | }
53 |
54 | struct SettingsNavigationView_Previews: PreviewProvider {
55 | static var previews: some View {
56 | MoreNavigationView()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/More/MoreNavigationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SettingsNavigationView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct MoreNavigationView: View {
11 |
12 | @EnvironmentObject var environment : Environment
13 |
14 | var body: some View {
15 | NavigationView {
16 | List {
17 | Section(content: {
18 | Toggle("Use Context Menu On Nodes", isOn: $environment.useContextMenuOnNodes)
19 | Toggle("Provide Hint On Node Connections", isOn: $environment.provideConnectionHint)
20 | Toggle("Blurry Node Background", isOn: $environment.enableBlurEffectOnNodes)
21 | Toggle("Debug Mode", isOn: $environment.debugMode)
22 | }, header: {
23 | Text("Settings")
24 | })
25 |
26 | Section {
27 | Link("Haotian Zheng's Website", destination: URL(string: "https://haotianzheng.com")!)
28 | Text("Haotian Zheng is currently a student in Carnegie Mellon University. He likes coding, photography, video-gaming, and driving.")
29 | } header: {
30 | Text("About")
31 | }
32 |
33 | Section {
34 | Text("My app, Pegboard, is an interactive canvas with dynamic execution. You can see it as the Shortcuts app or the Workflow app from Apple but packs a whole new node-based user interface that is more flexible and intuitive to use.")
35 | .lineLimit(nil)
36 | Text("You will see the power of Pegboard as in the app I created several node graphs to drive a simple Flappy Bird game. However, it is worth mentioning that, It is not strictly an editor for developers, rather, the underlying node-based UI framework I wrote with pure SwiftUI can support nearly any type of creative work, like music production, interactive story-telling, automation, educational programming learning experience, etc.")
37 | .lineLimit(nil)
38 | Text("As mentioned, Pegboard is a pure SwiftUI app, and heavily uses Combine for state management. Pegboard is designed with openness in mind, as you can extend one of the base classes to create a new node with your own desired logic and custom drawing. I fully believe an app like Pegboard will make users feel like at home with their iPads, as pro users can use it to do creative work, while daily users can use it to automate workflows, and students can use it to learn the basis of software / games development.")
39 | .lineLimit(nil)
40 | } header: {
41 | Text("Submission")
42 | }
43 |
44 | }
45 | .font(.body.monospaced())
46 | .navigationTitle("More")
47 | }
48 | .frame(minWidth: 360, idealWidth: 500, maxWidth: nil,
49 | minHeight: 360, idealHeight: 540, maxHeight: nil,
50 | alignment: .top)
51 | }
52 | }
53 |
54 | struct SettingsNavigationView_Previews: PreviewProvider {
55 | static var previews: some View {
56 | MoreNavigationView()
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/NodeEditor/Data/NodePortConnectionData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePortConnection.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | import SwiftUI
11 | import Combine
12 |
13 | class NodePortConnectionData : ObservableObject, Identifiable, Equatable, Hashable {
14 | static func == (lhs: NodePortConnectionData, rhs: NodePortConnectionData) -> Bool {
15 | return lhs.startPort == rhs.startPort
16 | && lhs.endPort == rhs.endPort
17 | && lhs.startPosIfPortNull == rhs.startPosIfPortNull
18 | && lhs.endPosIfPortNull == rhs.endPosIfPortNull
19 | }
20 |
21 | func hash(into hasher: inout Hasher) {
22 | hasher.combine(startPos)
23 | hasher.combine(endPos)
24 | }
25 |
26 | weak var startPort : NodePortData? {
27 | willSet {
28 | objectWillChange.send()
29 | }
30 | }
31 | @Published var startPosIfPortNull : CGPoint = .zero
32 | var startPos : CGPoint {
33 | return startPort?.canvasRect.toCenter() ?? startPosIfPortNull
34 | }
35 | weak var endPort : NodePortData?{
36 | willSet {
37 | objectWillChange.send()
38 | }
39 | }
40 | @Published var endPosIfPortNull : CGPoint = .zero
41 | var endPos : CGPoint {
42 | return endPort?.canvasRect.toCenter() ?? endPosIfPortNull
43 | }
44 |
45 |
46 | init(startPort: NodePortData?, endPort: NodePortData?) {
47 | self.startPort = startPort
48 | self.endPort = endPort
49 | }
50 |
51 | func connect() {
52 | startPort?.connections.append(self)
53 | endPort?.connections.append(self)
54 | }
55 |
56 | // get the port that is not connected
57 | var getPendingPortDirection : NodePortDirection? {
58 | if startPort == nil {
59 | return .output
60 | }
61 | if endPort == nil {
62 | return .input
63 | }
64 | return nil
65 | }
66 |
67 | func isolate(portDirection : NodePortDirection) {
68 | if portDirection == .output {
69 | startPort?.connections.removeAll { connection in
70 | connection == self
71 | }
72 | } else {
73 | endPort?.connections.removeAll { connection in
74 | connection == self
75 | }
76 | }
77 | }
78 |
79 | func disconnect(portDirection : NodePortDirection) {
80 | if portDirection == .output {
81 | startPort = nil
82 | } else {
83 | endPort = nil
84 | }
85 | }
86 |
87 | func disconnect() {
88 | disconnect(portDirection: .input)
89 | disconnect(portDirection: .output)
90 | }
91 |
92 | func isolate() {
93 | isolate(portDirection: .input)
94 | isolate(portDirection: .output)
95 | }
96 |
97 | func destroy() {
98 | isolate()
99 | disconnect()
100 | }
101 |
102 | var color : Color {
103 | let port = [startPort, endPort].compactMap { nodePortData in
104 | nodePortData
105 | }.first
106 | return port?.color() ?? .black
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/NodePortConnectionData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePortConnection.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/20/22.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 | import SwiftUI
11 | import Combine
12 |
13 | class NodePortConnectionData : ObservableObject, Identifiable, Equatable, Hashable {
14 | static func == (lhs: NodePortConnectionData, rhs: NodePortConnectionData) -> Bool {
15 | return lhs.startPort == rhs.startPort
16 | && lhs.endPort == rhs.endPort
17 | && lhs.startPosIfPortNull == rhs.startPosIfPortNull
18 | && lhs.endPosIfPortNull == rhs.endPosIfPortNull
19 | }
20 |
21 | func hash(into hasher: inout Hasher) {
22 | hasher.combine(startPos)
23 | hasher.combine(endPos)
24 | }
25 |
26 | weak var startPort : NodePortData? {
27 | willSet {
28 | objectWillChange.send()
29 | }
30 | }
31 | @Published var startPosIfPortNull : CGPoint = .zero
32 | var startPos : CGPoint {
33 | return startPort?.canvasRect.toCenter() ?? startPosIfPortNull
34 | }
35 | weak var endPort : NodePortData?{
36 | willSet {
37 | objectWillChange.send()
38 | }
39 | }
40 | @Published var endPosIfPortNull : CGPoint = .zero
41 | var endPos : CGPoint {
42 | return endPort?.canvasRect.toCenter() ?? endPosIfPortNull
43 | }
44 |
45 |
46 | init(startPort: NodePortData?, endPort: NodePortData?) {
47 | self.startPort = startPort
48 | self.endPort = endPort
49 | }
50 |
51 | func connect() {
52 | startPort?.connections.append(self)
53 | endPort?.connections.append(self)
54 | }
55 |
56 | // get the port that is not connected
57 | var getPendingPortDirection : NodePortDirection? {
58 | if startPort == nil {
59 | return .output
60 | }
61 | if endPort == nil {
62 | return .input
63 | }
64 | return nil
65 | }
66 |
67 | func isolate(portDirection : NodePortDirection) {
68 | if portDirection == .output {
69 | startPort?.connections.removeAll { connection in
70 | connection == self
71 | }
72 | } else {
73 | endPort?.connections.removeAll { connection in
74 | connection == self
75 | }
76 | }
77 | }
78 |
79 | func disconnect(portDirection : NodePortDirection) {
80 | if portDirection == .output {
81 | startPort = nil
82 | } else {
83 | endPort = nil
84 | }
85 | }
86 |
87 | func disconnect() {
88 | disconnect(portDirection: .input)
89 | disconnect(portDirection: .output)
90 | }
91 |
92 | func isolate() {
93 | isolate(portDirection: .input)
94 | isolate(portDirection: .output)
95 | }
96 |
97 | func destroy() {
98 | isolate()
99 | disconnect()
100 | }
101 |
102 | var color : Color {
103 | let port = [startPort, endPort].compactMap { nodePortData in
104 | nodePortData
105 | }.first
106 | return port?.color() ?? .black
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/NodeEditor/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]}
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]}
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/ComparsionNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComparsionNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 |
12 | class ComparsionNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Comparsion ⚖️"
20 | }
21 |
22 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
23 | return [
24 | NodeControlPortData(portID: 0, name: ">", direction: .output),
25 | NodeControlPortData(portID: 1, name: "=", direction: .output),
26 | NodeControlPortData(portID: 2, name: "<", direction: .output)
27 | ]
28 | }
29 |
30 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
31 | return [
32 | NodeControlPortData(portID: 0, name: "", direction: .input)
33 | ]
34 | }
35 |
36 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
37 | return [
38 | CGFloatNodeDataPort(portID: 0, name: "Value", direction: .input)
39 | ]
40 | }
41 |
42 |
43 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
44 | return {nodeData in
45 | if let nodeData = nodeData as? ComparsionNode,
46 | let inDataPort1 = nodeData.inDataPorts[safe: 0],
47 | let inDataPort1Value = inDataPort1.value as? CGFloat,
48 | let outControlPort1 = nodeData.outControlPorts[safe: 0],
49 | let outControlPort2 = nodeData.outControlPorts[safe: 1],
50 | let outControlPort3 = nodeData.outControlPorts[safe: 2]
51 | {
52 | if inDataPort1Value > nodeData.comparsionTo {
53 | outControlPort1.connections[safe: 0]?.endPort?.nodeData?.perform()
54 | } else if inDataPort1Value == nodeData.comparsionTo {
55 | outControlPort2.connections[safe: 0]?.endPort?.nodeData?.perform()
56 | } else {
57 | outControlPort3.connections[safe: 0]?.endPort?.nodeData?.perform()
58 | }
59 | }
60 | }
61 | }
62 |
63 | var comparsionTo : CGFloat = 0
64 |
65 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
66 | AnyView(
67 | VStack {
68 | Text("Comparing To:")
69 | .font(.footnote.monospaced())
70 | HStack {
71 | TextField("Compare To", value: .init(get: { () -> CGFloat in
72 | if let node = node as? ComparsionNode {
73 | return node.comparsionTo
74 | } else { return CGFloat(0) }
75 | }, set: { newValue in
76 | if let node = node as? ComparsionNode {
77 | node.comparsionTo = newValue
78 | }
79 | }), formatter: NumberFormatter(), prompt: Text("Compare To"))
80 | .textFieldStyle(.roundedBorder)
81 | }
82 | .font(.caption.monospaced())
83 | }
84 | .frame(minWidth: 100, maxWidth: 180, alignment: .center)
85 | )
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/ComparsionNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ComparsionNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 |
12 | class ComparsionNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Comparsion ⚖️"
20 | }
21 |
22 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
23 | return [
24 | NodeControlPortData(portID: 0, name: ">", direction: .output),
25 | NodeControlPortData(portID: 1, name: "=", direction: .output),
26 | NodeControlPortData(portID: 2, name: "<", direction: .output)
27 | ]
28 | }
29 |
30 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
31 | return [
32 | NodeControlPortData(portID: 0, name: "", direction: .input)
33 | ]
34 | }
35 |
36 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
37 | return [
38 | CGFloatNodeDataPort(portID: 0, name: "Value", direction: .input)
39 | ]
40 | }
41 |
42 |
43 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
44 | return {nodeData in
45 | if let nodeData = nodeData as? ComparsionNode,
46 | let inDataPort1 = nodeData.inDataPorts[safe: 0],
47 | let inDataPort1Value = inDataPort1.value as? CGFloat,
48 | let outControlPort1 = nodeData.outControlPorts[safe: 0],
49 | let outControlPort2 = nodeData.outControlPorts[safe: 1],
50 | let outControlPort3 = nodeData.outControlPorts[safe: 2]
51 | {
52 | if inDataPort1Value > nodeData.comparsionTo {
53 | outControlPort1.connections[safe: 0]?.endPort?.nodeData?.perform()
54 | } else if inDataPort1Value == nodeData.comparsionTo {
55 | outControlPort2.connections[safe: 0]?.endPort?.nodeData?.perform()
56 | } else {
57 | outControlPort3.connections[safe: 0]?.endPort?.nodeData?.perform()
58 | }
59 | }
60 | }
61 | }
62 |
63 | var comparsionTo : CGFloat = 0
64 |
65 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
66 | AnyView(
67 | VStack {
68 | Text("Comparing To:")
69 | .font(.footnote.monospaced())
70 | HStack {
71 | TextField("Compare To", value: .init(get: { () -> CGFloat in
72 | if let node = node as? ComparsionNode {
73 | return node.comparsionTo
74 | } else { return CGFloat(0) }
75 | }, set: { newValue in
76 | if let node = node as? ComparsionNode {
77 | node.comparsionTo = newValue
78 | }
79 | }), formatter: NumberFormatter(), prompt: Text("Compare To"))
80 | .textFieldStyle(.roundedBorder)
81 | }
82 | .font(.caption.monospaced())
83 | }
84 | .frame(minWidth: 100, maxWidth: 180, alignment: .center)
85 | )
86 | }
87 |
88 | }
89 |
--------------------------------------------------------------------------------
/NodeEditor/View/NodeCanvas/NodeAddSelectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeAddView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasAddNodePointView : View {
11 | @Binding var popoverPosition : CGPoint
12 | @Binding var showPopover : Bool
13 |
14 | var body: some View {
15 | Color.clear.frame(width: 1, height: 1, alignment: .center)
16 | .popover(isPresented: $showPopover, attachmentAnchor: .point(.zero)) {
17 | NodeAddSelectionView(showPopover: $showPopover, nodePosition: $popoverPosition)
18 | }
19 | }
20 | }
21 |
22 | struct NodeAddSelectionView: View {
23 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
24 | @Binding var showPopover : Bool
25 | @Binding var nodePosition : CGPoint
26 | @State private var nodeCategory : String = ""
27 |
28 | var nodeCategoryToTypeDict : [String: [NodeData]] {
29 | let categoryToType = Dictionary(grouping: subclasses(of: NodeData.self)
30 | .filter { nodeType in
31 | nodeType.self.getDefaultExposedToUser()
32 | }, by: { $0.getDefaultCategory() })
33 |
34 | let categoryToInstance = Dictionary(uniqueKeysWithValues:
35 | categoryToType.map { key, value in (key, value.enumerated().map({ (index, nodeType) in
36 | nodeType.init(nodeID: index)
37 | })) })
38 |
39 | return categoryToInstance
40 | }
41 |
42 | var nodeCategoryList : [String] {
43 | nodeCategoryToTypeDict.keys.sorted()
44 | }
45 |
46 | func nodeListFor(category: String) -> [NodeData] {
47 | return nodeCategoryToTypeDict[category] ?? []
48 | }
49 |
50 | var body: some View {
51 | NavigationView {
52 | List{
53 | ForEach(nodeListFor(category: nodeCategory)) { nodeData in
54 | Button {
55 | showPopover = false
56 | _ = nodeCanvasData.addNode(newNodeType: type(of: nodeData), position: nodePosition)
57 | } label: {
58 | HStack {
59 | Text("\(nodeData.title)")
60 | .font(.body.monospaced())
61 | Spacer()
62 | NodeView(demoMode: true, nodeData: nodeData)
63 | .padding()
64 | }
65 | .contentShape(Rectangle())
66 | }
67 | .buttonStyle(PlainButtonStyle())
68 |
69 | }
70 | }
71 | .onAppear(perform: {
72 | nodeCategory = nodeCategoryList[safe: 0] ?? ""
73 | })
74 | .listStyle(PlainListStyle())
75 | .toolbar {
76 | ToolbarItem(placement: .principal) {
77 | Picker("Category", selection: $nodeCategory) {
78 | ForEach(nodeCategoryList) { category in
79 | Text(category).tag(category)
80 | }
81 | }
82 | .pickerStyle(.segmented)
83 | }
84 | }
85 | .navigationTitle("\(nodeCategory)")
86 | }
87 | .frame(minWidth: 300, idealWidth: 380, maxWidth: nil,
88 | minHeight: 360, idealHeight: 540, maxHeight: nil,
89 | alignment: .top)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/NodeCanvas/NodeAddSelectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeAddView.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/21/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasAddNodePointView : View {
11 | @Binding var popoverPosition : CGPoint
12 | @Binding var showPopover : Bool
13 |
14 | var body: some View {
15 | Color.clear.frame(width: 1, height: 1, alignment: .center)
16 | .popover(isPresented: $showPopover, attachmentAnchor: .point(.zero)) {
17 | NodeAddSelectionView(showPopover: $showPopover, nodePosition: $popoverPosition)
18 | }
19 | }
20 | }
21 |
22 | struct NodeAddSelectionView: View {
23 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
24 | @Binding var showPopover : Bool
25 | @Binding var nodePosition : CGPoint
26 | @State private var nodeCategory : String = ""
27 |
28 | var nodeCategoryToTypeDict : [String: [NodeData]] {
29 | let categoryToType = Dictionary(grouping: subclasses(of: NodeData.self)
30 | .filter { nodeType in
31 | nodeType.self.getDefaultExposedToUser()
32 | }, by: { $0.getDefaultCategory() })
33 |
34 | let categoryToInstance = Dictionary(uniqueKeysWithValues:
35 | categoryToType.map { key, value in (key, value.enumerated().map({ (index, nodeType) in
36 | nodeType.init(nodeID: index)
37 | })) })
38 |
39 | return categoryToInstance
40 | }
41 |
42 | var nodeCategoryList : [String] {
43 | nodeCategoryToTypeDict.keys.sorted()
44 | }
45 |
46 | func nodeListFor(category: String) -> [NodeData] {
47 | return nodeCategoryToTypeDict[category] ?? []
48 | }
49 |
50 | var body: some View {
51 | NavigationView {
52 | List{
53 | ForEach(nodeListFor(category: nodeCategory)) { nodeData in
54 | Button {
55 | showPopover = false
56 | _ = nodeCanvasData.addNode(newNodeType: type(of: nodeData), position: nodePosition)
57 | } label: {
58 | HStack {
59 | Text("\(nodeData.title)")
60 | .font(.body.monospaced())
61 | Spacer()
62 | NodeView(demoMode: true, nodeData: nodeData)
63 | .padding()
64 | }
65 | .contentShape(Rectangle())
66 | }
67 | .buttonStyle(PlainButtonStyle())
68 |
69 | }
70 | }
71 | .onAppear(perform: {
72 | nodeCategory = nodeCategoryList[safe: 0] ?? ""
73 | })
74 | .listStyle(PlainListStyle())
75 | .toolbar {
76 | ToolbarItem(placement: .principal) {
77 | Picker("Category", selection: $nodeCategory) {
78 | ForEach(nodeCategoryList) { category in
79 | Text(category).tag(category)
80 | }
81 | }
82 | .pickerStyle(.segmented)
83 | }
84 | }
85 | .navigationTitle("\(nodeCategory)")
86 | }
87 | .frame(minWidth: 300, idealWidth: 380, maxWidth: nil,
88 | minHeight: 360, idealHeight: 540, maxHeight: nil,
89 | alignment: .top)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/NodeEditor/Data/Nodes/LoopFloatNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoopFloatNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import SpriteKit
11 |
12 | class LoopFloatNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Loop Float 🔂"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let nodeData = nodeData as? LoopFloatNode,
25 | let port1 = nodeData.inDataPorts[safe: 0] as? CGFloatNodeDataPort,
26 | let port1Float = port1.value as? CGFloat,
27 | let port2 = nodeData.outDataPorts[safe: 0] as? CGFloatNodeDataPort
28 | {
29 | if port1Float > nodeData.max {
30 | port2.value = nodeData.min
31 | } else if port1Float < nodeData.min {
32 | port2.value = nodeData.max
33 | } else {
34 | port2.value = port1Float
35 | }
36 | }
37 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
38 | }
39 | }
40 |
41 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
42 | return [
43 | NodeControlPortData(portID: 0, name: "", direction: .input)
44 | ]
45 | }
46 |
47 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
48 | return [
49 | NodeControlPortData(portID: 0, name: "", direction: .output)
50 | ]
51 | }
52 |
53 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
54 | return [
55 | CGFloatNodeDataPort(portID: 0, name: "Value", direction: .input)
56 | ]
57 | }
58 |
59 | override class func getDefaultDataOutPorts() -> [NodeDataPortData] {
60 | return [
61 | CGFloatNodeDataPort(portID: 0, name: "Result", direction: .output)
62 | ]
63 | }
64 |
65 | var min : CGFloat = -100
66 | var max : CGFloat = 100
67 |
68 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
69 | AnyView (
70 | HStack {
71 | Text("Min")
72 | TextField("Min", value: .init(get: { () -> CGFloat in
73 | if let node = node as? LoopFloatNode {
74 | return node.min
75 | } else { return CGFloat(0) }
76 | }, set: { newValue in
77 | if let node = node as? LoopFloatNode {
78 | node.min = newValue
79 | }
80 | }), formatter: NumberFormatter(), prompt: Text("Min"))
81 | .textFieldStyle(.roundedBorder)
82 |
83 | Text("Max")
84 | TextField("Min", value: .init(get: { () -> CGFloat in
85 | if let node = node as? LoopFloatNode {
86 | return node.max
87 | } else { return CGFloat(0) }
88 | }, set: { newValue in
89 | if let node = node as? LoopFloatNode {
90 | node.max = newValue
91 | }
92 | }), formatter: NumberFormatter(), prompt: Text("Max"))
93 | .textFieldStyle(.roundedBorder)
94 | }
95 | .font(.caption.monospaced())
96 | .frame(minWidth: 140, maxWidth: 180, alignment: .center)
97 | )
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/Nodes/LoopFloatNode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoopFloatNode.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import SpriteKit
11 |
12 | class LoopFloatNode : NodeData {
13 |
14 | override class func getDefaultCategory() -> String {
15 | "Operator"
16 | }
17 |
18 | class override func getDefaultTitle() -> String {
19 | "Loop Float 🔂"
20 | }
21 |
22 | override class func getDefaultPerformImplementation() -> ((NodeData) -> ()) {
23 | return { nodeData in
24 | if let nodeData = nodeData as? LoopFloatNode,
25 | let port1 = nodeData.inDataPorts[safe: 0] as? CGFloatNodeDataPort,
26 | let port1Float = port1.value as? CGFloat,
27 | let port2 = nodeData.outDataPorts[safe: 0] as? CGFloatNodeDataPort
28 | {
29 | if port1Float > nodeData.max {
30 | port2.value = nodeData.min
31 | } else if port1Float < nodeData.min {
32 | port2.value = nodeData.max
33 | } else {
34 | port2.value = port1Float
35 | }
36 | }
37 | nodeData.outControlPorts[safe: 0]?.connections[safe: 0]?.endPort?.nodeData?.perform()
38 | }
39 | }
40 |
41 | override class func getDefaultControlInPorts() -> [NodeControlPortData] {
42 | return [
43 | NodeControlPortData(portID: 0, name: "", direction: .input)
44 | ]
45 | }
46 |
47 | override class func getDefaultControlOutPorts() -> [NodeControlPortData] {
48 | return [
49 | NodeControlPortData(portID: 0, name: "", direction: .output)
50 | ]
51 | }
52 |
53 | override class func getDefaultDataInPorts() -> [NodeDataPortData] {
54 | return [
55 | CGFloatNodeDataPort(portID: 0, name: "Value", direction: .input)
56 | ]
57 | }
58 |
59 | override class func getDefaultDataOutPorts() -> [NodeDataPortData] {
60 | return [
61 | CGFloatNodeDataPort(portID: 0, name: "Result", direction: .output)
62 | ]
63 | }
64 |
65 | var min : CGFloat = -100
66 | var max : CGFloat = 100
67 |
68 | override class func getDefaultCustomRendering(node: NodeData) -> AnyView? {
69 | AnyView (
70 | HStack {
71 | Text("Min")
72 | TextField("Min", value: .init(get: { () -> CGFloat in
73 | if let node = node as? LoopFloatNode {
74 | return node.min
75 | } else { return CGFloat(0) }
76 | }, set: { newValue in
77 | if let node = node as? LoopFloatNode {
78 | node.min = newValue
79 | }
80 | }), formatter: NumberFormatter(), prompt: Text("Min"))
81 | .textFieldStyle(.roundedBorder)
82 |
83 | Text("Max")
84 | TextField("Min", value: .init(get: { () -> CGFloat in
85 | if let node = node as? LoopFloatNode {
86 | return node.max
87 | } else { return CGFloat(0) }
88 | }, set: { newValue in
89 | if let node = node as? LoopFloatNode {
90 | node.max = newValue
91 | }
92 | }), formatter: NumberFormatter(), prompt: Text("Max"))
93 | .textFieldStyle(.roundedBorder)
94 | }
95 | .font(.caption.monospaced())
96 | .frame(minWidth: 140, maxWidth: 180, alignment: .center)
97 | )
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/NodeEditor/View/NodeCanvas/NodeCanvasToolbarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasToolbarView.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/19/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasToolbarView: View {
11 |
12 |
13 | @State private var showSettings = false
14 | @State private var showResetAlert = false
15 | @State private var showReadingProgress = false
16 | @EnvironmentObject var nodePageData : NodePageData
17 | @EnvironmentObject var environment : Environment
18 |
19 | var body: some View {
20 | ZStack (alignment: .bottom) {
21 |
22 | HStack(alignment: .center, spacing: 18) {
23 |
24 | // Button {
25 | // showReadingProgress = true
26 | // } label: {
27 | // VStack(alignment: .leading) {
28 | // HStack {
29 | // Text("Currently Reading \(Image(systemName: "chevron.up"))")
30 | // .font(.subheadline.monospaced())
31 | // }
32 | // Text("Chapter 1")
33 | // .font(.caption.monospaced())
34 | // }.padding(.horizontal, 8)
35 | // }
36 | // .popover(isPresented: $showReadingProgress) {
37 | // Text("")
38 | // }
39 |
40 | // Divider()
41 |
42 | Button {
43 | showResetAlert = true
44 | } label: {
45 | Image(systemName: "memories")
46 | .padding(.all, 8)
47 | }
48 | .alert("Reset?", isPresented: $showResetAlert, actions: {
49 | Button(role: .destructive) {
50 | nodePageData.reset()
51 | } label: {
52 | Text("Confirm")
53 | }
54 | Button(role: .cancel) {
55 | showResetAlert = false
56 | } label: {
57 | Text("Cancel")
58 | }
59 |
60 | }, message: {
61 | Text("The live scene and the editor node graph will be reset to its initial state")
62 | })
63 | // Button {
64 | // if nodePageData.playing {
65 | // nodePageData.playing = false
66 | // nodePageData.reset()
67 | // } else {
68 | // nodePageData.playing = true
69 | // }
70 | // } label: {
71 | // Image(systemName: nodePageData.playing ? "stop.fill" : "play.fill")
72 | // .padding(.all, 8)
73 | // }
74 | //
75 | ToggleButtonView(icon: .init(systemName:"rectangle.lefthalf.inset.filled"), state: $environment.toggleDocPanel)
76 | ToggleButtonView(icon: .init(systemName:"rectangle.righthalf.inset.filled"), state: $environment.toggleLivePanel)
77 |
78 | Divider()
79 |
80 | Button {
81 | showSettings = true
82 | } label: {
83 | Image(systemName: "ellipsis")
84 | .padding(.all, 8)
85 | }
86 | .popover(isPresented: $showSettings) {
87 | MoreNavigationView()
88 | }
89 | }
90 | .padding()
91 | .frame(height: 64)
92 | .background(
93 | Material.regular
94 | )
95 | .mask({
96 | RoundedRectangle(cornerRadius: 32)
97 | })
98 | .padding()
99 | .shadow(color: .black.opacity(0.1), radius: 16, x: 0, y: 0)
100 | Color.clear
101 | }
102 | }
103 | }
104 |
105 | struct NodeCanvasToolbarView_Previews: PreviewProvider {
106 | static var previews: some View {
107 | NodeCanvasToolbarView()
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/NodeCanvas/NodeCanvasToolbarView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodeCanvasToolbarView.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/19/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodeCanvasToolbarView: View {
11 |
12 |
13 | @State private var showSettings = false
14 | @State private var showResetAlert = false
15 | @State private var showReadingProgress = false
16 | @EnvironmentObject var nodePageData : NodePageData
17 | @EnvironmentObject var environment : Environment
18 |
19 | var body: some View {
20 | ZStack (alignment: .bottom) {
21 |
22 | HStack(alignment: .center, spacing: 18) {
23 |
24 | // Button {
25 | // showReadingProgress = true
26 | // } label: {
27 | // VStack(alignment: .leading) {
28 | // HStack {
29 | // Text("Currently Reading \(Image(systemName: "chevron.up"))")
30 | // .font(.subheadline.monospaced())
31 | // }
32 | // Text("Chapter 1")
33 | // .font(.caption.monospaced())
34 | // }.padding(.horizontal, 8)
35 | // }
36 | // .popover(isPresented: $showReadingProgress) {
37 | // Text("")
38 | // }
39 |
40 | // Divider()
41 |
42 | Button {
43 | showResetAlert = true
44 | } label: {
45 | Image(systemName: "memories")
46 | .padding(.all, 8)
47 | }
48 | .alert("Reset?", isPresented: $showResetAlert, actions: {
49 | Button(role: .destructive) {
50 | nodePageData.reset()
51 | } label: {
52 | Text("Confirm")
53 | }
54 | Button(role: .cancel) {
55 | showResetAlert = false
56 | } label: {
57 | Text("Cancel")
58 | }
59 |
60 | }, message: {
61 | Text("The live scene and the editor node graph will be reset to its initial state")
62 | })
63 | // Button {
64 | // if nodePageData.playing {
65 | // nodePageData.playing = false
66 | // nodePageData.reset()
67 | // } else {
68 | // nodePageData.playing = true
69 | // }
70 | // } label: {
71 | // Image(systemName: nodePageData.playing ? "stop.fill" : "play.fill")
72 | // .padding(.all, 8)
73 | // }
74 | //
75 | ToggleButtonView(icon: .init(systemName:"rectangle.lefthalf.inset.filled"), state: $environment.toggleDocPanel)
76 | ToggleButtonView(icon: .init(systemName:"rectangle.righthalf.inset.filled"), state: $environment.toggleLivePanel)
77 |
78 | Divider()
79 |
80 | Button {
81 | showSettings = true
82 | } label: {
83 | Image(systemName: "ellipsis")
84 | .padding(.all, 8)
85 | }
86 | .popover(isPresented: $showSettings) {
87 | MoreNavigationView()
88 | }
89 | }
90 | .padding()
91 | .frame(height: 64)
92 | .background(
93 | Material.regular
94 | )
95 | .mask({
96 | RoundedRectangle(cornerRadius: 32)
97 | })
98 | .padding()
99 | .shadow(color: .black.opacity(0.1), radius: 16, x: 0, y: 0)
100 | Color.clear
101 | }
102 | }
103 | }
104 |
105 | struct NodeCanvasToolbarView_Previews: PreviewProvider {
106 | static var previews: some View {
107 | NodeCanvasToolbarView()
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/NodeEditor/View/NodeCanvas/NodePortView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePortView.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/19/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodePortView: View {
11 |
12 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
13 | @ObservedObject var nodePortData : NodePortData
14 | @State var holdingKnot : Bool = false
15 | @State var holdingConnection : NodePortConnectionData? = nil
16 |
17 | var textView : some View {
18 | Text("\(nodePortData.name)")
19 | .lineLimit(1)
20 | .font(.footnote.monospaced())
21 | }
22 |
23 | var circleView : some View {
24 | nodePortData.icon()
25 | .foregroundColor(nodePortData.color())
26 | .scaleEffect(holdingKnot ? 1.5 : 1)
27 | .frame(width: 8, height: 8, alignment: .center)
28 | .padding(.all, 8)
29 | .background(GeometryReader(content: { proxy in
30 | Color.clear
31 | .onAppear {
32 | nodePortData.canvasRect = proxy.frame(in: .named("canvas"))
33 | }
34 | .onChange(of: proxy.frame(in: .named("canvas"))) { portKnotFrame in
35 | nodePortData.canvasRect = portKnotFrame
36 | }
37 | }))
38 | .contentShape(Rectangle())
39 | .gesture(
40 | DragGesture(minimumDistance: 0, coordinateSpace: .named("canvas"))
41 | .onChanged({ value in
42 | if !holdingKnot {
43 | holdingKnot = true
44 |
45 | if case .can = nodePortData.canConnect() {
46 | // can connect
47 | let newConnection : NodePortConnectionData
48 |
49 | // new connection with basic setup
50 | if self.nodePortData.direction == .output {
51 | newConnection = NodePortConnectionData(startPort: self.nodePortData, endPort: nil)
52 | } else {
53 | newConnection = NodePortConnectionData(startPort: nil, endPort: self.nodePortData)
54 | }
55 | nodeCanvasData.pendingConnections.append(newConnection)
56 | holdingConnection = newConnection
57 | } else if let existingConnection = nodePortData.connections.first {
58 | // cannot connect, but if there is an existing line, disconnect that line
59 | existingConnection.isolate()
60 | existingConnection.disconnect(portDirection: nodePortData.direction)
61 | nodeCanvasData.pendingConnections.append(existingConnection)
62 | holdingConnection = existingConnection
63 | }
64 | }
65 |
66 | if let holdingConnection = holdingConnection,
67 | let pendingDirection = holdingConnection.getPendingPortDirection
68 | {
69 | if pendingDirection == .input {
70 | holdingConnection.endPosIfPortNull = value.location
71 | } else {
72 | holdingConnection.startPosIfPortNull = value.location
73 | }
74 | }
75 | })
76 | .onEnded({ value in
77 | holdingKnot = false
78 |
79 | if let pendingDirection = holdingConnection?.getPendingPortDirection,
80 | let portToConnectTo = nodeCanvasData.nodes.flatMap({ nodeData in
81 | pendingDirection == .input ? nodeData.inPorts : nodeData.outPorts
82 | }).filter({ nodePortData in
83 | if case .can = nodePortData.canConnectTo(anotherPort: self.nodePortData) {
84 | return true
85 | }
86 | return false
87 | }).filter({ nodePortData in
88 | nodePortData.canvasRect.contains(value.location)
89 | }).first {
90 | // if knot can be connected
91 | portToConnectTo.connectTo(anotherPort: self.nodePortData)
92 |
93 | } else {
94 |
95 | // if no knot to connect to
96 | holdingConnection?.disconnect()
97 | }
98 |
99 | // remove pending connection
100 | nodeCanvasData.pendingConnections.removeAll { connection in
101 | connection == holdingConnection
102 | }
103 | holdingConnection = nil
104 | })
105 | )
106 | }
107 |
108 | var body: some View {
109 | HStack {
110 | if self.nodePortData.direction == .input {
111 | circleView
112 | textView
113 | } else {
114 | textView
115 | circleView
116 | }
117 | }
118 | .padding(self.nodePortData.direction == .output ? .leading : .trailing, 8)
119 | .animation(.easeInOut, value: holdingKnot)
120 | }
121 | }
122 |
123 | struct NodePortView_Previews: PreviewProvider {
124 | static var previews: some View {
125 | NodePortView(nodePortData: NodeDataPortData(portID: 0, direction: .input))
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/View/NodeCanvas/NodePortView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePortView.swift
3 | // ShaderNodeEditor
4 | //
5 | // Created by fincher on 4/19/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NodePortView: View {
11 |
12 | @EnvironmentObject var nodeCanvasData : NodeCanvasData
13 | @ObservedObject var nodePortData : NodePortData
14 | @State var holdingKnot : Bool = false
15 | @State var holdingConnection : NodePortConnectionData? = nil
16 |
17 | var textView : some View {
18 | Text("\(nodePortData.name)")
19 | .lineLimit(1)
20 | .font(.footnote.monospaced())
21 | }
22 |
23 | var circleView : some View {
24 | nodePortData.icon()
25 | .foregroundColor(nodePortData.color())
26 | .scaleEffect(holdingKnot ? 1.5 : 1)
27 | .frame(width: 8, height: 8, alignment: .center)
28 | .padding(.all, 8)
29 | .background(GeometryReader(content: { proxy in
30 | Color.clear
31 | .onAppear {
32 | nodePortData.canvasRect = proxy.frame(in: .named("canvas"))
33 | }
34 | .onChange(of: proxy.frame(in: .named("canvas"))) { portKnotFrame in
35 | nodePortData.canvasRect = portKnotFrame
36 | }
37 | }))
38 | .contentShape(Rectangle())
39 | .gesture(
40 | DragGesture(minimumDistance: 0, coordinateSpace: .named("canvas"))
41 | .onChanged({ value in
42 | if !holdingKnot {
43 | holdingKnot = true
44 |
45 | if case .can = nodePortData.canConnect() {
46 | // can connect
47 | let newConnection : NodePortConnectionData
48 |
49 | // new connection with basic setup
50 | if self.nodePortData.direction == .output {
51 | newConnection = NodePortConnectionData(startPort: self.nodePortData, endPort: nil)
52 | } else {
53 | newConnection = NodePortConnectionData(startPort: nil, endPort: self.nodePortData)
54 | }
55 | nodeCanvasData.pendingConnections.append(newConnection)
56 | holdingConnection = newConnection
57 | } else if let existingConnection = nodePortData.connections.first {
58 | // cannot connect, but if there is an existing line, disconnect that line
59 | existingConnection.isolate()
60 | existingConnection.disconnect(portDirection: nodePortData.direction)
61 | nodeCanvasData.pendingConnections.append(existingConnection)
62 | holdingConnection = existingConnection
63 | }
64 | }
65 |
66 | if let holdingConnection = holdingConnection,
67 | let pendingDirection = holdingConnection.getPendingPortDirection
68 | {
69 | if pendingDirection == .input {
70 | holdingConnection.endPosIfPortNull = value.location
71 | } else {
72 | holdingConnection.startPosIfPortNull = value.location
73 | }
74 | }
75 | })
76 | .onEnded({ value in
77 | holdingKnot = false
78 |
79 | if let pendingDirection = holdingConnection?.getPendingPortDirection,
80 | let portToConnectTo = nodeCanvasData.nodes.flatMap({ nodeData in
81 | pendingDirection == .input ? nodeData.inPorts : nodeData.outPorts
82 | }).filter({ nodePortData in
83 | if case .can = nodePortData.canConnectTo(anotherPort: self.nodePortData) {
84 | return true
85 | }
86 | return false
87 | }).filter({ nodePortData in
88 | nodePortData.canvasRect.contains(value.location)
89 | }).first {
90 | // if knot can be connected
91 | portToConnectTo.connectTo(anotherPort: self.nodePortData)
92 |
93 | } else {
94 |
95 | // if no knot to connect to
96 | holdingConnection?.disconnect()
97 | }
98 |
99 | // remove pending connection
100 | nodeCanvasData.pendingConnections.removeAll { connection in
101 | connection == holdingConnection
102 | }
103 | holdingConnection = nil
104 | })
105 | )
106 | }
107 |
108 | var body: some View {
109 | HStack {
110 | if self.nodePortData.direction == .input {
111 | circleView
112 | textView
113 | } else {
114 | textView
115 | circleView
116 | }
117 | }
118 | .padding(self.nodePortData.direction == .output ? .leading : .trailing, 8)
119 | .animation(.easeInOut, value: holdingKnot)
120 | }
121 | }
122 |
123 | struct NodePortView_Previews: PreviewProvider {
124 | static var previews: some View {
125 | NodePortView(nodePortData: NodeDataPortData(portID: 0, direction: .input))
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/NodeEditor/Data/NodePages/NodePageDataChapterZero.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePageDataChapterZero.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import SpriteKit
11 |
12 | class NodePageDataProviderChapterZero : NodePageDataProvider
13 | {
14 | func modifyCanvas(nodePageData : NodePageData) {
15 | nodePageData.nodeCanvasData.nodes = [
16 | TriggerNode(nodeID: 0).withCanvasPosition(canvasPosition: .init(x: 188, y: 228)).withCanvas(canvasData: nodePageData.nodeCanvasData),
17 | PrintNode(nodeID: 1).withCanvasPosition(canvasPosition: .init(x: 286, y: 530)).withCanvas(canvasData: nodePageData.nodeCanvasData),
18 | ]
19 | if let port1 = nodePageData.nodeCanvasData.nodes[safe: 0]?.outControlPorts[safe: 0], let port2 = nodePageData.nodeCanvasData.nodes[safe: 1]?.inControlPorts[safe: 0] {
20 | port1.connectTo(anotherPort: port2)
21 | }
22 | if let node1 = nodePageData.nodeCanvasData.nodes[safe: 1] as? PrintNode {
23 | node1.content = "Hello World!"
24 | }
25 | }
26 |
27 | func modifyDocView(nodePageData : NodePageData) {
28 | nodePageData.docView = AnyView(
29 | List {
30 | Section {
31 | Text("👾 How to make games with Pegboard")
32 | .font(.title2.monospaced())
33 | Text("🖇 Chapter 0 - Pegboard?")
34 | .font(.subheadline.monospaced())
35 | } header: {
36 | VStack(alignment: .leading) {
37 | Color.clear.frame(height: 20)
38 | Text("TITLE")
39 | }
40 | }
41 |
42 | Section {
43 | Text("🤯 Pegboard is my take on the node-editor based interactive scripting solution. Node Editor is a common UI pattern used in visual programming, game dev, and low-code programming environments.")
44 | .font(.footnote.monospaced())
45 |
46 | Text("💡 A node represents a piece of logic block that can be chained together with other nodes via connection lines. The whole node graph, if composed in a well-orgainzed fashion, can greatly visualize the underlying logic. If feels right at home when you combine it with an iPad Pro.")
47 | .font(.footnote.monospaced())
48 | } header: {
49 | Text("A CRASH COURSE")
50 | }
51 |
52 | Section {
53 | Text("📱 In Pegboard, I have implemented a simple graphical editor as your can see at the right hand side. Try drag the two nodes around, connect and disconnect the in/out ports on nodes, and click buttons to see if the console prints the value defined by the print node! (Remember to turn on console logs if you are using Swift Playground)")
54 | .font(.footnote.monospaced())
55 |
56 | } header: {
57 | Text("DO IT YOURSELF")
58 | }
59 |
60 |
61 | Section {
62 | Text("🔍 You can long press on the blank area of canvas to add new nodes to the graph, some of these new nodes will be very important in the next chapter!")
63 | .font(.footnote.monospaced())
64 | Text("🎮 For now, just play around with the node editor I built and get familiar with it, then, click the 'Next Chapter' button below to learn how to build a little game.")
65 | .font(.footnote.monospaced())
66 |
67 | } header: {
68 | Text("LOOK AROUND")
69 | }
70 |
71 |
72 | Section {
73 | Button {
74 | nodePageData.switchTo(index: 1)
75 | } label: {
76 | Label("Next Chapter", systemImage: "arrow.right")
77 | .font(.body.bold().monospaced())
78 | }
79 | } header: {
80 | Text("CONTEXT")
81 | }
82 |
83 |
84 | }
85 | )
86 | }
87 |
88 | func modifyLiveScene(nodePageData : NodePageData) {
89 | let newScene = SKScene(fileNamed: "FlappyBird") ?? SKScene(size: .init(width: 375, height: 667))
90 |
91 | let birdAtlas = SKTextureAtlas(dictionary: ["downflap": UIImage(named: "yellowbird-downflap.png") as Any,
92 | "midflap": UIImage(named: "yellowbird-midflap.png") as Any,
93 | "upflap": UIImage(named: "yellowbird-upflap.png") as Any])
94 |
95 | let birdFlyFrames: [SKTexture] = [
96 | birdAtlas.textureNamed("downflap"),
97 | birdAtlas.textureNamed("midflap"),
98 | birdAtlas.textureNamed("upflap")
99 | ]
100 | birdFlyFrames.forEach { texture in
101 | texture.filteringMode = .nearest
102 | }
103 |
104 | let cityTexture = SKTexture(imageNamed: "background-day")
105 | cityTexture.filteringMode = .nearest
106 | let cityNode = SKSpriteNode(texture: cityTexture)
107 | cityNode.position = .init(x: 0, y: 50.5)
108 |
109 |
110 | let groundTexture = SKTexture(imageNamed: "base")
111 | groundTexture.filteringMode = .nearest
112 | let groundNode = SKSpriteNode(texture: groundTexture)
113 | groundNode.position = .init(x: 0, y: -240)
114 | groundNode.physicsBody = SKPhysicsBody(rectangleOf: groundNode.size)
115 | groundNode.physicsBody?.pinned = true
116 | groundNode.physicsBody?.affectedByGravity = false
117 | groundNode.physicsBody?.isDynamic = false
118 | groundNode.physicsBody?.allowsRotation = false
119 |
120 | newScene.addChild(cityNode)
121 | newScene.addChild(groundNode)
122 | newScene.scaleMode = .aspectFill
123 |
124 | nodePageData.liveScene = newScene
125 | EnvironmentManager.shared.environment.toggleLivePanel = false
126 | }
127 |
128 | func cheat(nodePageData : NodePageData) {
129 |
130 | }
131 |
132 | func destroy(nodePageData : NodePageData) {
133 | nodePageData.liveScene.removeAllChildren()
134 | nodePageData.nodeCanvasData.destroy()
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/Playgrounds/Pegboard.swiftpm/App/Data/NodePages/NodePageDataChapterZero.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NodePageDataChapterZero.swift
3 | // ScriptNode
4 | //
5 | // Created by fincher on 4/24/22.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import SpriteKit
11 |
12 | class NodePageDataProviderChapterZero : NodePageDataProvider
13 | {
14 | func modifyCanvas(nodePageData : NodePageData) {
15 | nodePageData.nodeCanvasData.nodes = [
16 | TriggerNode(nodeID: 0).withCanvasPosition(canvasPosition: .init(x: 188, y: 228)).withCanvas(canvasData: nodePageData.nodeCanvasData),
17 | PrintNode(nodeID: 1).withCanvasPosition(canvasPosition: .init(x: 286, y: 530)).withCanvas(canvasData: nodePageData.nodeCanvasData),
18 | ]
19 | if let port1 = nodePageData.nodeCanvasData.nodes[safe: 0]?.outControlPorts[safe: 0], let port2 = nodePageData.nodeCanvasData.nodes[safe: 1]?.inControlPorts[safe: 0] {
20 | port1.connectTo(anotherPort: port2)
21 | }
22 | if let node1 = nodePageData.nodeCanvasData.nodes[safe: 1] as? PrintNode {
23 | node1.content = "Hello World!"
24 | }
25 | }
26 |
27 | func modifyDocView(nodePageData : NodePageData) {
28 | nodePageData.docView = AnyView(
29 | List {
30 | Section {
31 | Text("👾 How to make games with Pegboard")
32 | .font(.title2.monospaced())
33 | Text("🖇 Chapter 0 - Pegboard?")
34 | .font(.subheadline.monospaced())
35 | } header: {
36 | VStack(alignment: .leading) {
37 | Color.clear.frame(height: 20)
38 | Text("TITLE")
39 | }
40 | }
41 |
42 | Section {
43 | Text("🤯 Pegboard is my take on the node-editor based interactive scripting solution. Node Editor is a common UI pattern used in visual programming, game dev, and low-code programming environments.")
44 | .font(.footnote.monospaced())
45 |
46 | Text("💡 A node represents a piece of logic block that can be chained together with other nodes via connection lines. The whole node graph, if composed in a well-orgainzed fashion, can greatly visualize the underlying logic. If feels right at home when you combine it with an iPad Pro.")
47 | .font(.footnote.monospaced())
48 | } header: {
49 | Text("A CRASH COURSE")
50 | }
51 |
52 | Section {
53 | Text("📱 In Pegboard, I have implemented a simple graphical editor as your can see at the right hand side. Try drag the two nodes around, connect and disconnect the in/out ports on nodes, and click buttons to see if the console prints the value defined by the print node! (Remember to turn on console logs if you are using Swift Playground)")
54 | .font(.footnote.monospaced())
55 |
56 | } header: {
57 | Text("DO IT YOURSELF")
58 | }
59 |
60 |
61 | Section {
62 | Text("🔍 You can long press on the blank area of canvas to add new nodes to the graph, some of these new nodes will be very important in the next chapter!")
63 | .font(.footnote.monospaced())
64 | Text("🎮 For now, just play around with the node editor I built and get familiar with it, then, click the 'Next Chapter' button below to learn how to build a little game.")
65 | .font(.footnote.monospaced())
66 |
67 | } header: {
68 | Text("LOOK AROUND")
69 | }
70 |
71 |
72 | Section {
73 | Button {
74 | nodePageData.switchTo(index: 1)
75 | } label: {
76 | Label("Next Chapter", systemImage: "arrow.right")
77 | .font(.body.bold().monospaced())
78 | }
79 | } header: {
80 | Text("CONTEXT")
81 | }
82 |
83 |
84 | }
85 | )
86 | }
87 |
88 | func modifyLiveScene(nodePageData : NodePageData) {
89 | let newScene = SKScene(fileNamed: "FlappyBird") ?? SKScene(size: .init(width: 375, height: 667))
90 |
91 | let birdAtlas = SKTextureAtlas(dictionary: ["downflap": UIImage(named: "yellowbird-downflap.png") as Any,
92 | "midflap": UIImage(named: "yellowbird-midflap.png") as Any,
93 | "upflap": UIImage(named: "yellowbird-upflap.png") as Any])
94 |
95 | let birdFlyFrames: [SKTexture] = [
96 | birdAtlas.textureNamed("downflap"),
97 | birdAtlas.textureNamed("midflap"),
98 | birdAtlas.textureNamed("upflap")
99 | ]
100 | birdFlyFrames.forEach { texture in
101 | texture.filteringMode = .nearest
102 | }
103 |
104 | let cityTexture = SKTexture(imageNamed: "background-day")
105 | cityTexture.filteringMode = .nearest
106 | let cityNode = SKSpriteNode(texture: cityTexture)
107 | cityNode.position = .init(x: 0, y: 50.5)
108 |
109 |
110 | let groundTexture = SKTexture(imageNamed: "base")
111 | groundTexture.filteringMode = .nearest
112 | let groundNode = SKSpriteNode(texture: groundTexture)
113 | groundNode.position = .init(x: 0, y: -240)
114 | groundNode.physicsBody = SKPhysicsBody(rectangleOf: groundNode.size)
115 | groundNode.physicsBody?.pinned = true
116 | groundNode.physicsBody?.affectedByGravity = false
117 | groundNode.physicsBody?.isDynamic = false
118 | groundNode.physicsBody?.allowsRotation = false
119 |
120 | newScene.addChild(cityNode)
121 | newScene.addChild(groundNode)
122 | newScene.scaleMode = .aspectFill
123 |
124 | nodePageData.liveScene = newScene
125 | EnvironmentManager.shared.environment.toggleLivePanel = false
126 | }
127 |
128 | func cheat(nodePageData : NodePageData) {
129 |
130 | }
131 |
132 | func destroy(nodePageData : NodePageData) {
133 | nodePageData.liveScene.removeAllChildren()
134 | nodePageData.nodeCanvasData.destroy()
135 | }
136 | }
137 |
--------------------------------------------------------------------------------