├── 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 | ![](GitHub/banner.jpg) 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 | ![](GitHub/IMG_0881.jpeg) 18 | ![](GitHub/IMG_0882.jpeg) 19 | ![](GitHub/IMG_0884.jpeg) 20 | ![](GitHub/IMG_0885.jpeg) 21 | ![](GitHub/IMG_0887.jpeg) 22 | ![](GitHub/IMG_0888.jpeg) 23 | ![](GitHub/IMG_0889.jpeg) 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 | ![](intro.png) 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 | --------------------------------------------------------------------------------