]> git.r.bdr.sh - rbdr/map/commitdiff
Map 3 first commit: files, groups and layout
authorRuben Beltran del Rio <redacted>
Mon, 16 Sep 2024 09:10:32 +0000 (11:10 +0200)
committerRuben Beltran del Rio <redacted>
Mon, 16 Sep 2024 09:10:32 +0000 (11:10 +0200)
52 files changed:
.gitignore
Makefile
Map.xcodeproj/project.pbxproj
Map.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Map/Assets.xcassets/Colors/Theme/Darker Neutral Gray.colorset/Contents.json [new file with mode: 0644]
Map/Assets.xcassets/Colors/Theme/Hermosa Pink.colorset/Contents.json [new file with mode: 0644]
Map/Assets.xcassets/Colors/Theme/Light Porcelain Green.colorset/Contents.json [new file with mode: 0644]
Map/Assets.xcassets/Colors/Theme/Naples Yellow.colorset/Contents.json [new file with mode: 0644]
Map/Core Extensions/Date+format.swift [deleted file]
Map/Core Extensions/Font+Theme.swift [deleted file]
Map/Core Extensions/NSImage+writePNG.swift [deleted file]
Map/Data/AppState.swift [deleted file]
Map/Data/MapDocument.swift [new file with mode: 0644]
Map/Data/Models/Map+parse.swift [deleted file]
Map/Data/Persistence.swift [deleted file]
Map/Data/Store.swift [deleted file]
Map/Info.plist
Map/Logic/Constants.swift [new file with mode: 0644]
Map/Logic/MapParser/MapParser.swift
Map/Logic/MapParser/Strategies/GroupParserStrategy.swift [new file with mode: 0644]
Map/Map.entitlements
Map/Map.xcdatamodeld/.xccurrentversion [deleted file]
Map/Map.xcdatamodeld/Map.xcdatamodel/contents [deleted file]
Map/MapApp.swift
Map/Presentation/Base Components/EvolutionPicker.swift
Map/Presentation/Base Components/MapRender/MapAxes.swift
Map/Presentation/Base Components/MapRender/MapBlockers.swift
Map/Presentation/Base Components/MapRender/MapEdges.swift
Map/Presentation/Base Components/MapRender/MapGroup.swift [new file with mode: 0644]
Map/Presentation/Base Components/MapRender/MapGroups.swift [new file with mode: 0644]
Map/Presentation/Base Components/MapRender/MapNotes.swift
Map/Presentation/Base Components/MapRender/MapOpportunities.swift
Map/Presentation/Base Components/MapRender/MapStages.swift
Map/Presentation/Base Components/MapRender/MapVertices.swift
Map/Presentation/Base Components/MapTextEditor.swift
Map/Presentation/Commands/MapCommands.swift [new file with mode: 0644]
Map/Presentation/Complex Components/MapRender/MapRenderView.swift
Map/Presentation/EvolutionPicker.swift [new file with mode: 0644]
Map/Presentation/MapEditor.swift [new file with mode: 0644]
Map/Presentation/Screens/EmptyMapDetailScreen.swift [deleted file]
Map/Presentation/Screens/MapDetailScreen.swift [deleted file]
Map/Presentation/Theme/Color+theme.swift [moved from Map/Core Extensions/Color+Theme.swift with 66% similarity]
Map/Presentation/Theme/Dimensions.swift [new file with mode: 0644]
Map/Presentation/Theme/Font+theme.swift [new file with mode: 0644]
Map/Presentation/Theme/NSColor+theme.swift [moved from Map/Core Extensions/NSColor+Theme.swift with 75% similarity]
Map/Presentation/ViewStyle.swift [new file with mode: 0644]
Map/Presentation/Windows/MapEditorWindow.swift [deleted file]
MapTests/Info.plist [deleted file]
MapTests/MapTests.swift
MapUITests/Info.plist [deleted file]
MapUITests/MapUITests.swift
MapUITests/MapUITestsLaunchTests.swift [new file with mode: 0644]

index c20bac49f5671047ca3b4bce432b81bf674bf5b7..996111eea43f93e418718475dc9bc88da690cccc 100644 (file)
@@ -24,4 +24,4 @@ DerivedData/
 
 ## Gcc Patch
 /*.gcno
 
 ## Gcc Patch
 /*.gcno
-
+.DS_Store
index fe56dfc322dc8cf63cfcd5b85768131ad66bb676..c02a2276e9ec30412e53433737d35a645131f279 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -4,12 +4,6 @@ format:
        swift format -i -r .
 
 lint:
        swift format -i -r .
 
 lint:
-       swift format -m lint -r .
-
-docker-build:
-       docker build --force-rm --build-arg swift_version=$(swift_version) -t doapp/do:$(swift_version) .
-
-docker-push: docker-build
-       docker push doapp/do:$(swift_version)
+       swift format lint -r .
 
 .PHONY: format lint docker-build docker-push
 
 .PHONY: format lint docker-build docker-push
index 9cd5d87f48829e9bc33f0d6064131bcb2e1c9c58..15ddee9fa9111c33edcced54d68eb457a225e924 100644 (file)
        archiveVersion = 1;
        classes = {
        };
        archiveVersion = 1;
        classes = {
        };
-       objectVersion = 53;
+       objectVersion = 60;
        objects = {
 
 /* Begin PBXBuildFile section */
        objects = {
 
 /* Begin PBXBuildFile section */
-               B523C73D25C98D9800C44061 /* NSImage+writePNG.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C73C25C98D9800C44061 /* NSImage+writePNG.swift */; };
-               B523C74625C9BD3500C44061 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B523C74525C9BD3500C44061 /* CloudKit.framework */; };
-               B523C74B25C9C1BA00C44061 /* EmptyMapDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C74A25C9C1BA00C44061 /* EmptyMapDetailScreen.swift */; };
-               B523C75A25C9FD4900C44061 /* MapAxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C75925C9FD4900C44061 /* MapAxes.swift */; };
-               B523C76225CA05A300C44061 /* MapStages.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C76125CA05A300C44061 /* MapStages.swift */; };
-               B523C76725CA071B00C44061 /* MapVertices.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C76625CA071B00C44061 /* MapVertices.swift */; };
-               B523C76C25CA0DFA00C44061 /* MapEdges.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C76B25CA0DFA00C44061 /* MapEdges.swift */; };
-               B523C77125CA121300C44061 /* MapBlockers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C77025CA121300C44061 /* MapBlockers.swift */; };
-               B523C77E25CA294C00C44061 /* MapOpportunities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B523C77D25CA294C00C44061 /* MapOpportunities.swift */; };
-               B526257225C874F9003E73B7 /* MapApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B526257125C874F9003E73B7 /* MapApp.swift */; };
-               B526257425C874F9003E73B7 /* MapEditorWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B526257325C874F9003E73B7 /* MapEditorWindow.swift */; };
-               B526257625C874FA003E73B7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B526257525C874FA003E73B7 /* Assets.xcassets */; };
-               B526257925C874FA003E73B7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B526257825C874FA003E73B7 /* Preview Assets.xcassets */; };
-               B526257B25C874FA003E73B7 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = B526257A25C874FA003E73B7 /* Persistence.swift */; };
-               B526257E25C874FA003E73B7 /* Map.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B526257C25C874FA003E73B7 /* Map.xcdatamodeld */; };
-               B526258A25C874FA003E73B7 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B526258925C874FA003E73B7 /* MapTests.swift */; };
-               B526259525C874FA003E73B7 /* MapUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B526259425C874FA003E73B7 /* MapUITests.swift */; };
-               B52625A625C876C3003E73B7 /* MapDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625A525C876C3003E73B7 /* MapDetailScreen.swift */; };
-               B52625AB25C87909003E73B7 /* Date+format.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625AA25C87909003E73B7 /* Date+format.swift */; };
-               B52625B025C87C14003E73B7 /* MapRenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625AF25C87C14003E73B7 /* MapRenderView.swift */; };
-               B52625BB25C884C2003E73B7 /* Map+parse.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625BA25C884C2003E73B7 /* Map+parse.swift */; };
-               B52625C625C8BD2A003E73B7 /* Stage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B52625C525C8BD2A003E73B7 /* Stage.swift */; };
-               B539516C25CB0C9300959F72 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539516B25CB0C9200959F72 /* Store.swift */; };
-               B539517425CB0CA400959F72 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539517325CB0CA400959F72 /* AppState.swift */; };
-               B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B539518025CB2D7A00959F72 /* MapTextEditor.swift */; };
-               B5CF75C925CC19FC003BFF3D /* EvolutionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */; };
-               B5CF75CF25CC7965003BFF3D /* MapParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75CE25CC7965003BFF3D /* MapParser.swift */; };
-               B5CF75D825CC79BC003BFF3D /* VertexParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */; };
-               B5CF75DD25CC79D7003BFF3D /* EdgeParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */; };
-               B5CF75E225CC79ED003BFF3D /* BlockerParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */; };
-               B5CF75EA25CC7A13003BFF3D /* OpportunityParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */; };
-               B5CF75EF25CC7A4A003BFF3D /* StageParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */; };
-               B5CF75F725CC97CA003BFF3D /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5CF75F625CC97CA003BFF3D /* Debouncer.swift */; };
-               B5F8D3082A06DD8C000EEA24 /* Font+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F8D3072A06DD8C000EEA24 /* Font+Theme.swift */; };
-               B5F8D30B2A06E3E6000EEA24 /* Color+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F8D30A2A06E3E6000EEA24 /* Color+Theme.swift */; };
-               B5F8D30E2A06E5C2000EEA24 /* Patterns in Frameworks */ = {isa = PBXBuildFile; productRef = B5F8D30D2A06E5C2000EEA24 /* Patterns */; };
-               B5F8D3102A07B33D000EEA24 /* NSColor+Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F8D30F2A07B33D000EEA24 /* NSColor+Theme.swift */; };
-               B5F8D3122A07B690000EEA24 /* NoteParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F8D3112A07B690000EEA24 /* NoteParserStrategy.swift */; };
-               B5F8D3142A07C05F000EEA24 /* MapNotes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F8D3132A07C05F000EEA24 /* MapNotes.swift */; };
+               B5012E3F2C96232A00AC4D68 /* EvolutionPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E3E2C96232300AC4D68 /* EvolutionPicker.swift */; };
+               B5012E422C96235E00AC4D68 /* Stage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E412C96235B00AC4D68 /* Stage.swift */; };
+               B5012E452C9623C700AC4D68 /* Font+theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E442C9623C500AC4D68 /* Font+theme.swift */; };
+               B5012E472C96243C00AC4D68 /* MapTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E462C96243500AC4D68 /* MapTextEditor.swift */; };
+               B5012E492C96245B00AC4D68 /* Color+theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E482C96245800AC4D68 /* Color+theme.swift */; };
+               B5012E4B2C96246F00AC4D68 /* NSColor+theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4A2C96246D00AC4D68 /* NSColor+theme.swift */; };
+               B5012E572C96249400AC4D68 /* NoteParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4D2C96249400AC4D68 /* NoteParserStrategy.swift */; };
+               B5012E582C96249400AC4D68 /* BlockerParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4F2C96249400AC4D68 /* BlockerParserStrategy.swift */; };
+               B5012E592C96249400AC4D68 /* VertexParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4C2C96249400AC4D68 /* VertexParserStrategy.swift */; };
+               B5012E5A2C96249400AC4D68 /* MapParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E532C96249400AC4D68 /* MapParser.swift */; };
+               B5012E5B2C96249400AC4D68 /* OpportunityParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E502C96249400AC4D68 /* OpportunityParserStrategy.swift */; };
+               B5012E5C2C96249400AC4D68 /* EdgeParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E4E2C96249400AC4D68 /* EdgeParserStrategy.swift */; };
+               B5012E5D2C96249400AC4D68 /* StageParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E512C96249400AC4D68 /* StageParserStrategy.swift */; };
+               B5012E5E2C96249400AC4D68 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E552C96249400AC4D68 /* Debouncer.swift */; };
+               B5012E622C96254700AC4D68 /* MapRenderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E5F2C96254700AC4D68 /* MapRenderView.swift */; };
+               B5012E6B2C96255A00AC4D68 /* MapAxes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E632C96255A00AC4D68 /* MapAxes.swift */; };
+               B5012E6C2C96255A00AC4D68 /* MapVertices.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E652C96255A00AC4D68 /* MapVertices.swift */; };
+               B5012E6D2C96255A00AC4D68 /* MapStages.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E642C96255A00AC4D68 /* MapStages.swift */; };
+               B5012E6E2C96255A00AC4D68 /* MapNotes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E662C96255A00AC4D68 /* MapNotes.swift */; };
+               B5012E6F2C96255A00AC4D68 /* MapEdges.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E672C96255A00AC4D68 /* MapEdges.swift */; };
+               B5012E702C96255A00AC4D68 /* MapOpportunities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E692C96255A00AC4D68 /* MapOpportunities.swift */; };
+               B5012E712C96255A00AC4D68 /* MapBlockers.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E682C96255A00AC4D68 /* MapBlockers.swift */; };
+               B5012E742C9625E200AC4D68 /* Patterns in Frameworks */ = {isa = PBXBuildFile; productRef = B5012E732C9625E200AC4D68 /* Patterns */; };
+               B5012E7A2C96F02F00AC4D68 /* Dimensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E792C96F02E00AC4D68 /* Dimensions.swift */; };
+               B5012E7C2C972B6C00AC4D68 /* GroupParserStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E7B2C972B6600AC4D68 /* GroupParserStrategy.swift */; };
+               B5012E7F2C97315800AC4D68 /* ConcaveHull in Frameworks */ = {isa = PBXBuildFile; productRef = B5012E7E2C97315800AC4D68 /* ConcaveHull */; };
+               B5012E812C97318600AC4D68 /* MapGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E802C97318300AC4D68 /* MapGroups.swift */; };
+               B5012E872C97874600AC4D68 /* MapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E862C97874400AC4D68 /* MapGroup.swift */; };
+               B5012E8A2C98235500AC4D68 /* MapCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E892C98235300AC4D68 /* MapCommands.swift */; };
+               B5012E8C2C98244000AC4D68 /* ViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E8B2C98243E00AC4D68 /* ViewStyle.swift */; };
+               B5012E8E2C9828D000AC4D68 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5012E8D2C9828CE00AC4D68 /* Constants.swift */; };
+               B54587102C961E9C0067B788 /* MapApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B545870F2C961E9C0067B788 /* MapApp.swift */; };
+               B54587122C961E9C0067B788 /* MapDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587112C961E9C0067B788 /* MapDocument.swift */; };
+               B54587142C961E9C0067B788 /* MapEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587132C961E9C0067B788 /* MapEditor.swift */; };
+               B54587162C961E9E0067B788 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B54587152C961E9E0067B788 /* Assets.xcassets */; };
+               B54587192C961E9E0067B788 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B54587182C961E9E0067B788 /* Preview Assets.xcassets */; };
+               B54587252C961E9E0067B788 /* MapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587242C961E9E0067B788 /* MapTests.swift */; };
+               B545872F2C961E9E0067B788 /* MapUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B545872E2C961E9E0067B788 /* MapUITests.swift */; };
+               B54587312C961E9E0067B788 /* MapUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
-               B526258625C874FA003E73B7 /* PBXContainerItemProxy */ = {
+               B54587212C961E9E0067B788 /* PBXContainerItemProxy */ = {
                        isa = PBXContainerItemProxy;
                        isa = PBXContainerItemProxy;
-                       containerPortal = B526256625C874F9003E73B7 /* Project object */;
+                       containerPortal = B54587042C961E9C0067B788 /* Project object */;
                        proxyType = 1;
                        proxyType = 1;
-                       remoteGlobalIDString = B526256D25C874F9003E73B7;
-                       remoteInfo = Map;
+                       remoteGlobalIDString = B545870B2C961E9C0067B788;
+                       remoteInfo = Map2;
                };
                };
-               B526259125C874FA003E73B7 /* PBXContainerItemProxy */ = {
+               B545872B2C961E9E0067B788 /* PBXContainerItemProxy */ = {
                        isa = PBXContainerItemProxy;
                        isa = PBXContainerItemProxy;
-                       containerPortal = B526256625C874F9003E73B7 /* Project object */;
+                       containerPortal = B54587042C961E9C0067B788 /* Project object */;
                        proxyType = 1;
                        proxyType = 1;
-                       remoteGlobalIDString = B526256D25C874F9003E73B7;
-                       remoteInfo = Map;
+                       remoteGlobalIDString = B545870B2C961E9C0067B788;
+                       remoteInfo = Map2;
                };
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
                };
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
-               B523C73C25C98D9800C44061 /* NSImage+writePNG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+writePNG.swift"; sourceTree = "<group>"; };
-               B523C74525C9BD3500C44061 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
-               B523C74A25C9C1BA00C44061 /* EmptyMapDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyMapDetailScreen.swift; sourceTree = "<group>"; };
-               B523C75925C9FD4900C44061 /* MapAxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAxes.swift; sourceTree = "<group>"; };
-               B523C76125CA05A300C44061 /* MapStages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapStages.swift; sourceTree = "<group>"; };
-               B523C76625CA071B00C44061 /* MapVertices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapVertices.swift; sourceTree = "<group>"; };
-               B523C76B25CA0DFA00C44061 /* MapEdges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapEdges.swift; sourceTree = "<group>"; };
-               B523C77025CA121300C44061 /* MapBlockers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapBlockers.swift; sourceTree = "<group>"; };
-               B523C77D25CA294C00C44061 /* MapOpportunities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapOpportunities.swift; sourceTree = "<group>"; };
-               B526256E25C874F9003E73B7 /* Map.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Map.app; sourceTree = BUILT_PRODUCTS_DIR; };
-               B526257125C874F9003E73B7 /* MapApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapApp.swift; sourceTree = "<group>"; };
-               B526257325C874F9003E73B7 /* MapEditorWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapEditorWindow.swift; sourceTree = "<group>"; };
-               B526257525C874FA003E73B7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
-               B526257825C874FA003E73B7 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
-               B526257A25C874FA003E73B7 /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
-               B526257D25C874FA003E73B7 /* Map.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Map.xcdatamodel; sourceTree = "<group>"; };
-               B526257F25C874FA003E73B7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-               B526258025C874FA003E73B7 /* Map.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Map.entitlements; sourceTree = "<group>"; };
-               B526258525C874FA003E73B7 /* MapTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MapTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
-               B526258925C874FA003E73B7 /* MapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTests.swift; sourceTree = "<group>"; };
-               B526258B25C874FA003E73B7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-               B526259025C874FA003E73B7 /* MapUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MapUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
-               B526259425C874FA003E73B7 /* MapUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITests.swift; sourceTree = "<group>"; };
-               B526259625C874FA003E73B7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-               B52625A525C876C3003E73B7 /* MapDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDetailScreen.swift; sourceTree = "<group>"; };
-               B52625AA25C87909003E73B7 /* Date+format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+format.swift"; sourceTree = "<group>"; };
-               B52625AF25C87C14003E73B7 /* MapRenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRenderView.swift; sourceTree = "<group>"; };
-               B52625BA25C884C2003E73B7 /* Map+parse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Map+parse.swift"; sourceTree = "<group>"; };
-               B52625C525C8BD2A003E73B7 /* Stage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stage.swift; sourceTree = "<group>"; };
-               B539516B25CB0C9200959F72 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
-               B539517325CB0CA400959F72 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
-               B539518025CB2D7A00959F72 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = "<group>"; };
-               B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvolutionPicker.swift; sourceTree = "<group>"; };
-               B5CF75CE25CC7965003BFF3D /* MapParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapParser.swift; sourceTree = "<group>"; };
-               B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VertexParserStrategy.swift; sourceTree = "<group>"; };
-               B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeParserStrategy.swift; sourceTree = "<group>"; };
-               B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockerParserStrategy.swift; sourceTree = "<group>"; };
-               B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpportunityParserStrategy.swift; sourceTree = "<group>"; };
-               B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageParserStrategy.swift; sourceTree = "<group>"; };
-               B5CF75F625CC97CA003BFF3D /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
-               B5F8D3072A06DD8C000EEA24 /* Font+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+Theme.swift"; sourceTree = "<group>"; };
-               B5F8D30A2A06E3E6000EEA24 /* Color+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Theme.swift"; sourceTree = "<group>"; };
-               B5F8D30F2A07B33D000EEA24 /* NSColor+Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Theme.swift"; sourceTree = "<group>"; };
-               B5F8D3112A07B690000EEA24 /* NoteParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteParserStrategy.swift; sourceTree = "<group>"; };
-               B5F8D3132A07C05F000EEA24 /* MapNotes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapNotes.swift; sourceTree = "<group>"; };
+               B5012E3E2C96232300AC4D68 /* EvolutionPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EvolutionPicker.swift; sourceTree = "<group>"; };
+               B5012E412C96235B00AC4D68 /* Stage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stage.swift; sourceTree = "<group>"; };
+               B5012E442C9623C500AC4D68 /* Font+theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Font+theme.swift"; sourceTree = "<group>"; };
+               B5012E462C96243500AC4D68 /* MapTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTextEditor.swift; sourceTree = "<group>"; };
+               B5012E482C96245800AC4D68 /* Color+theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+theme.swift"; sourceTree = "<group>"; };
+               B5012E4A2C96246D00AC4D68 /* NSColor+theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+theme.swift"; sourceTree = "<group>"; };
+               B5012E4C2C96249400AC4D68 /* VertexParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VertexParserStrategy.swift; sourceTree = "<group>"; };
+               B5012E4D2C96249400AC4D68 /* NoteParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteParserStrategy.swift; sourceTree = "<group>"; };
+               B5012E4E2C96249400AC4D68 /* EdgeParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeParserStrategy.swift; sourceTree = "<group>"; };
+               B5012E4F2C96249400AC4D68 /* BlockerParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockerParserStrategy.swift; sourceTree = "<group>"; };
+               B5012E502C96249400AC4D68 /* OpportunityParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpportunityParserStrategy.swift; sourceTree = "<group>"; };
+               B5012E512C96249400AC4D68 /* StageParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageParserStrategy.swift; sourceTree = "<group>"; };
+               B5012E532C96249400AC4D68 /* MapParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapParser.swift; sourceTree = "<group>"; };
+               B5012E552C96249400AC4D68 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
+               B5012E5F2C96254700AC4D68 /* MapRenderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRenderView.swift; sourceTree = "<group>"; };
+               B5012E632C96255A00AC4D68 /* MapAxes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAxes.swift; sourceTree = "<group>"; };
+               B5012E642C96255A00AC4D68 /* MapStages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapStages.swift; sourceTree = "<group>"; };
+               B5012E652C96255A00AC4D68 /* MapVertices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapVertices.swift; sourceTree = "<group>"; };
+               B5012E662C96255A00AC4D68 /* MapNotes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapNotes.swift; sourceTree = "<group>"; };
+               B5012E672C96255A00AC4D68 /* MapEdges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapEdges.swift; sourceTree = "<group>"; };
+               B5012E682C96255A00AC4D68 /* MapBlockers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapBlockers.swift; sourceTree = "<group>"; };
+               B5012E692C96255A00AC4D68 /* MapOpportunities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapOpportunities.swift; sourceTree = "<group>"; };
+               B5012E792C96F02E00AC4D68 /* Dimensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dimensions.swift; sourceTree = "<group>"; };
+               B5012E7B2C972B6600AC4D68 /* GroupParserStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupParserStrategy.swift; sourceTree = "<group>"; };
+               B5012E802C97318300AC4D68 /* MapGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapGroups.swift; sourceTree = "<group>"; };
+               B5012E862C97874400AC4D68 /* MapGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapGroup.swift; sourceTree = "<group>"; };
+               B5012E892C98235300AC4D68 /* MapCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapCommands.swift; sourceTree = "<group>"; };
+               B5012E8B2C98243E00AC4D68 /* ViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStyle.swift; sourceTree = "<group>"; };
+               B5012E8D2C9828CE00AC4D68 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
+               B545870C2C961E9C0067B788 /* Map.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Map.app; sourceTree = BUILT_PRODUCTS_DIR; };
+               B545870F2C961E9C0067B788 /* MapApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapApp.swift; sourceTree = "<group>"; };
+               B54587112C961E9C0067B788 /* MapDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapDocument.swift; sourceTree = "<group>"; };
+               B54587132C961E9C0067B788 /* MapEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapEditor.swift; sourceTree = "<group>"; };
+               B54587152C961E9E0067B788 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+               B54587182C961E9E0067B788 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
+               B545871A2C961E9E0067B788 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+               B545871B2C961E9E0067B788 /* Map.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Map.entitlements; sourceTree = "<group>"; };
+               B54587202C961E9E0067B788 /* MapTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MapTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+               B54587242C961E9E0067B788 /* MapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapTests.swift; sourceTree = "<group>"; };
+               B545872A2C961E9E0067B788 /* Map2UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Map2UITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+               B545872E2C961E9E0067B788 /* MapUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITests.swift; sourceTree = "<group>"; };
+               B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUITestsLaunchTests.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
-               B526256B25C874F9003E73B7 /* Frameworks */ = {
+               B54587092C961E9C0067B788 /* Frameworks */ = {
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
-                               B523C74625C9BD3500C44061 /* CloudKit.framework in Frameworks */,
-                               B5F8D30E2A06E5C2000EEA24 /* Patterns in Frameworks */,
+                               B5012E742C9625E200AC4D68 /* Patterns in Frameworks */,
+                               B5012E7F2C97315800AC4D68 /* ConcaveHull in Frameworks */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
-               B526258225C874FA003E73B7 /* Frameworks */ = {
+               B545871D2C961E9E0067B788 /* Frameworks */ = {
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
-               B526258D25C874FA003E73B7 /* Frameworks */ = {
+               B54587272C961E9E0067B788 /* Frameworks */ = {
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        isa = PBXFrameworksBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
-               B523C74425C9BD3500C44061 /* Frameworks */ = {
+               B5012E3C2C96222E00AC4D68 /* Data */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B523C74525C9BD3500C44061 /* CloudKit.framework */,
+                               B5012E412C96235B00AC4D68 /* Stage.swift */,
+                               B54587112C961E9C0067B788 /* MapDocument.swift */,
                        );
                        );
-                       name = Frameworks;
-                       sourceTree = "<group>";
-               };
-               B523C75825C9FD3A00C44061 /* MapRender */ = {
-                       isa = PBXGroup;
-                       children = (
-                               B523C75925C9FD4900C44061 /* MapAxes.swift */,
-                               B523C76125CA05A300C44061 /* MapStages.swift */,
-                               B523C76625CA071B00C44061 /* MapVertices.swift */,
-                               B5F8D3132A07C05F000EEA24 /* MapNotes.swift */,
-                               B523C76B25CA0DFA00C44061 /* MapEdges.swift */,
-                               B523C77025CA121300C44061 /* MapBlockers.swift */,
-                               B523C77D25CA294C00C44061 /* MapOpportunities.swift */,
-                       );
-                       path = MapRender;
-                       sourceTree = "<group>";
-               };
-               B526256525C874F9003E73B7 = {
-                       isa = PBXGroup;
-                       children = (
-                               B526257025C874F9003E73B7 /* Map */,
-                               B526258825C874FA003E73B7 /* MapTests */,
-                               B526259325C874FA003E73B7 /* MapUITests */,
-                               B526256F25C874F9003E73B7 /* Products */,
-                               B523C74425C9BD3500C44061 /* Frameworks */,
-                       );
-                       sourceTree = "<group>";
-               };
-               B526256F25C874F9003E73B7 /* Products */ = {
-                       isa = PBXGroup;
-                       children = (
-                               B526256E25C874F9003E73B7 /* Map.app */,
-                               B526258525C874FA003E73B7 /* MapTests.xctest */,
-                               B526259025C874FA003E73B7 /* MapUITests.xctest */,
-                       );
-                       name = Products;
+                       path = Data;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B526257025C874F9003E73B7 /* Map */ = {
+               B5012E3D2C96223800AC4D68 /* Presentation */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B526257125C874F9003E73B7 /* MapApp.swift */,
-                               B5F8D2FE2A06DB3A000EEA24 /* Data */,
-                               B5F8D3092A06DE1A000EEA24 /* Logic */,
-                               B5F8D2FF2A06DB40000EEA24 /* Presentation */,
-                               B52625B425C87D54003E73B7 /* Core Extensions */,
-                               B526257525C874FA003E73B7 /* Assets.xcassets */,
-                               B526257F25C874FA003E73B7 /* Info.plist */,
-                               B526258025C874FA003E73B7 /* Map.entitlements */,
-                               B526257C25C874FA003E73B7 /* Map.xcdatamodeld */,
-                               B526257725C874FA003E73B7 /* Preview Content */,
+                               B5012E8B2C98243E00AC4D68 /* ViewStyle.swift */,
+                               B5012E882C98234F00AC4D68 /* Commands */,
+                               B5012E612C96254700AC4D68 /* Complex Components */,
+                               B5012E432C9623BC00AC4D68 /* Theme */,
+                               B5012E402C96232E00AC4D68 /* Base Components */,
+                               B54587132C961E9C0067B788 /* MapEditor.swift */,
                        );
                        );
-                       path = Map;
+                       path = Presentation;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B526257725C874FA003E73B7 /* Preview Content */ = {
+               B5012E402C96232E00AC4D68 /* Base Components */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B526257825C874FA003E73B7 /* Preview Assets.xcassets */,
+                               B5012E6A2C96255A00AC4D68 /* MapRender */,
+                               B5012E462C96243500AC4D68 /* MapTextEditor.swift */,
+                               B5012E3E2C96232300AC4D68 /* EvolutionPicker.swift */,
                        );
                        );
-                       path = "Preview Content";
+                       path = "Base Components";
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B526258825C874FA003E73B7 /* MapTests */ = {
+               B5012E432C9623BC00AC4D68 /* Theme */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B526258925C874FA003E73B7 /* MapTests.swift */,
-                               B526258B25C874FA003E73B7 /* Info.plist */,
+                               B5012E792C96F02E00AC4D68 /* Dimensions.swift */,
+                               B5012E4A2C96246D00AC4D68 /* NSColor+theme.swift */,
+                               B5012E482C96245800AC4D68 /* Color+theme.swift */,
+                               B5012E442C9623C500AC4D68 /* Font+theme.swift */,
                        );
                        );
-                       path = MapTests;
+                       path = Theme;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B526259325C874FA003E73B7 /* MapUITests */ = {
+               B5012E522C96249400AC4D68 /* Strategies */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B526259425C874FA003E73B7 /* MapUITests.swift */,
-                               B526259625C874FA003E73B7 /* Info.plist */,
+                               B5012E7B2C972B6600AC4D68 /* GroupParserStrategy.swift */,
+                               B5012E4C2C96249400AC4D68 /* VertexParserStrategy.swift */,
+                               B5012E4D2C96249400AC4D68 /* NoteParserStrategy.swift */,
+                               B5012E4E2C96249400AC4D68 /* EdgeParserStrategy.swift */,
+                               B5012E4F2C96249400AC4D68 /* BlockerParserStrategy.swift */,
+                               B5012E502C96249400AC4D68 /* OpportunityParserStrategy.swift */,
+                               B5012E512C96249400AC4D68 /* StageParserStrategy.swift */,
                        );
                        );
-                       path = MapUITests;
+                       path = Strategies;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B52625B425C87D54003E73B7 /* Core Extensions */ = {
+               B5012E542C96249400AC4D68 /* MapParser */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B52625AA25C87909003E73B7 /* Date+format.swift */,
-                               B523C73C25C98D9800C44061 /* NSImage+writePNG.swift */,
-                               B5F8D3072A06DD8C000EEA24 /* Font+Theme.swift */,
-                               B5F8D30F2A07B33D000EEA24 /* NSColor+Theme.swift */,
-                               B5F8D30A2A06E3E6000EEA24 /* Color+Theme.swift */,
+                               B5012E522C96249400AC4D68 /* Strategies */,
+                               B5012E532C96249400AC4D68 /* MapParser.swift */,
                        );
                        );
-                       path = "Core Extensions";
+                       path = MapParser;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5CF75CD25CC7953003BFF3D /* MapParser */ = {
+               B5012E562C96249400AC4D68 /* Logic */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B5CF75D625CC79A4003BFF3D /* Strategies */,
-                               B5CF75CE25CC7965003BFF3D /* MapParser.swift */,
+                               B5012E8D2C9828CE00AC4D68 /* Constants.swift */,
+                               B5012E542C96249400AC4D68 /* MapParser */,
+                               B5012E552C96249400AC4D68 /* Debouncer.swift */,
                        );
                        );
-                       path = MapParser;
+                       path = Logic;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5CF75D625CC79A4003BFF3D /* Strategies */ = {
+               B5012E602C96254700AC4D68 /* MapRender */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B5CF75D725CC79BC003BFF3D /* VertexParserStrategy.swift */,
-                               B5F8D3112A07B690000EEA24 /* NoteParserStrategy.swift */,
-                               B5CF75DC25CC79D7003BFF3D /* EdgeParserStrategy.swift */,
-                               B5CF75E125CC79ED003BFF3D /* BlockerParserStrategy.swift */,
-                               B5CF75E925CC7A13003BFF3D /* OpportunityParserStrategy.swift */,
-                               B5CF75EE25CC7A4A003BFF3D /* StageParserStrategy.swift */,
+                               B5012E5F2C96254700AC4D68 /* MapRenderView.swift */,
                        );
                        );
-                       path = Strategies;
+                       path = MapRender;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D2FE2A06DB3A000EEA24 /* Data */ = {
+               B5012E612C96254700AC4D68 /* Complex Components */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B5F8D3012A06DB75000EEA24 /* Models */,
-                               B52625C525C8BD2A003E73B7 /* Stage.swift */,
-                               B526257A25C874FA003E73B7 /* Persistence.swift */,
-                               B539516B25CB0C9200959F72 /* Store.swift */,
-                               B539517325CB0CA400959F72 /* AppState.swift */,
+                               B5012E602C96254700AC4D68 /* MapRender */,
                        );
                        );
-                       path = Data;
+                       path = "Complex Components";
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D2FF2A06DB40000EEA24 /* Presentation */ = {
+               B5012E6A2C96255A00AC4D68 /* MapRender */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B5F8D3022A06DBC3000EEA24 /* Windows */,
-                               B5F8D3032A06DC2D000EEA24 /* Screens */,
-                               B5F8D3052A06DCF3000EEA24 /* Complex Components */,
-                               B5F8D3042A06DCC4000EEA24 /* Base Components */,
+                               B5012E802C97318300AC4D68 /* MapGroups.swift */,
+                               B5012E862C97874400AC4D68 /* MapGroup.swift */,
+                               B5012E632C96255A00AC4D68 /* MapAxes.swift */,
+                               B5012E642C96255A00AC4D68 /* MapStages.swift */,
+                               B5012E652C96255A00AC4D68 /* MapVertices.swift */,
+                               B5012E662C96255A00AC4D68 /* MapNotes.swift */,
+                               B5012E672C96255A00AC4D68 /* MapEdges.swift */,
+                               B5012E682C96255A00AC4D68 /* MapBlockers.swift */,
+                               B5012E692C96255A00AC4D68 /* MapOpportunities.swift */,
                        );
                        );
-                       path = Presentation;
+                       path = MapRender;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D3012A06DB75000EEA24 /* Models */ = {
+               B5012E882C98234F00AC4D68 /* Commands */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B52625BA25C884C2003E73B7 /* Map+parse.swift */,
+                               B5012E892C98235300AC4D68 /* MapCommands.swift */,
                        );
                        );
-                       path = Models;
+                       path = Commands;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D3022A06DBC3000EEA24 /* Windows */ = {
+               B54587032C961E9C0067B788 = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B526257325C874F9003E73B7 /* MapEditorWindow.swift */,
+                               B545870E2C961E9C0067B788 /* Map */,
+                               B54587232C961E9E0067B788 /* MapTests */,
+                               B545872D2C961E9E0067B788 /* MapUITests */,
+                               B545870D2C961E9C0067B788 /* Products */,
                        );
                        );
-                       path = Windows;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D3032A06DC2D000EEA24 /* Screens */ = {
+               B545870D2C961E9C0067B788 /* Products */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B523C74A25C9C1BA00C44061 /* EmptyMapDetailScreen.swift */,
-                               B52625A525C876C3003E73B7 /* MapDetailScreen.swift */,
+                               B545870C2C961E9C0067B788 /* Map.app */,
+                               B54587202C961E9E0067B788 /* MapTests.xctest */,
+                               B545872A2C961E9E0067B788 /* Map2UITests.xctest */,
                        );
                        );
-                       path = Screens;
+                       name = Products;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D3042A06DCC4000EEA24 /* Base Components */ = {
+               B545870E2C961E9C0067B788 /* Map */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B5CF75C825CC19FC003BFF3D /* EvolutionPicker.swift */,
-                               B539518025CB2D7A00959F72 /* MapTextEditor.swift */,
-                               B523C75825C9FD3A00C44061 /* MapRender */,
+                               B5012E562C96249400AC4D68 /* Logic */,
+                               B5012E3C2C96222E00AC4D68 /* Data */,
+                               B5012E3D2C96223800AC4D68 /* Presentation */,
+                               B545870F2C961E9C0067B788 /* MapApp.swift */,
+                               B54587152C961E9E0067B788 /* Assets.xcassets */,
+                               B545871A2C961E9E0067B788 /* Info.plist */,
+                               B545871B2C961E9E0067B788 /* Map.entitlements */,
+                               B54587172C961E9E0067B788 /* Preview Content */,
                        );
                        );
-                       path = "Base Components";
+                       path = Map;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D3052A06DCF3000EEA24 /* Complex Components */ = {
+               B54587172C961E9E0067B788 /* Preview Content */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B5F8D3062A06DD2A000EEA24 /* MapRender */,
+                               B54587182C961E9E0067B788 /* Preview Assets.xcassets */,
                        );
                        );
-                       path = "Complex Components";
+                       path = "Preview Content";
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D3062A06DD2A000EEA24 /* MapRender */ = {
+               B54587232C961E9E0067B788 /* MapTests */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B52625AF25C87C14003E73B7 /* MapRenderView.swift */,
+                               B54587242C961E9E0067B788 /* MapTests.swift */,
                        );
                        );
-                       path = MapRender;
+                       path = MapTests;
                        sourceTree = "<group>";
                };
                        sourceTree = "<group>";
                };
-               B5F8D3092A06DE1A000EEA24 /* Logic */ = {
+               B545872D2C961E9E0067B788 /* MapUITests */ = {
                        isa = PBXGroup;
                        children = (
                        isa = PBXGroup;
                        children = (
-                               B5CF75CD25CC7953003BFF3D /* MapParser */,
-                               B5CF75F625CC97CA003BFF3D /* Debouncer.swift */,
+                               B545872E2C961E9E0067B788 /* MapUITests.swift */,
+                               B54587302C961E9E0067B788 /* MapUITestsLaunchTests.swift */,
                        );
                        );
-                       path = Logic;
+                       path = MapUITests;
                        sourceTree = "<group>";
                };
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
                        sourceTree = "<group>";
                };
 /* End PBXGroup section */
 
 /* Begin PBXNativeTarget section */
-               B526256D25C874F9003E73B7 /* Map */ = {
+               B545870B2C961E9C0067B788 /* Map */ = {
                        isa = PBXNativeTarget;
                        isa = PBXNativeTarget;
-                       buildConfigurationList = B526259925C874FA003E73B7 /* Build configuration list for PBXNativeTarget "Map" */;
+                       buildConfigurationList = B54587342C961E9E0067B788 /* Build configuration list for PBXNativeTarget "Map" */;
                        buildPhases = (
                        buildPhases = (
-                               B526256A25C874F9003E73B7 /* Sources */,
-                               B526256B25C874F9003E73B7 /* Frameworks */,
-                               B526256C25C874F9003E73B7 /* Resources */,
+                               B54587082C961E9C0067B788 /* Sources */,
+                               B54587092C961E9C0067B788 /* Frameworks */,
+                               B545870A2C961E9C0067B788 /* Resources */,
                        );
                        buildRules = (
                        );
                        );
                        buildRules = (
                        );
                        );
                        name = Map;
                        packageProductDependencies = (
                        );
                        name = Map;
                        packageProductDependencies = (
-                               B5F8D30D2A06E5C2000EEA24 /* Patterns */,
+                               B5012E732C9625E200AC4D68 /* Patterns */,
+                               B5012E7E2C97315800AC4D68 /* ConcaveHull */,
                        );
                        );
-                       productName = Map;
-                       productReference = B526256E25C874F9003E73B7 /* Map.app */;
+                       productName = Map2;
+                       productReference = B545870C2C961E9C0067B788 /* Map.app */;
                        productType = "com.apple.product-type.application";
                };
                        productType = "com.apple.product-type.application";
                };
-               B526258425C874FA003E73B7 /* MapTests */ = {
+               B545871F2C961E9E0067B788 /* MapTests */ = {
                        isa = PBXNativeTarget;
                        isa = PBXNativeTarget;
-                       buildConfigurationList = B526259C25C874FA003E73B7 /* Build configuration list for PBXNativeTarget "MapTests" */;
+                       buildConfigurationList = B54587372C961E9E0067B788 /* Build configuration list for PBXNativeTarget "MapTests" */;
                        buildPhases = (
                        buildPhases = (
-                               B526258125C874FA003E73B7 /* Sources */,
-                               B526258225C874FA003E73B7 /* Frameworks */,
-                               B526258325C874FA003E73B7 /* Resources */,
+                               B545871C2C961E9E0067B788 /* Sources */,
+                               B545871D2C961E9E0067B788 /* Frameworks */,
+                               B545871E2C961E9E0067B788 /* Resources */,
                        );
                        buildRules = (
                        );
                        dependencies = (
                        );
                        buildRules = (
                        );
                        dependencies = (
-                               B526258725C874FA003E73B7 /* PBXTargetDependency */,
+                               B54587222C961E9E0067B788 /* PBXTargetDependency */,
                        );
                        name = MapTests;
                        );
                        name = MapTests;
-                       productName = MapTests;
-                       productReference = B526258525C874FA003E73B7 /* MapTests.xctest */;
+                       productName = Map2Tests;
+                       productReference = B54587202C961E9E0067B788 /* MapTests.xctest */;
                        productType = "com.apple.product-type.bundle.unit-test";
                };
                        productType = "com.apple.product-type.bundle.unit-test";
                };
-               B526258F25C874FA003E73B7 /* MapUITests */ = {
+               B54587292C961E9E0067B788 /* Map2UITests */ = {
                        isa = PBXNativeTarget;
                        isa = PBXNativeTarget;
-                       buildConfigurationList = B526259F25C874FA003E73B7 /* Build configuration list for PBXNativeTarget "MapUITests" */;
+                       buildConfigurationList = B545873A2C961E9E0067B788 /* Build configuration list for PBXNativeTarget "Map2UITests" */;
                        buildPhases = (
                        buildPhases = (
-                               B526258C25C874FA003E73B7 /* Sources */,
-                               B526258D25C874FA003E73B7 /* Frameworks */,
-                               B526258E25C874FA003E73B7 /* Resources */,
+                               B54587262C961E9E0067B788 /* Sources */,
+                               B54587272C961E9E0067B788 /* Frameworks */,
+                               B54587282C961E9E0067B788 /* Resources */,
                        );
                        buildRules = (
                        );
                        dependencies = (
                        );
                        buildRules = (
                        );
                        dependencies = (
-                               B526259225C874FA003E73B7 /* PBXTargetDependency */,
+                               B545872C2C961E9E0067B788 /* PBXTargetDependency */,
                        );
                        );
-                       name = MapUITests;
-                       productName = MapUITests;
-                       productReference = B526259025C874FA003E73B7 /* MapUITests.xctest */;
+                       name = Map2UITests;
+                       productName = Map2UITests;
+                       productReference = B545872A2C961E9E0067B788 /* Map2UITests.xctest */;
                        productType = "com.apple.product-type.bundle.ui-testing";
                };
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
                        productType = "com.apple.product-type.bundle.ui-testing";
                };
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
-               B526256625C874F9003E73B7 /* Project object */ = {
+               B54587042C961E9C0067B788 /* Project object */ = {
                        isa = PBXProject;
                        attributes = {
                        isa = PBXProject;
                        attributes = {
-                               BuildIndependentTargetsInParallel = YES;
-                               LastSwiftUpdateCheck = 1240;
-                               LastUpgradeCheck = 1430;
+                               BuildIndependentTargetsInParallel = 1;
+                               LastSwiftUpdateCheck = 1600;
+                               LastUpgradeCheck = 1600;
                                TargetAttributes = {
                                TargetAttributes = {
-                                       B526256D25C874F9003E73B7 = {
-                                               CreatedOnToolsVersion = 12.4;
+                                       B545870B2C961E9C0067B788 = {
+                                               CreatedOnToolsVersion = 16.0;
                                        };
                                        };
-                                       B526258425C874FA003E73B7 = {
-                                               CreatedOnToolsVersion = 12.4;
-                                               TestTargetID = B526256D25C874F9003E73B7;
+                                       B545871F2C961E9E0067B788 = {
+                                               CreatedOnToolsVersion = 16.0;
+                                               TestTargetID = B545870B2C961E9C0067B788;
                                        };
                                        };
-                                       B526258F25C874FA003E73B7 = {
-                                               CreatedOnToolsVersion = 12.4;
-                                               TestTargetID = B526256D25C874F9003E73B7;
+                                       B54587292C961E9E0067B788 = {
+                                               CreatedOnToolsVersion = 16.0;
+                                               TestTargetID = B545870B2C961E9C0067B788;
                                        };
                                };
                        };
                                        };
                                };
                        };
-                       buildConfigurationList = B526256925C874F9003E73B7 /* Build configuration list for PBXProject "Map" */;
-                       compatibilityVersion = "Xcode 9.3";
+                       buildConfigurationList = B54587072C961E9C0067B788 /* Build configuration list for PBXProject "Map" */;
+                       compatibilityVersion = "Xcode 15.0";
                        developmentRegion = en;
                        hasScannedForEncodings = 0;
                        knownRegions = (
                                en,
                                Base,
                        );
                        developmentRegion = en;
                        hasScannedForEncodings = 0;
                        knownRegions = (
                                en,
                                Base,
                        );
-                       mainGroup = B526256525C874F9003E73B7;
+                       mainGroup = B54587032C961E9C0067B788;
                        packageReferences = (
                        packageReferences = (
-                               B5F8D30C2A06E5C2000EEA24 /* XCRemoteSwiftPackageReference "patterns" */,
+                               B5012E722C9625E200AC4D68 /* XCRemoteSwiftPackageReference "patterns" */,
+                               B5012E7D2C97315800AC4D68 /* XCRemoteSwiftPackageReference "ConcaveHull" */,
                        );
                        );
-                       productRefGroup = B526256F25C874F9003E73B7 /* Products */;
+                       productRefGroup = B545870D2C961E9C0067B788 /* Products */;
                        projectDirPath = "";
                        projectRoot = "";
                        targets = (
                        projectDirPath = "";
                        projectRoot = "";
                        targets = (
-                               B526256D25C874F9003E73B7 /* Map */,
-                               B526258425C874FA003E73B7 /* MapTests */,
-                               B526258F25C874FA003E73B7 /* MapUITests */,
+                               B545870B2C961E9C0067B788 /* Map */,
+                               B545871F2C961E9E0067B788 /* MapTests */,
+                               B54587292C961E9E0067B788 /* Map2UITests */,
                        );
                };
 /* End PBXProject section */
 
 /* Begin PBXResourcesBuildPhase section */
                        );
                };
 /* End PBXProject section */
 
 /* Begin PBXResourcesBuildPhase section */
-               B526256C25C874F9003E73B7 /* Resources */ = {
+               B545870A2C961E9C0067B788 /* Resources */ = {
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
-                               B526257925C874FA003E73B7 /* Preview Assets.xcassets in Resources */,
-                               B526257625C874FA003E73B7 /* Assets.xcassets in Resources */,
+                               B54587192C961E9E0067B788 /* Preview Assets.xcassets in Resources */,
+                               B54587162C961E9E0067B788 /* Assets.xcassets in Resources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
-               B526258325C874FA003E73B7 /* Resources */ = {
+               B545871E2C961E9E0067B788 /* Resources */ = {
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
-               B526258E25C874FA003E73B7 /* Resources */ = {
+               B54587282C961E9E0067B788 /* Resources */ = {
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        isa = PBXResourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
 /* End PBXResourcesBuildPhase section */
 
 /* Begin PBXSourcesBuildPhase section */
-               B526256A25C874F9003E73B7 /* Sources */ = {
+               B54587082C961E9C0067B788 /* Sources */ = {
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
-                               B52625B025C87C14003E73B7 /* MapRenderView.swift in Sources */,
-                               B523C77E25CA294C00C44061 /* MapOpportunities.swift in Sources */,
-                               B52625AB25C87909003E73B7 /* Date+format.swift in Sources */,
-                               B5CF75D825CC79BC003BFF3D /* VertexParserStrategy.swift in Sources */,
-                               B5CF75EA25CC7A13003BFF3D /* OpportunityParserStrategy.swift in Sources */,
-                               B5F8D3142A07C05F000EEA24 /* MapNotes.swift in Sources */,
-                               B523C77125CA121300C44061 /* MapBlockers.swift in Sources */,
-                               B523C76725CA071B00C44061 /* MapVertices.swift in Sources */,
-                               B5CF75CF25CC7965003BFF3D /* MapParser.swift in Sources */,
-                               B5CF75E225CC79ED003BFF3D /* BlockerParserStrategy.swift in Sources */,
-                               B5F8D3102A07B33D000EEA24 /* NSColor+Theme.swift in Sources */,
-                               B5F8D30B2A06E3E6000EEA24 /* Color+Theme.swift in Sources */,
-                               B539517425CB0CA400959F72 /* AppState.swift in Sources */,
-                               B523C75A25C9FD4900C44061 /* MapAxes.swift in Sources */,
-                               B539518125CB2D7A00959F72 /* MapTextEditor.swift in Sources */,
-                               B52625BB25C884C2003E73B7 /* Map+parse.swift in Sources */,
-                               B52625C625C8BD2A003E73B7 /* Stage.swift in Sources */,
-                               B523C73D25C98D9800C44061 /* NSImage+writePNG.swift in Sources */,
-                               B5F8D3122A07B690000EEA24 /* NoteParserStrategy.swift in Sources */,
-                               B526257B25C874FA003E73B7 /* Persistence.swift in Sources */,
-                               B5F8D3082A06DD8C000EEA24 /* Font+Theme.swift in Sources */,
-                               B5CF75EF25CC7A4A003BFF3D /* StageParserStrategy.swift in Sources */,
-                               B5CF75DD25CC79D7003BFF3D /* EdgeParserStrategy.swift in Sources */,
-                               B526257425C874F9003E73B7 /* MapEditorWindow.swift in Sources */,
-                               B526257E25C874FA003E73B7 /* Map.xcdatamodeld in Sources */,
-                               B5CF75C925CC19FC003BFF3D /* EvolutionPicker.swift in Sources */,
-                               B523C74B25C9C1BA00C44061 /* EmptyMapDetailScreen.swift in Sources */,
-                               B539516C25CB0C9300959F72 /* Store.swift in Sources */,
-                               B526257225C874F9003E73B7 /* MapApp.swift in Sources */,
-                               B52625A625C876C3003E73B7 /* MapDetailScreen.swift in Sources */,
-                               B523C76225CA05A300C44061 /* MapStages.swift in Sources */,
-                               B5CF75F725CC97CA003BFF3D /* Debouncer.swift in Sources */,
-                               B523C76C25CA0DFA00C44061 /* MapEdges.swift in Sources */,
+                               B5012E872C97874600AC4D68 /* MapGroup.swift in Sources */,
+                               B5012E472C96243C00AC4D68 /* MapTextEditor.swift in Sources */,
+                               B5012E622C96254700AC4D68 /* MapRenderView.swift in Sources */,
+                               B5012E492C96245B00AC4D68 /* Color+theme.swift in Sources */,
+                               B54587122C961E9C0067B788 /* MapDocument.swift in Sources */,
+                               B54587102C961E9C0067B788 /* MapApp.swift in Sources */,
+                               B5012E8C2C98244000AC4D68 /* ViewStyle.swift in Sources */,
+                               B5012E8E2C9828D000AC4D68 /* Constants.swift in Sources */,
+                               B5012E7C2C972B6C00AC4D68 /* GroupParserStrategy.swift in Sources */,
+                               B5012E6B2C96255A00AC4D68 /* MapAxes.swift in Sources */,
+                               B5012E6C2C96255A00AC4D68 /* MapVertices.swift in Sources */,
+                               B5012E6D2C96255A00AC4D68 /* MapStages.swift in Sources */,
+                               B5012E7A2C96F02F00AC4D68 /* Dimensions.swift in Sources */,
+                               B5012E6E2C96255A00AC4D68 /* MapNotes.swift in Sources */,
+                               B5012E6F2C96255A00AC4D68 /* MapEdges.swift in Sources */,
+                               B5012E702C96255A00AC4D68 /* MapOpportunities.swift in Sources */,
+                               B5012E712C96255A00AC4D68 /* MapBlockers.swift in Sources */,
+                               B5012E4B2C96246F00AC4D68 /* NSColor+theme.swift in Sources */,
+                               B54587142C961E9C0067B788 /* MapEditor.swift in Sources */,
+                               B5012E3F2C96232A00AC4D68 /* EvolutionPicker.swift in Sources */,
+                               B5012E572C96249400AC4D68 /* NoteParserStrategy.swift in Sources */,
+                               B5012E582C96249400AC4D68 /* BlockerParserStrategy.swift in Sources */,
+                               B5012E592C96249400AC4D68 /* VertexParserStrategy.swift in Sources */,
+                               B5012E5A2C96249400AC4D68 /* MapParser.swift in Sources */,
+                               B5012E5B2C96249400AC4D68 /* OpportunityParserStrategy.swift in Sources */,
+                               B5012E5C2C96249400AC4D68 /* EdgeParserStrategy.swift in Sources */,
+                               B5012E5D2C96249400AC4D68 /* StageParserStrategy.swift in Sources */,
+                               B5012E812C97318600AC4D68 /* MapGroups.swift in Sources */,
+                               B5012E8A2C98235500AC4D68 /* MapCommands.swift in Sources */,
+                               B5012E5E2C96249400AC4D68 /* Debouncer.swift in Sources */,
+                               B5012E452C9623C700AC4D68 /* Font+theme.swift in Sources */,
+                               B5012E422C96235E00AC4D68 /* Stage.swift in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
-               B526258125C874FA003E73B7 /* Sources */ = {
+               B545871C2C961E9E0067B788 /* Sources */ = {
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
-                               B526258A25C874FA003E73B7 /* MapTests.swift in Sources */,
+                               B54587252C961E9E0067B788 /* MapTests.swift in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
-               B526258C25C874FA003E73B7 /* Sources */ = {
+               B54587262C961E9E0067B788 /* Sources */ = {
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
                        isa = PBXSourcesBuildPhase;
                        buildActionMask = 2147483647;
                        files = (
-                               B526259525C874FA003E73B7 /* MapUITests.swift in Sources */,
+                               B54587312C961E9E0067B788 /* MapUITestsLaunchTests.swift in Sources */,
+                               B545872F2C961E9E0067B788 /* MapUITests.swift in Sources */,
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
                        );
                        runOnlyForDeploymentPostprocessing = 0;
                };
 /* End PBXSourcesBuildPhase section */
 
 /* Begin PBXTargetDependency section */
-               B526258725C874FA003E73B7 /* PBXTargetDependency */ = {
+               B54587222C961E9E0067B788 /* PBXTargetDependency */ = {
                        isa = PBXTargetDependency;
                        isa = PBXTargetDependency;
-                       target = B526256D25C874F9003E73B7 /* Map */;
-                       targetProxy = B526258625C874FA003E73B7 /* PBXContainerItemProxy */;
+                       target = B545870B2C961E9C0067B788 /* Map */;
+                       targetProxy = B54587212C961E9E0067B788 /* PBXContainerItemProxy */;
                };
                };
-               B526259225C874FA003E73B7 /* PBXTargetDependency */ = {
+               B545872C2C961E9E0067B788 /* PBXTargetDependency */ = {
                        isa = PBXTargetDependency;
                        isa = PBXTargetDependency;
-                       target = B526256D25C874F9003E73B7 /* Map */;
-                       targetProxy = B526259125C874FA003E73B7 /* PBXContainerItemProxy */;
+                       target = B545870B2C961E9C0067B788 /* Map */;
+                       targetProxy = B545872B2C961E9E0067B788 /* PBXContainerItemProxy */;
                };
 /* End PBXTargetDependency section */
 
 /* Begin XCBuildConfiguration section */
                };
 /* End PBXTargetDependency section */
 
 /* Begin XCBuildConfiguration section */
-               B526259725C874FA003E73B7 /* Debug */ = {
+               B54587322C961E9E0067B788 /* Debug */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                                ALWAYS_SEARCH_USER_PATHS = NO;
                        isa = XCBuildConfiguration;
                        buildSettings = {
                                ALWAYS_SEARCH_USER_PATHS = NO;
+                               ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
                                CLANG_ANALYZER_NONNULL = YES;
                                CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
                                CLANG_ANALYZER_NONNULL = YES;
                                CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-                               CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
-                               CLANG_CXX_LIBRARY = "libc++";
+                               CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
                                CLANG_ENABLE_MODULES = YES;
                                CLANG_ENABLE_OBJC_ARC = YES;
                                CLANG_ENABLE_OBJC_WEAK = YES;
                                CLANG_ENABLE_MODULES = YES;
                                CLANG_ENABLE_OBJC_ARC = YES;
                                CLANG_ENABLE_OBJC_WEAK = YES;
                                CLANG_WARN_UNREACHABLE_CODE = YES;
                                CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
                                COPY_PHASE_STRIP = NO;
                                CLANG_WARN_UNREACHABLE_CODE = YES;
                                CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
                                COPY_PHASE_STRIP = NO;
-                               DEAD_CODE_STRIPPING = YES;
                                DEBUG_INFORMATION_FORMAT = dwarf;
                                ENABLE_STRICT_OBJC_MSGSEND = YES;
                                ENABLE_TESTABILITY = YES;
                                DEBUG_INFORMATION_FORMAT = dwarf;
                                ENABLE_STRICT_OBJC_MSGSEND = YES;
                                ENABLE_TESTABILITY = YES;
-                               GCC_C_LANGUAGE_STANDARD = gnu11;
+                               ENABLE_USER_SCRIPT_SANDBOXING = YES;
+                               GCC_C_LANGUAGE_STANDARD = gnu17;
                                GCC_DYNAMIC_NO_PIC = NO;
                                GCC_NO_COMMON_BLOCKS = YES;
                                GCC_OPTIMIZATION_LEVEL = 0;
                                GCC_DYNAMIC_NO_PIC = NO;
                                GCC_NO_COMMON_BLOCKS = YES;
                                GCC_OPTIMIZATION_LEVEL = 0;
                                GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
                                GCC_WARN_UNUSED_FUNCTION = YES;
                                GCC_WARN_UNUSED_VARIABLE = YES;
                                GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
                                GCC_WARN_UNUSED_FUNCTION = YES;
                                GCC_WARN_UNUSED_VARIABLE = YES;
-                               MACOSX_DEPLOYMENT_TARGET = 11.1;
+                               LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+                               MACOSX_DEPLOYMENT_TARGET = 15.0;
                                MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
                                MTL_FAST_MATH = YES;
                                ONLY_ACTIVE_ARCH = YES;
                                SDKROOT = macosx;
                                MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
                                MTL_FAST_MATH = YES;
                                ONLY_ACTIVE_ARCH = YES;
                                SDKROOT = macosx;
-                               SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+                               SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
                                SWIFT_OPTIMIZATION_LEVEL = "-Onone";
                        };
                        name = Debug;
                };
                                SWIFT_OPTIMIZATION_LEVEL = "-Onone";
                        };
                        name = Debug;
                };
-               B526259825C874FA003E73B7 /* Release */ = {
+               B54587332C961E9E0067B788 /* Release */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                                ALWAYS_SEARCH_USER_PATHS = NO;
                        isa = XCBuildConfiguration;
                        buildSettings = {
                                ALWAYS_SEARCH_USER_PATHS = NO;
+                               ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
                                CLANG_ANALYZER_NONNULL = YES;
                                CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
                                CLANG_ANALYZER_NONNULL = YES;
                                CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-                               CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
-                               CLANG_CXX_LIBRARY = "libc++";
+                               CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
                                CLANG_ENABLE_MODULES = YES;
                                CLANG_ENABLE_OBJC_ARC = YES;
                                CLANG_ENABLE_OBJC_WEAK = YES;
                                CLANG_ENABLE_MODULES = YES;
                                CLANG_ENABLE_OBJC_ARC = YES;
                                CLANG_ENABLE_OBJC_WEAK = YES;
                                CLANG_WARN_UNREACHABLE_CODE = YES;
                                CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
                                COPY_PHASE_STRIP = NO;
                                CLANG_WARN_UNREACHABLE_CODE = YES;
                                CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
                                COPY_PHASE_STRIP = NO;
-                               DEAD_CODE_STRIPPING = YES;
                                DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
                                ENABLE_NS_ASSERTIONS = NO;
                                ENABLE_STRICT_OBJC_MSGSEND = YES;
                                DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
                                ENABLE_NS_ASSERTIONS = NO;
                                ENABLE_STRICT_OBJC_MSGSEND = YES;
-                               GCC_C_LANGUAGE_STANDARD = gnu11;
+                               ENABLE_USER_SCRIPT_SANDBOXING = YES;
+                               GCC_C_LANGUAGE_STANDARD = gnu17;
                                GCC_NO_COMMON_BLOCKS = YES;
                                GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
                                GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
                                GCC_NO_COMMON_BLOCKS = YES;
                                GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
                                GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
                                GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
                                GCC_WARN_UNUSED_FUNCTION = YES;
                                GCC_WARN_UNUSED_VARIABLE = YES;
                                GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
                                GCC_WARN_UNUSED_FUNCTION = YES;
                                GCC_WARN_UNUSED_VARIABLE = YES;
-                               MACOSX_DEPLOYMENT_TARGET = 11.1;
+                               LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+                               MACOSX_DEPLOYMENT_TARGET = 15.0;
                                MTL_ENABLE_DEBUG_INFO = NO;
                                MTL_FAST_MATH = YES;
                                SDKROOT = macosx;
                                SWIFT_COMPILATION_MODE = wholemodule;
                                MTL_ENABLE_DEBUG_INFO = NO;
                                MTL_FAST_MATH = YES;
                                SDKROOT = macosx;
                                SWIFT_COMPILATION_MODE = wholemodule;
-                               SWIFT_OPTIMIZATION_LEVEL = "-O";
                        };
                        name = Release;
                };
                        };
                        name = Release;
                };
-               B526259A25C874FA003E73B7 /* Debug */ = {
+               B54587352C961E9E0067B788 /* Debug */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                                ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
                        isa = XCBuildConfiguration;
                        buildSettings = {
                                ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
                                CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements;
                                CODE_SIGN_STYLE = Automatic;
                                COMBINE_HIDPI_IMAGES = YES;
                                CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements;
                                CODE_SIGN_STYLE = Automatic;
                                COMBINE_HIDPI_IMAGES = YES;
-                               CURRENT_PROJECT_VERSION = 4;
-                               DEAD_CODE_STRIPPING = YES;
+                               CURRENT_PROJECT_VERSION = 3;
                                DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                ENABLE_HARDENED_RUNTIME = YES;
                                ENABLE_PREVIEWS = YES;
                                DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                ENABLE_HARDENED_RUNTIME = YES;
                                ENABLE_PREVIEWS = YES;
+                               GENERATE_INFOPLIST_FILE = YES;
                                INFOPLIST_FILE = Map/Info.plist;
                                INFOPLIST_FILE = Map/Info.plist;
+                               INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
+                               INFOPLIST_KEY_NSHumanReadableCopyright = "";
                                LD_RUNPATH_SEARCH_PATHS = (
                                        "$(inherited)",
                                        "@executable_path/../Frameworks",
                                );
                                LD_RUNPATH_SEARCH_PATHS = (
                                        "$(inherited)",
                                        "@executable_path/../Frameworks",
                                );
-                               MACOSX_DEPLOYMENT_TARGET = 12.0;
-                               MARKETING_VERSION = 2.0.0;
-                               PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map;
+                               MACOSX_DEPLOYMENT_TARGET = 14.0;
+                               MARKETING_VERSION = 3.0.0;
+                               PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                PRODUCT_NAME = "$(TARGET_NAME)";
+                               SWIFT_EMIT_LOC_STRINGS = YES;
                                SWIFT_VERSION = 5.0;
                        };
                        name = Debug;
                };
                                SWIFT_VERSION = 5.0;
                        };
                        name = Debug;
                };
-               B526259B25C874FA003E73B7 /* Release */ = {
+               B54587362C961E9E0067B788 /* Release */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                                ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
                        isa = XCBuildConfiguration;
                        buildSettings = {
                                ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
                                CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements;
                                CODE_SIGN_STYLE = Automatic;
                                COMBINE_HIDPI_IMAGES = YES;
                                CODE_SIGN_ENTITLEMENTS = Map/Map.entitlements;
                                CODE_SIGN_STYLE = Automatic;
                                COMBINE_HIDPI_IMAGES = YES;
-                               CURRENT_PROJECT_VERSION = 4;
-                               DEAD_CODE_STRIPPING = YES;
+                               CURRENT_PROJECT_VERSION = 3;
                                DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                ENABLE_HARDENED_RUNTIME = YES;
                                ENABLE_PREVIEWS = YES;
                                DEVELOPMENT_ASSET_PATHS = "\"Map/Preview Content\"";
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                ENABLE_HARDENED_RUNTIME = YES;
                                ENABLE_PREVIEWS = YES;
+                               GENERATE_INFOPLIST_FILE = YES;
                                INFOPLIST_FILE = Map/Info.plist;
                                INFOPLIST_FILE = Map/Info.plist;
+                               INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
+                               INFOPLIST_KEY_NSHumanReadableCopyright = "";
                                LD_RUNPATH_SEARCH_PATHS = (
                                        "$(inherited)",
                                        "@executable_path/../Frameworks",
                                );
                                LD_RUNPATH_SEARCH_PATHS = (
                                        "$(inherited)",
                                        "@executable_path/../Frameworks",
                                );
-                               MACOSX_DEPLOYMENT_TARGET = 12.0;
-                               MARKETING_VERSION = 2.0.0;
-                               PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.Map;
+                               MACOSX_DEPLOYMENT_TARGET = 14.0;
+                               MARKETING_VERSION = 3.0.0;
+                               PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                PRODUCT_NAME = "$(TARGET_NAME)";
+                               SWIFT_EMIT_LOC_STRINGS = YES;
                                SWIFT_VERSION = 5.0;
                        };
                        name = Release;
                };
                                SWIFT_VERSION = 5.0;
                        };
                        name = Release;
                };
-               B526259D25C874FA003E73B7 /* Debug */ = {
+               B54587382C961E9E0067B788 /* Debug */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
-                               ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
                                BUNDLE_LOADER = "$(TEST_HOST)";
                                CODE_SIGN_STYLE = Automatic;
                                BUNDLE_LOADER = "$(TEST_HOST)";
                                CODE_SIGN_STYLE = Automatic;
-                               COMBINE_HIDPI_IMAGES = YES;
-                               DEAD_CODE_STRIPPING = YES;
+                               CURRENT_PROJECT_VERSION = 1;
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                DEVELOPMENT_TEAM = S68NHQVJXW;
-                               INFOPLIST_FILE = MapTests/Info.plist;
-                               LD_RUNPATH_SEARCH_PATHS = (
-                                       "$(inherited)",
-                                       "@executable_path/../Frameworks",
-                                       "@loader_path/../Frameworks",
-                               );
-                               MACOSX_DEPLOYMENT_TARGET = 11.0;
-                               PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.MapTests;
+                               GENERATE_INFOPLIST_FILE = YES;
+                               MACOSX_DEPLOYMENT_TARGET = 15.0;
+                               MARKETING_VERSION = 1.0;
+                               PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map2Tests;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                PRODUCT_NAME = "$(TARGET_NAME)";
+                               SWIFT_EMIT_LOC_STRINGS = NO;
                                SWIFT_VERSION = 5.0;
                                SWIFT_VERSION = 5.0;
-                               TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Map.app/Contents/MacOS/Map";
+                               TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Map2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Map2";
                        };
                        name = Debug;
                };
                        };
                        name = Debug;
                };
-               B526259E25C874FA003E73B7 /* Release */ = {
+               B54587392C961E9E0067B788 /* Release */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
-                               ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
                                BUNDLE_LOADER = "$(TEST_HOST)";
                                CODE_SIGN_STYLE = Automatic;
                                BUNDLE_LOADER = "$(TEST_HOST)";
                                CODE_SIGN_STYLE = Automatic;
-                               COMBINE_HIDPI_IMAGES = YES;
-                               DEAD_CODE_STRIPPING = YES;
+                               CURRENT_PROJECT_VERSION = 1;
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                DEVELOPMENT_TEAM = S68NHQVJXW;
-                               INFOPLIST_FILE = MapTests/Info.plist;
-                               LD_RUNPATH_SEARCH_PATHS = (
-                                       "$(inherited)",
-                                       "@executable_path/../Frameworks",
-                                       "@loader_path/../Frameworks",
-                               );
-                               MACOSX_DEPLOYMENT_TARGET = 11.0;
-                               PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.MapTests;
+                               GENERATE_INFOPLIST_FILE = YES;
+                               MACOSX_DEPLOYMENT_TARGET = 15.0;
+                               MARKETING_VERSION = 1.0;
+                               PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map2Tests;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                PRODUCT_NAME = "$(TARGET_NAME)";
+                               SWIFT_EMIT_LOC_STRINGS = NO;
                                SWIFT_VERSION = 5.0;
                                SWIFT_VERSION = 5.0;
-                               TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Map.app/Contents/MacOS/Map";
+                               TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Map2.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Map2";
                        };
                        name = Release;
                };
                        };
                        name = Release;
                };
-               B52625A025C874FA003E73B7 /* Debug */ = {
+               B545873B2C961E9E0067B788 /* Debug */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
-                               ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
                                CODE_SIGN_STYLE = Automatic;
                                CODE_SIGN_STYLE = Automatic;
-                               COMBINE_HIDPI_IMAGES = YES;
-                               DEAD_CODE_STRIPPING = YES;
+                               CURRENT_PROJECT_VERSION = 1;
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                DEVELOPMENT_TEAM = S68NHQVJXW;
-                               INFOPLIST_FILE = MapUITests/Info.plist;
-                               LD_RUNPATH_SEARCH_PATHS = (
-                                       "$(inherited)",
-                                       "@executable_path/../Frameworks",
-                                       "@loader_path/../Frameworks",
-                               );
-                               PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.MapUITests;
+                               GENERATE_INFOPLIST_FILE = YES;
+                               MARKETING_VERSION = 1.0;
+                               PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map2UITests;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                PRODUCT_NAME = "$(TARGET_NAME)";
+                               SWIFT_EMIT_LOC_STRINGS = NO;
                                SWIFT_VERSION = 5.0;
                                SWIFT_VERSION = 5.0;
-                               TEST_TARGET_NAME = Map;
+                               TEST_TARGET_NAME = Map2;
                        };
                        name = Debug;
                };
                        };
                        name = Debug;
                };
-               B52625A125C874FA003E73B7 /* Release */ = {
+               B545873C2C961E9E0067B788 /* Release */ = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
                        isa = XCBuildConfiguration;
                        buildSettings = {
-                               ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
                                CODE_SIGN_STYLE = Automatic;
                                CODE_SIGN_STYLE = Automatic;
-                               COMBINE_HIDPI_IMAGES = YES;
-                               DEAD_CODE_STRIPPING = YES;
+                               CURRENT_PROJECT_VERSION = 1;
                                DEVELOPMENT_TEAM = S68NHQVJXW;
                                DEVELOPMENT_TEAM = S68NHQVJXW;
-                               INFOPLIST_FILE = MapUITests/Info.plist;
-                               LD_RUNPATH_SEARCH_PATHS = (
-                                       "$(inherited)",
-                                       "@executable_path/../Frameworks",
-                                       "@loader_path/../Frameworks",
-                               );
-                               PRODUCT_BUNDLE_IDENTIFIER = pizza.unlimited.MapUITests;
+                               GENERATE_INFOPLIST_FILE = YES;
+                               MARKETING_VERSION = 1.0;
+                               PRODUCT_BUNDLE_IDENTIFIER = systems.tranquil.Map2UITests;
                                PRODUCT_NAME = "$(TARGET_NAME)";
                                PRODUCT_NAME = "$(TARGET_NAME)";
+                               SWIFT_EMIT_LOC_STRINGS = NO;
                                SWIFT_VERSION = 5.0;
                                SWIFT_VERSION = 5.0;
-                               TEST_TARGET_NAME = Map;
+                               TEST_TARGET_NAME = Map2;
                        };
                        name = Release;
                };
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
                        };
                        name = Release;
                };
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
-               B526256925C874F9003E73B7 /* Build configuration list for PBXProject "Map" */ = {
+               B54587072C961E9C0067B788 /* Build configuration list for PBXProject "Map" */ = {
                        isa = XCConfigurationList;
                        buildConfigurations = (
                        isa = XCConfigurationList;
                        buildConfigurations = (
-                               B526259725C874FA003E73B7 /* Debug */,
-                               B526259825C874FA003E73B7 /* Release */,
+                               B54587322C961E9E0067B788 /* Debug */,
+                               B54587332C961E9E0067B788 /* Release */,
                        );
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
                        );
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
-               B526259925C874FA003E73B7 /* Build configuration list for PBXNativeTarget "Map" */ = {
+               B54587342C961E9E0067B788 /* Build configuration list for PBXNativeTarget "Map" */ = {
                        isa = XCConfigurationList;
                        buildConfigurations = (
                        isa = XCConfigurationList;
                        buildConfigurations = (
-                               B526259A25C874FA003E73B7 /* Debug */,
-                               B526259B25C874FA003E73B7 /* Release */,
+                               B54587352C961E9E0067B788 /* Debug */,
+                               B54587362C961E9E0067B788 /* Release */,
                        );
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
                        );
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
-               B526259C25C874FA003E73B7 /* Build configuration list for PBXNativeTarget "MapTests" */ = {
+               B54587372C961E9E0067B788 /* Build configuration list for PBXNativeTarget "MapTests" */ = {
                        isa = XCConfigurationList;
                        buildConfigurations = (
                        isa = XCConfigurationList;
                        buildConfigurations = (
-                               B526259D25C874FA003E73B7 /* Debug */,
-                               B526259E25C874FA003E73B7 /* Release */,
+                               B54587382C961E9E0067B788 /* Debug */,
+                               B54587392C961E9E0067B788 /* Release */,
                        );
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
                        );
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                };
-               B526259F25C874FA003E73B7 /* Build configuration list for PBXNativeTarget "MapUITests" */ = {
+               B545873A2C961E9E0067B788 /* Build configuration list for PBXNativeTarget "Map2UITests" */ = {
                        isa = XCConfigurationList;
                        buildConfigurations = (
                        isa = XCConfigurationList;
                        buildConfigurations = (
-                               B52625A025C874FA003E73B7 /* Debug */,
-                               B52625A125C874FA003E73B7 /* Release */,
+                               B545873B2C961E9E0067B788 /* Debug */,
+                               B545873C2C961E9E0067B788 /* Release */,
                        );
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
                        );
                        defaultConfigurationIsVisible = 0;
                        defaultConfigurationName = Release;
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */
 /* End XCConfigurationList section */
 
 /* Begin XCRemoteSwiftPackageReference section */
-               B5F8D30C2A06E5C2000EEA24 /* XCRemoteSwiftPackageReference "patterns" */ = {
+               B5012E722C9625E200AC4D68 /* XCRemoteSwiftPackageReference "patterns" */ = {
                        isa = XCRemoteSwiftPackageReference;
                        repositoryURL = "https://git.sr.ht/~rbdr/patterns";
                        requirement = {
                        isa = XCRemoteSwiftPackageReference;
                        repositoryURL = "https://git.sr.ht/~rbdr/patterns";
                        requirement = {
                                minimumVersion = 2.0.0;
                        };
                };
                                minimumVersion = 2.0.0;
                        };
                };
+               B5012E7D2C97315800AC4D68 /* XCRemoteSwiftPackageReference "ConcaveHull" */ = {
+                       isa = XCRemoteSwiftPackageReference;
+                       repositoryURL = "https://github.com/Syncheo/ConcaveHull";
+                       requirement = {
+                               branch = master;
+                               kind = branch;
+                       };
+               };
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
 /* End XCRemoteSwiftPackageReference section */
 
 /* Begin XCSwiftPackageProductDependency section */
-               B5F8D30D2A06E5C2000EEA24 /* Patterns */ = {
+               B5012E732C9625E200AC4D68 /* Patterns */ = {
                        isa = XCSwiftPackageProductDependency;
                        isa = XCSwiftPackageProductDependency;
-                       package = B5F8D30C2A06E5C2000EEA24 /* XCRemoteSwiftPackageReference "patterns" */;
+                       package = B5012E722C9625E200AC4D68 /* XCRemoteSwiftPackageReference "patterns" */;
                        productName = Patterns;
                };
                        productName = Patterns;
                };
-/* End XCSwiftPackageProductDependency section */
-
-/* Begin XCVersionGroup section */
-               B526257C25C874FA003E73B7 /* Map.xcdatamodeld */ = {
-                       isa = XCVersionGroup;
-                       children = (
-                               B526257D25C874FA003E73B7 /* Map.xcdatamodel */,
-                       );
-                       currentVersion = B526257D25C874FA003E73B7 /* Map.xcdatamodel */;
-                       path = Map.xcdatamodeld;
-                       sourceTree = "<group>";
-                       versionGroupType = wrapper.xcdatamodel;
+               B5012E7E2C97315800AC4D68 /* ConcaveHull */ = {
+                       isa = XCSwiftPackageProductDependency;
+                       package = B5012E7D2C97315800AC4D68 /* XCRemoteSwiftPackageReference "ConcaveHull" */;
+                       productName = ConcaveHull;
                };
                };
-/* End XCVersionGroup section */
+/* End XCSwiftPackageProductDependency section */
        };
        };
-       rootObject = B526256625C874F9003E73B7 /* Project object */;
+       rootObject = B54587042C961E9C0067B788 /* Project object */;
 }
 }
index 9606e6c446e13cf574072c2e5df3d31d917f6b89..887d9eee0f754e9e6ec9ae4e6a046c768d0d1daf 100644 (file)
@@ -1,5 +1,15 @@
 {
 {
+  "originHash" : "02d7b701c0ab6f32b55ad490056b5cb87cdec5003f477a828ca9a07b09a2d7b3",
   "pins" : [
   "pins" : [
+    {
+      "identity" : "concavehull",
+      "kind" : "remoteSourceControl",
+      "location" : "https://github.com/Syncheo/ConcaveHull",
+      "state" : {
+        "branch" : "master",
+        "revision" : "2583fea7f4b2a34830e47b64fc076cd3fbd4038c"
+      }
+    },
     {
       "identity" : "patterns",
       "kind" : "remoteSourceControl",
     {
       "identity" : "patterns",
       "kind" : "remoteSourceControl",
@@ -10,5 +20,5 @@
       }
     }
   ],
       }
     }
   ],
-  "version" : 2
+  "version" : 3
 }
 }
diff --git a/Map/Assets.xcassets/Colors/Theme/Darker Neutral Gray.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Darker Neutral Gray.colorset/Contents.json
new file mode 100644 (file)
index 0000000..e890acd
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0.488",
+          "green" : "0.500",
+          "red" : "0.434"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Map/Assets.xcassets/Colors/Theme/Hermosa Pink.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Hermosa Pink.colorset/Contents.json
new file mode 100644 (file)
index 0000000..b90aa6c
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0xF0",
+          "green" : "0xB3",
+          "red" : "0xFF"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Map/Assets.xcassets/Colors/Theme/Light Porcelain Green.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Light Porcelain Green.colorset/Contents.json
new file mode 100644 (file)
index 0000000..13a53a1
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x7C",
+          "green" : "0xC1",
+          "red" : "0x23"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Map/Assets.xcassets/Colors/Theme/Naples Yellow.colorset/Contents.json b/Map/Assets.xcassets/Colors/Theme/Naples Yellow.colorset/Contents.json
new file mode 100644 (file)
index 0000000..1c11b82
--- /dev/null
@@ -0,0 +1,20 @@
+{
+  "colors" : [
+    {
+      "color" : {
+        "color-space" : "srgb",
+        "components" : {
+          "alpha" : "1.000",
+          "blue" : "0x8F",
+          "green" : "0xED",
+          "red" : "0xFA"
+        }
+      },
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
diff --git a/Map/Core Extensions/Date+format.swift b/Map/Core Extensions/Date+format.swift
deleted file mode 100644 (file)
index d29ed0f..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-//
-//  Date+format.swift
-//  Map
-//
-//  Created by Ruben Beltran del Rio on 2/1/21.
-//
-
-import Foundation
-
-extension Date {
-  func format() -> String {
-    let formatter = DateFormatter()
-    formatter.dateStyle = .short
-    formatter.timeStyle = .medium
-    return formatter.string(from: self)
-  }
-}
diff --git a/Map/Core Extensions/Font+Theme.swift b/Map/Core Extensions/Font+Theme.swift
deleted file mode 100644 (file)
index 30bc6f9..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-import SwiftUI
-
-public extension Font {
-  struct theme {
-    static let axisLabel = Font.custom("Hiragino Mincho ProN", size: 14)
-    static let vertexLabel = Font.custom("Hiragino Mincho ProN", size: 12)
-  }
-}
diff --git a/Map/Core Extensions/NSImage+writePNG.swift b/Map/Core Extensions/NSImage+writePNG.swift
deleted file mode 100644 (file)
index c24c0cf..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-import Cocoa
-
-extension NSImage {
-  public func writePNG(toURL url: URL) {
-
-    guard let data = tiffRepresentation,
-      let rep = NSBitmapImageRep(data: data),
-      let imgData = rep.representation(
-        using: .png, properties: [.compressionFactor: NSNumber(floatLiteral: 1.0)])
-    else {
-
-      print(
-        "\(self.self) Error Function '\(#function)' Line: \(#line) No tiff rep found for image writing to \(url)"
-      )
-      return
-    }
-
-    do {
-      try imgData.write(to: url)
-    } catch let error {
-      print(
-        "\(self.self) Error Function '\(#function)' Line: \(#line) \(error.localizedDescription)")
-    }
-  }
-}
diff --git a/Map/Data/AppState.swift b/Map/Data/AppState.swift
deleted file mode 100644 (file)
index d0d5670..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-import Cocoa
-import Foundation
-import SwiftUI
-
-struct AppState {
-  var selectedEvolution: StageType = .general
-}
-
-enum AppAction {
-  case selectEvolution(evolution: StageType)
-  case exportMapAsImage(map: Map)
-  case exportMapAsText(map: Map)
-  case deleteMap(map: Map)
-}
-
-func appStateReducer(state: inout AppState, action: AppAction) {
-
-  switch action {
-
-  case .selectEvolution(let evolution):
-    state.selectedEvolution = evolution
-
-  case .exportMapAsImage(let map):
-    let window = NSWindow(
-      contentRect: .init(
-        origin: .zero,
-        size: .init(
-          width: NSScreen.main!.frame.width,
-          height: NSScreen.main!.frame.height)),
-      styleMask: [.closable],
-      backing: .buffered,
-      defer: false)
-
-    window.title = map.title ?? "Untitled Map"
-    window.isOpaque = true
-    window.center()
-    window.isMovableByWindowBackground = true
-    window.makeKeyAndOrderFront(nil)
-
-    let renderView = MapRenderView(
-      content: Binding.constant(map.content ?? ""),
-      evolution: Binding.constant(state.selectedEvolution))
-
-    let view = NSHostingView(rootView: renderView)
-    window.contentView = view
-
-    let imageRepresentation = view.bitmapImageRepForCachingDisplay(in: view.bounds)!
-    view.cacheDisplay(in: view.bounds, to: imageRepresentation)
-    let image = NSImage(cgImage: imageRepresentation.cgImage!, size: view.bounds.size)
-
-    let dialog = NSSavePanel()
-
-    dialog.title = "Save Map"
-    dialog.showsResizeIndicator = false
-    dialog.canCreateDirectories = true
-    dialog.showsHiddenFiles = false
-    dialog.allowedContentTypes = [.png]
-    dialog.nameFieldStringValue = map.title ?? "Untitled Map"
-
-    if dialog.runModal() == NSApplication.ModalResponse.OK {
-      let result = dialog.url
-
-      if result != nil {
-
-        image.writePNG(toURL: result!)
-        print("saved at \(result!)")
-      }
-    } else {
-      print("Cancel")
-    }
-    window.orderOut(nil)
-
-  case .exportMapAsText(let map):
-    let dialog = NSSavePanel()
-
-    dialog.title = "Save Map Text"
-    dialog.showsResizeIndicator = false
-    dialog.canCreateDirectories = true
-    dialog.showsHiddenFiles = false
-    dialog.allowedContentTypes = [.text]
-    dialog.nameFieldStringValue = map.title ?? "Untitled Map"
-
-    if let content = map.content {
-
-      if dialog.runModal() == NSApplication.ModalResponse.OK {
-        let result = dialog.url
-
-        if let result = result {
-          try? content.write(to: result, atomically: true, encoding: String.Encoding.utf8)
-        }
-      } else {
-        print("Cancel")
-      }
-    }
-  case .deleteMap(let map):
-    let context = PersistenceController.shared.container.viewContext
-    context.delete(map)
-
-    try? context.save()
-  }
-}
-
-typealias AppStore = Store<AppState, AppAction>
diff --git a/Map/Data/MapDocument.swift b/Map/Data/MapDocument.swift
new file mode 100644 (file)
index 0000000..9340684
--- /dev/null
@@ -0,0 +1,42 @@
+import SwiftUI
+import UniformTypeIdentifiers
+
+extension UTType {
+  static var exampleText: UTType {
+    UTType(importedAs: "systems.tranquil.map.wmap")
+  }
+}
+
+struct MapDocument: FileDocument {
+  var text: String
+
+  init(text: String = "Hello, world!") {
+    self.text = text
+  }
+
+  static var readableContentTypes: [UTType] { [.exampleText] }
+
+  init(configuration: ReadConfiguration) throws {
+    guard let data = configuration.file.regularFileContents,
+      let string = String(data: data, encoding: .utf8)
+    else {
+      throw CocoaError(.fileReadCorruptFile)
+    }
+    text = string
+  }
+
+  func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
+    let data = text.data(using: .utf8)!
+    return .init(regularFileWithContents: data)
+  }
+
+  @MainActor
+  func exportAsImage(withEvolution selectedEvolution: StageType) -> NSImage? {
+    let renderView = MapRenderView(
+      document: .constant(self),
+      evolution: .constant(selectedEvolution))
+    let renderer = ImageRenderer(content: renderView)
+
+    return renderer.nsImage
+  }
+}
diff --git a/Map/Data/Models/Map+parse.swift b/Map/Data/Models/Map+parse.swift
deleted file mode 100644 (file)
index 5181daf..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-extension Map {
-  static func parse(content: String) -> ParsedMap {
-
-    let parsers = [
-      AnyMapParserStrategy(NoteParserStrategy()),
-      AnyMapParserStrategy(VertexParserStrategy()),
-      AnyMapParserStrategy(EdgeParserStrategy()),
-      AnyMapParserStrategy(BlockerParserStrategy()),
-      AnyMapParserStrategy(OpportunityParserStrategy()),
-      AnyMapParserStrategy(StageParserStrategy()),
-    ]
-    let builder = MapBuilder()
-
-    let lines = content.split(whereSeparator: \.isNewline)
-
-    for (index, line) in lines.enumerated() {
-      for parser in parsers {
-        if parser.canHandle(line: String(line)) {
-          let (type, object) = parser.handle(
-            index: index, line: String(line), vertices: builder.vertices)
-          builder.addObjectToMap(type: type, object: object)
-          break
-        }
-      }
-    }
-
-    return builder.build()
-  }
-}
diff --git a/Map/Data/Persistence.swift b/Map/Data/Persistence.swift
deleted file mode 100644 (file)
index 1eb09e8..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-import CoreData
-
-struct PersistenceController {
-  static let shared = PersistenceController()
-
-  static var preview: PersistenceController = {
-    let result = PersistenceController(inMemory: true)
-    let viewContext = result.container.viewContext
-    for _ in 0..<10 {
-      let newMap = Map(context: viewContext)
-      newMap.uuid = UUID()
-      newMap.createdAt = Date()
-      newMap.title = "Map \(newMap.createdAt!.format())"
-      newMap.content = ""
-    }
-    do {
-      try viewContext.save()
-    } catch {
-      let nsError = error as NSError
-      fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
-    }
-    return result
-  }()
-
-  let container: NSPersistentCloudKitContainer
-
-  init(inMemory: Bool = false) {
-    container = NSPersistentCloudKitContainer(name: "Map")
-
-    container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
-    container.viewContext.automaticallyMergesChangesFromParent = true
-
-    if inMemory {
-      container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null")
-    }
-    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
-      if let error = error as NSError? {
-        fatalError("Unresolved error \(error), \(error.userInfo)")
-      }
-    })
-  }
-}
diff --git a/Map/Data/Store.swift b/Map/Data/Store.swift
deleted file mode 100644 (file)
index 7860f33..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-import Foundation
-
-final class Store<State, Action>: ObservableObject {
-  @Published private(set) var state: State
-
-  private let reducer: Reducer<State, Action>
-
-  init(initialState: State, reducer: @escaping Reducer<State, Action>) {
-    self.state = initialState
-    self.reducer = reducer
-  }
-
-  func send(_ action: Action) {
-    reducer(&state, action)
-  }
-}
-
-typealias Reducer<State, Action> = (inout State, Action) -> Void
index 6ba8cf0dcdc91e1ec37666bc341e3eeedc32ffba..61150cf6e956228449194f844856e1a6931931c6 100644 (file)
@@ -2,25 +2,42 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-       <key>CFBundleDevelopmentRegion</key>
-       <string>$(DEVELOPMENT_LANGUAGE)</string>
-       <key>CFBundleExecutable</key>
-       <string>$(EXECUTABLE_NAME)</string>
-       <key>CFBundleIdentifier</key>
-       <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
-       <key>CFBundleInfoDictionaryVersion</key>
-       <string>6.0</string>
-       <key>CFBundleName</key>
-       <string>$(PRODUCT_NAME)</string>
-       <key>CFBundlePackageType</key>
-       <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
-       <key>CFBundleShortVersionString</key>
-       <string>$(MARKETING_VERSION)</string>
-       <key>CFBundleVersion</key>
-       <string>$(CURRENT_PROJECT_VERSION)</string>
-       <key>LSApplicationCategoryType</key>
-       <string>public.app-category.productivity</string>
-       <key>LSMinimumSystemVersion</key>
-       <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+       <key>CFBundleDocumentTypes</key>
+       <array>
+               <dict>
+                       <key>CFBundleTypeRole</key>
+                       <string>Editor</string>
+                       <key>LSHandlerRank</key>
+                       <string>Default</string>
+                       <key>LSItemContentTypes</key>
+                       <array>
+                               <string>systems.tranquil.map.wmap</string>
+                       </array>
+                       <key>NSUbiquitousDocumentUserActivityType</key>
+                       <string>$(PRODUCT_BUNDLE_IDENTIFIER).exampledocument</string>
+               </dict>
+       </array>
+       <key>UTImportedTypeDeclarations</key>
+       <array>
+               <dict>
+                       <key>UTTypeConformsTo</key>
+                       <array>
+                               <string>public.plain-text</string>
+                       </array>
+                       <key>UTTypeDescription</key>
+                       <string>Wardley Map written in Map's wmap syntax</string>
+                       <key>UTTypeIcons</key>
+                       <dict/>
+                       <key>UTTypeIdentifier</key>
+                       <string>systems.tranquil.map.wmap</string>
+                       <key>UTTypeTagSpecification</key>
+                       <dict>
+                               <key>public.filename-extension</key>
+                               <array>
+                                       <string>wmap</string>
+                               </array>
+                       </dict>
+               </dict>
+       </array>
 </dict>
 </plist>
 </dict>
 </plist>
diff --git a/Map/Logic/Constants.swift b/Map/Logic/Constants.swift
new file mode 100644 (file)
index 0000000..8cb95e5
--- /dev/null
@@ -0,0 +1,4 @@
+struct Constants {
+  static let kMaxZoom = 2.0
+  static let kMinZoom = 0.1
+}
index 5f78d5d46b43887a88d8abead935feb5c3ac98d8..1a66f9ad5f055e11fbfe434f94604e4c21909775 100644 (file)
@@ -1,25 +1,60 @@
 import CoreGraphics
 import Foundation
 
 import CoreGraphics
 import Foundation
 
+struct MapParser {
+  static func parse(content: String) -> ParsedMap {
+
+    let parsers = [
+      AnyMapParserStrategy(NoteParserStrategy()),
+      AnyMapParserStrategy(VertexParserStrategy()),
+      AnyMapParserStrategy(EdgeParserStrategy()),
+      AnyMapParserStrategy(BlockerParserStrategy()),
+      AnyMapParserStrategy(OpportunityParserStrategy()),
+      AnyMapParserStrategy(StageParserStrategy()),
+      AnyMapParserStrategy(GroupParserStrategy()),
+    ]
+    let builder = MapBuilder()
+
+    let lines = content.split(whereSeparator: \.isNewline)
+
+    for (index, line) in lines.enumerated() {
+      for parser in parsers {
+        if parser.canHandle(line: String(line)) {
+          let (type, object) = parser.handle(
+            index: index, line: String(line), vertices: builder.vertices)
+          builder.addObjectToMap(type: type, object: object)
+          break
+        }
+      }
+    }
+
+    return builder.build()
+  }
+}
+
 // MARK: - Types
 
 struct MapParsingPatterns {
   static let vertex = try! NSRegularExpression(
     pattern:
 // MARK: - Types
 
 struct MapParsingPatterns {
   static let vertex = try! NSRegularExpression(
     pattern:
-      "([^\\(\\[\\]]*?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?",
-    options: .caseInsensitive)
+      "^([^\\(\\[\\]]*?)[\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(?:\\[(.*?)\\])?[\\s]*$",
+    options: [.caseInsensitive, .anchorsMatchLines])
   static let edge = try! NSRegularExpression(
   static let edge = try! NSRegularExpression(
-    pattern: "(.+?)[\\s]*-([->])[\\s]*(.+)", options: .caseInsensitive)
+    pattern: "^(.+?)[\\s]*-([->])[\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines])
   static let blocker = try! NSRegularExpression(
   static let blocker = try! NSRegularExpression(
-    pattern: "\\[(Blocker)\\][\\s]*(.+)", options: .caseInsensitive)
+    pattern: "^\\[(Blocker)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines])
   static let opportunity = try! NSRegularExpression(
   static let opportunity = try! NSRegularExpression(
-    pattern: "\\[(Evolution)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)",
-    options: .caseInsensitive)
+    pattern: "^\\[(Evolution)\\][\\s]*(.+)[\\s]+([-+])[\\s]*([0-9]+.?[0-9]*)",
+    options: [.caseInsensitive, .anchorsMatchLines])
   static let note = try! NSRegularExpression(
   static let note = try! NSRegularExpression(
-    pattern: "\\[(Note)\\][\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(.*)",
-    options: .caseInsensitive)
+    pattern:
+      "^\\[(Note)\\][\\s]*\\([\\s]*([0-9]+.?[0-9]*)[\\s]*,[\\s]*([0-9]+.?[0-9]*)[\\s]*\\)[\\s]*(.*)",
+    options: [.caseInsensitive, .anchorsMatchLines])
   static let stage = try! NSRegularExpression(
   static let stage = try! NSRegularExpression(
-    pattern: "\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)", options: .caseInsensitive)
+    pattern: "^\\[(I{1,3})\\][\\s]*([0-9]+.?[0-9]*)",
+    options: [.caseInsensitive, .anchorsMatchLines])
+  static let group = try! NSRegularExpression(
+    pattern: "^\\[(Group)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines])
 }
 
 struct ParsedMap {
 }
 
 struct ParsedMap {
@@ -29,12 +64,14 @@ struct ParsedMap {
   let opportunities: [Opportunity]
   let notes: [Note]
   let stages: [CGFloat]
   let opportunities: [Opportunity]
   let notes: [Note]
   let stages: [CGFloat]
+  let groups: [[Vertex]]
 
   static let empty: ParsedMap = ParsedMap(
 
   static let empty: ParsedMap = ParsedMap(
-    vertices: [], edges: [], blockers: [], opportunities: [], notes: [], stages: defaultDimensions)
+    vertices: [], edges: [], blockers: [], opportunities: [], notes: [], stages: defaultDimensions,
+    groups: [])
 }
 
 }
 
-struct Vertex {
+struct Vertex: Identifiable, Hashable {
   let id: Int
   let label: String
   let position: CGPoint
   let id: Int
   let label: String
   let position: CGPoint
@@ -115,6 +152,7 @@ class MapBuilder {
   private var opportunities: [Opportunity] = []
   private var notes: [Note] = []
   private var stages: [CGFloat] = defaultDimensions
   private var opportunities: [Opportunity] = []
   private var notes: [Note] = []
   private var stages: [CGFloat] = defaultDimensions
+  private var groups: [[Vertex]] = []
 
   func addObjectToMap(type: Any.Type, object: Any) {
     if type == Vertex.self {
 
   func addObjectToMap(type: Any.Type, object: Any) {
     if type == Vertex.self {
@@ -136,7 +174,7 @@ class MapBuilder {
       let opportunity = object as! Opportunity
       opportunities.append(opportunity)
     }
       let opportunity = object as! Opportunity
       opportunities.append(opportunity)
     }
-    
+
     if type == Note.self {
       let note = object as! Note
       notes.append(note)
     if type == Note.self {
       let note = object as! Note
       notes.append(note)
@@ -146,12 +184,18 @@ class MapBuilder {
       let stageDimensions = object as! StageDimensions
       stages[stageDimensions.index] = stageDimensions.dimensions
     }
       let stageDimensions = object as! StageDimensions
       stages[stageDimensions.index] = stageDimensions.dimensions
     }
+
+    if type == [Vertex].self {
+      let group = object as! [Vertex]
+      groups.append(group)
+    }
   }
 
   func build() -> ParsedMap {
     let mappedVertices = vertices.map { label, vertex in return vertex }
     return ParsedMap(
   }
 
   func build() -> ParsedMap {
     let mappedVertices = vertices.map { label, vertex in return vertex }
     return ParsedMap(
-      vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities, notes: notes,
-      stages: stages)
+      vertices: mappedVertices, edges: edges, blockers: blockers, opportunities: opportunities,
+      notes: notes,
+      stages: stages, groups: groups)
   }
 }
   }
 }
diff --git a/Map/Logic/MapParser/Strategies/GroupParserStrategy.swift b/Map/Logic/MapParser/Strategies/GroupParserStrategy.swift
new file mode 100644 (file)
index 0000000..7924935
--- /dev/null
@@ -0,0 +1,34 @@
+import Foundation
+
+struct GroupParserStrategy: MapParserStrategy {
+  private let regex = MapParsingPatterns.group
+
+  func canHandle(line: String) -> Bool {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+    return matches.count > 0 && matches[0].numberOfRanges == 3
+  }
+
+  func handle(index: Int, line: String, vertices: [String: Vertex]) -> (Any.Type, Any) {
+    let range = NSRange(location: 0, length: line.utf16.count)
+    let matches = regex.matches(in: String(line), options: [], range: range)
+
+    let match = matches[0]
+    var groupVertices: [Vertex] = []
+    let vertexIdString = String(line[Range(match.range(at: 2), in: line)!])
+    let vertexIds = vertexIdString.split(separator: " ", omittingEmptySubsequences: true).map(
+      String.init)
+
+    for vertexId in vertexIds {
+      if let vertex = vertices[vertexId] {
+        groupVertices.append(vertex)
+      }
+    }
+
+    if groupVertices.count > 0 {
+      return ([Vertex].self, groupVertices)
+    }
+
+    return (NSObject.self, NSObject())
+  }
+}
index 6060301e73053c3a7737aba76441b166361f9a95..6d968edb4f8cd6ec865ba1cc4f804c71ef522b3e 100644 (file)
@@ -2,19 +2,9 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
-       <key>com.apple.developer.aps-environment</key>
-       <string>development</string>
-       <key>com.apple.developer.icloud-container-identifiers</key>
-       <array>
-               <string>iCloud.pizza.unlimited.map</string>
-       </array>
-       <key>com.apple.developer.icloud-services</key>
-       <array>
-               <string>CloudKit</string>
-       </array>
-       <key>com.apple.security.app-sandbox</key>
-       <true/>
-       <key>com.apple.security.files.user-selected.read-write</key>
-       <true/>
+    <key>com.apple.security.app-sandbox</key>
+    <true/>
+    <key>com.apple.security.files.user-selected.read-write</key>
+    <true/>
 </dict>
 </plist>
 </dict>
 </plist>
diff --git a/Map/Map.xcdatamodeld/.xccurrentversion b/Map/Map.xcdatamodeld/.xccurrentversion
deleted file mode 100644 (file)
index ee72e60..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-       <key>_XCCurrentVersionName</key>
-       <string>Map.xcdatamodel</string>
-</dict>
-</plist>
diff --git a/Map/Map.xcdatamodeld/Map.xcdatamodel/contents b/Map/Map.xcdatamodeld/Map.xcdatamodel/contents
deleted file mode 100644 (file)
index ea8b276..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D5029f" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
-    <entity name="Map" representedClassName="Map" syncable="YES" codeGenerationType="class">
-        <attribute name="content" optional="YES" attributeType="String"/>
-        <attribute name="createdAt" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
-        <attribute name="title" optional="YES" attributeType="String"/>
-        <attribute name="uuid" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
-    </entity>
-    <elements>
-        <element name="Map" positionX="-63" positionY="-18" width="128" height="89"/>
-    </elements>
-</model>
\ No newline at end of file
index 14e5605792713e070c0a57a4790d6a79d20d0395..9aff010b2021f042f3d7579faac0a6c7b975b616 100644 (file)
@@ -1,23 +1,12 @@
-//
-//  MapApp.swift
-//  Map
-//
-//  Created by Ruben Beltran del Rio on 2/1/21.
-//
-
 import SwiftUI
 
 @main
 struct MapApp: App {
 import SwiftUI
 
 @main
 struct MapApp: App {
-  let persistenceController = PersistenceController.shared
-
   var body: some Scene {
   var body: some Scene {
-    WindowGroup {
-      MapEditorWindow()
-        .environment(\.managedObjectContext, persistenceController.container.viewContext)
-        .environmentObject(AppStore(initialState: AppState(), reducer: appStateReducer))
-    }.windowStyle(HiddenTitleBarWindowStyle()).commands {
-      SidebarCommands()
+    DocumentGroup(newDocument: MapDocument()) { file in
+      MapEditor(document: file.$document, url: file.fileURL)
+    }.commands {
+      MapCommands()
     }
   }
 }
     }
   }
 }
index c68e90cdf51ce6c8896e0e43119abc1050060fdc..c30798a21934d0b6fa014b2db46b17bf8111d0c8 100644 (file)
@@ -2,40 +2,30 @@ import SwiftUI
 
 struct EvolutionPicker: View {
 
 
 struct EvolutionPicker: View {
 
-  @EnvironmentObject private var store: AppStore
-
-  private var selectedEvolution: Binding<StageType> {
-    Binding(
-      get: { store.state.selectedEvolution },
-      set: { evolution in
-        store.send(.selectEvolution(evolution: evolution))
-      }
-    )
-  }
+  @Binding var selectedEvolution: StageType
 
   var body: some View {
 
   var body: some View {
-    Picker("Evolution", selection: selectedEvolution) {
+    Picker("Evolution", selection: $selectedEvolution) {
       ForEach(StageType.types) { stage in
       ForEach(StageType.types) { stage in
-        Text(Stage.title(stage)).tag(stage).padding(4.0)
+        Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
       }
       Divider()
       ForEach(StageType.characteristics) { stage in
       }
       Divider()
       ForEach(StageType.characteristics) { stage in
-        Text(Stage.title(stage)).tag(stage).padding(4.0)
+        Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0).font(.theme.body)
       }
       Divider()
       ForEach(StageType.properties) { stage in
       }
       Divider()
       ForEach(StageType.properties) { stage in
-        Text(Stage.title(stage)).tag(stage).padding(4.0)
+        Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
       }
       Divider()
       ForEach(StageType.custom) { stage in
       }
       Divider()
       ForEach(StageType.custom) { stage in
-        Text(Stage.title(stage)).tag(stage).padding(4.0)
+        Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
       }
       }
-    }.padding(.horizontal, 8.0).padding(.vertical, 4.0)
+    }.font(.theme.body).padding(.horizontal, 8.0).padding(.vertical, 4.0)
   }
 }
 
   }
 }
 
-struct EvolutionPicker_Previews: PreviewProvider {
-  static var previews: some View {
-    EvolutionPicker()
-  }
+#Preview {
+  let selectedEvolution: StageType = .behavior
+  EvolutionPicker(selectedEvolution: .constant(selectedEvolution))
 }
 }
index 6ba758f71675ac2dbe4403d8e0f82a102a8e5faa..1b9c226544e91dc3459f7b087380ae296b4352b9 100644 (file)
@@ -22,10 +22,14 @@ struct MapAxes: View {
       }.stroke(Color.map.axisColor, lineWidth: lineWidth * 2)
 
       // Y Labels
       }.stroke(Color.map.axisColor, lineWidth: lineWidth * 2)
 
       // Y Labels
-      Text("Visible").font(.theme.axisLabel).foregroundColor(.map.labelColor).rotationEffect(Angle(degrees: -90.0))
-        .offset(CGSize(width: -35.0, height: 0.0))
-      Text("Invisible").font(.theme.axisLabel).foregroundColor(.map.labelColor).rotationEffect(Angle(degrees: -90.0))
-        .offset(CGSize(width: -40.0, height: mapSize.height - 20))
+      Text("Visible").font(.theme.axisLabel).foregroundColor(.map.labelColor).rotationEffect(
+        Angle(degrees: -90.0)
+      )
+      .offset(CGSize(width: -35.0, height: 0.0))
+      Text("Invisible").font(.theme.axisLabel).foregroundColor(.map.labelColor).rotationEffect(
+        Angle(degrees: -90.0)
+      )
+      .offset(CGSize(width: -40.0, height: mapSize.height - 20))
 
       // X Labels
 
 
       // X Labels
 
@@ -71,11 +75,9 @@ struct MapAxes: View {
   }
 }
 
   }
 }
 
-struct MapAxes_Previews: PreviewProvider {
-  static var previews: some View {
-    MapAxes(
-      mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(1.0),
-      evolution: Stage.stages(.general), stages: [25.0, 50.0, 75.0]
-    ).padding(50.0)
-  }
+#Preview {
+  MapAxes(
+    mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(1.0),
+    evolution: Stage.stages(.general), stages: [25.0, 50.0, 75.0]
+  ).padding(50.0)
 }
 }
index d943ba8bb61a8a72f00379518b111c10a17659b7..efd84fae4ef2e7a2864ef65e5e4d37257872aec2 100644 (file)
@@ -31,13 +31,11 @@ struct MapBlockers: View {
   }
 }
 
   }
 }
 
-struct MapBlockers_Previews: PreviewProvider {
-  static var previews: some View {
-    MapBlockers(
-      mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0),
-      blockers: [
-        Blocker(id: 0, position: CGPoint(x: 50.0, y: 50.0)),
-        Blocker(id: 1, position: CGPoint(x: 10.0, y: 20.0)),
-      ])
-  }
+#Preview {
+  MapBlockers(
+    mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0),
+    blockers: [
+      Blocker(id: 0, position: CGPoint(x: 50.0, y: 50.0)),
+      Blocker(id: 1, position: CGPoint(x: 10.0, y: 20.0)),
+    ])
 }
 }
index 38144951d20e224c5f4c4b094f0a64baf77c5cc0..2af7089257b2a6e93f1fb3a770f198d5c6a1f43b 100644 (file)
@@ -65,15 +65,13 @@ struct MapEdges: View {
   }
 }
 
   }
 }
 
-struct MapEdges_Previews: PreviewProvider {
-  static var previews: some View {
-    MapEdges(
-      mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0,
-      vertexSize: CGSize(width: 25.0, height: 25.0),
-      edges: [
-        MapEdge(
-          id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2),
-          arrowhead: true)
-      ])
-  }
+#Preview {
+  MapEdges(
+    mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0,
+    vertexSize: CGSize(width: 25.0, height: 25.0),
+    edges: [
+      MapEdge(
+        id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2),
+        arrowhead: true)
+    ])
 }
 }
diff --git a/Map/Presentation/Base Components/MapRender/MapGroup.swift b/Map/Presentation/Base Components/MapRender/MapGroup.swift
new file mode 100644 (file)
index 0000000..9df3338
--- /dev/null
@@ -0,0 +1,83 @@
+import ConcaveHull
+import SwiftUI
+
+struct MapGroup: View {
+
+  let mapSize: CGSize
+  let vertexSize: CGSize
+  let group: [Vertex]
+  let color: Color
+
+  let cornerSize = CGSize(width: 2.0, height: 2.0)
+  var strokeSize: CGFloat { 1.75 * vertexSize.width }
+
+  var hull: [CGPoint] {
+    let groupList = group.map({ vertex in
+      return [Double(vertex.position.x), Double(vertex.position.y)]
+    })
+    let hull = Hull()
+    let hullPoints = hull.hull(groupList, nil)
+    return hullPoints.compactMap({ object in
+      if let point = object as? [Double] {
+        return CGPoint(x: point[0], y: point[1])
+      }
+      return nil
+    })
+  }
+
+  var body: some View {
+    Path { path in
+      var initialMove: CGPoint?
+
+      for point in hull {
+        let offsetPoint = CGPoint(x: w(point.x), y: h(point.y))
+
+        if initialMove == nil {
+          path.move(to: offsetPoint)
+          initialMove = offsetPoint
+        } else {
+          path.addLine(to: offsetPoint)
+        }
+      }
+
+      if let initialMove = initialMove {
+        path.addLine(to: initialMove)
+      }
+
+    }
+    .applying(
+      CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0)
+    )
+    .fill(color)
+    .stroke(
+      color,
+      style: StrokeStyle(
+        lineWidth: strokeSize,
+        lineCap: .round,
+        lineJoin: .round,
+        miterLimit: 0,
+        dash: [],
+        dashPhase: 0
+      )
+    )
+  }
+
+  func h(_ dimension: CGFloat) -> CGFloat {
+    max(0.0, min(mapSize.height, dimension * mapSize.height / 100.0))
+  }
+
+  func w(_ dimension: CGFloat) -> CGFloat {
+    max(0.0, min(mapSize.width, dimension * mapSize.width / 100.0))
+  }
+}
+
+#Preview {
+  MapGroup(
+    mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0),
+    group: [
+      Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)),
+      Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square),
+      Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle),
+      Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x),
+    ], color: .red)
+}
diff --git a/Map/Presentation/Base Components/MapRender/MapGroups.swift b/Map/Presentation/Base Components/MapRender/MapGroups.swift
new file mode 100644 (file)
index 0000000..84f7fb2
--- /dev/null
@@ -0,0 +1,32 @@
+import ConcaveHull
+import SwiftUI
+
+struct MapGroups: View {
+
+  let mapSize: CGSize
+  let vertexSize: CGSize
+  let groups: [[Vertex]]
+
+  var body: some View {
+    ForEach(Array(groups.enumerated()), id: \.element) { index, group in
+      MapGroup(mapSize: mapSize, vertexSize: vertexSize, group: group, color: color(index))
+    }
+  }
+
+  private func color(_ index: Int) -> Color {
+    return .map.groupColors[index % Color.map.groupColors.count]
+  }
+}
+
+#Preview {
+  MapGroups(
+    mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0),
+    groups: [
+      [
+        Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)),
+        Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square),
+        Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle),
+        Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x),
+      ]
+    ])
+}
index f35b3fe2e2c587239e0b77e28c4b6c0a80df1150..bbb1aba2e58581a071a7a816f34e481d7a896198 100644 (file)
@@ -5,13 +5,12 @@ struct MapNotes: View {
   let mapSize: CGSize
   let lineWidth: CGFloat
   let notes: [Note]
   let mapSize: CGSize
   let lineWidth: CGFloat
   let notes: [Note]
-  
+
   let maxWidth = 400.0
   let maxWidth = 400.0
-  
 
   var body: some View {
     ForEach(notes, id: \.id) { note in
 
   var body: some View {
     ForEach(notes, id: \.id) { note in
-      Text(note.text.replacingOccurrences(of: "\\n", with: "\n")).font(.theme.axisLabel)
+      Text(note.text.replacingOccurrences(of: "\\n", with: "\n")).font(.theme.note)
         .padding(2.0)
         .background(.white)
         .foregroundColor(.map.labelColor)
         .padding(2.0)
         .background(.white)
         .foregroundColor(.map.labelColor)
@@ -35,12 +34,13 @@ struct MapNotes: View {
   }
 }
 
   }
 }
 
-struct MapNotes_Previews: PreviewProvider {
-  static var previews: some View {
-    MapNotes(
-      mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0,
-        notes: [
-          Note(id: 0, position: CGPoint(x: 50.0, y: 50.0), text: "Notes can have a lot more text, so we need to make sure that they're resized correctly"),
-        ])
-  }
+#Preview {
+  MapNotes(
+    mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0,
+    notes: [
+      Note(
+        id: 0, position: CGPoint(x: 50.0, y: 50.0),
+        text:
+          "Notes can have a lot more text, so we need to make sure that they're resized correctly")
+    ])
 }
 }
index 7fcadff8ffb46bfe26aa52b336a90ff6da08e24b..b3051ee5a4ab7699ee918092d3a8a13d138a303d 100644 (file)
@@ -46,7 +46,8 @@ struct MapOpportunities: View {
         path.closeSubpath()
       }.applying(
         CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0)
         path.closeSubpath()
       }.applying(
         CGAffineTransform(translationX: vertexSize.width / 2.0, y: vertexSize.height / 2.0)
-      ).strokedPath(StrokeStyle(lineWidth: lineWidth / 4, dash: [10.0])).stroke(Color.map.opportunityColor)
+      ).strokedPath(StrokeStyle(lineWidth: lineWidth / 4, dash: [10.0])).stroke(
+        Color.map.opportunityColor)
     }
   }
 
     }
   }
 
@@ -59,13 +60,11 @@ struct MapOpportunities: View {
   }
 }
 
   }
 }
 
-struct MapOpportunities_Previews: PreviewProvider {
-  static var previews: some View {
-    MapOpportunities(
-      mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0,
-      vertexSize: CGSize(width: 25.0, height: 25.0),
-      opportunities: [
-        Opportunity(id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2))
-      ])
-  }
+#Preview {
+  MapOpportunities(
+    mapSize: CGSize(width: 400.0, height: 400.0), lineWidth: 1.0,
+    vertexSize: CGSize(width: 25.0, height: 25.0),
+    opportunities: [
+      Opportunity(id: 1, origin: CGPoint(x: 2.0, y: 34.0), destination: CGPoint(x: 23.0, y: 76.2))
+    ])
 }
 }
index 0fc8f486a7ec16e5ef49d469a0e992387576b786..fc3bfa196e1e3fe6ade821d4f4e81ae09809138c 100644 (file)
@@ -1,5 +1,5 @@
-import SwiftUI
 import Patterns
 import Patterns
+import SwiftUI
 
 struct MapStages: View {
 
 
 struct MapStages: View {
 
@@ -10,17 +10,29 @@ struct MapStages: View {
 
   var body: some View {
     ZStack(alignment: .topLeading) {
 
   var body: some View {
     ZStack(alignment: .topLeading) {
-      PatternView(design: .constant(.stitch), pixelSize: 1.0, foregroundColor: .map.stageForeground, backgroundColor: .map.stageBackground)
-        .frame(width: w(stages[0]), height: mapSize.height)
-      PatternView(design: .constant(.shingles), pixelSize: 1.0, foregroundColor: .map.stageForeground, backgroundColor: .map.stageBackground)
-        .offset(CGSize(width: w(stages[0]), height: 0))
-        .frame(width: w(stages[1]) - w(stages[0]), height: mapSize.height)
-      PatternView(design: .constant(.shadowGrid), pixelSize: 1.0, foregroundColor: .map.stageForeground, backgroundColor: .map.stageBackground)
-        .offset(CGSize(width: w(stages[1]), height: 0))
-        .frame(width: w(stages[2]) - w(stages[1]), height: mapSize.height)
-      PatternView(design: .constant(.wicker), pixelSize: 1.0, foregroundColor: .map.stageForeground, backgroundColor: .map.stageBackground)
-        .offset(CGSize(width: w(stages[2]), height: 0))
-        .frame(width: mapSize.width - w(stages[2]), height: mapSize.height)
+      PatternView(
+        design: .constant(.stitch), pixelSize: 1.0, foregroundColor: .map.stageForeground,
+        backgroundColor: .map.stageBackground
+      )
+      .frame(width: w(stages[0]), height: mapSize.height)
+      PatternView(
+        design: .constant(.shingles), pixelSize: 1.0, foregroundColor: .map.stageForeground,
+        backgroundColor: .map.stageBackground
+      )
+      .offset(CGSize(width: w(stages[0]), height: 0))
+      .frame(width: w(stages[1]) - w(stages[0]), height: mapSize.height)
+      PatternView(
+        design: .constant(.shadowGrid), pixelSize: 1.0, foregroundColor: .map.stageForeground,
+        backgroundColor: .map.stageBackground
+      )
+      .offset(CGSize(width: w(stages[1]), height: 0))
+      .frame(width: w(stages[2]) - w(stages[1]), height: mapSize.height)
+      PatternView(
+        design: .constant(.wicker), pixelSize: 1.0, foregroundColor: .map.stageForeground,
+        backgroundColor: .map.stageBackground
+      )
+      .offset(CGSize(width: w(stages[2]), height: 0))
+      .frame(width: mapSize.width - w(stages[2]), height: mapSize.height)
 
       Path { path in
         path.move(to: CGPoint(x: w(stages[0]), y: 0))
 
       Path { path in
         path.move(to: CGPoint(x: w(stages[0]), y: 0))
@@ -34,7 +46,8 @@ struct MapStages: View {
         path.closeSubpath()
         path.move(to: CGPoint(x: w(stages[0]), y: 0))
         path.closeSubpath()
         path.closeSubpath()
         path.move(to: CGPoint(x: w(stages[0]), y: 0))
         path.closeSubpath()
-      }.strokedPath(StrokeStyle(lineWidth: lineWidth / 4, dash: [10.0, 18.0])).stroke(Color.map.axisColor)
+      }.strokedPath(StrokeStyle(lineWidth: lineWidth / 4, dash: [10.0, 18.0])).stroke(
+        Color.map.axisColor)
     }
   }
 
     }
   }
 
@@ -43,10 +56,8 @@ struct MapStages: View {
   }
 }
 
   }
 }
 
-struct MapStages_Previews: PreviewProvider {
-  static var previews: some View {
-    MapStages(
-      mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(0.5),
-      stages: [25.0, 50.0, 75.0])
-  }
+#Preview {
+  MapStages(
+    mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(0.5),
+    stages: [25.0, 50.0, 75.0])
 }
 }
index 74cac6d6ccb4b171e426cc4b93dae740dae034d5..5bc0a967aef078789bbf1bd1a56aa9f05d898e9a 100644 (file)
@@ -7,18 +7,29 @@ struct MapVertices: View {
   let vertices: [Vertex]
   let padding = CGFloat(5.0)
 
   let vertices: [Vertex]
   let padding = CGFloat(5.0)
 
+  var onDragVertex: (Vertex, CGFloat, CGFloat) -> Void = { _, _, _ in }
+
   var body: some View {
     ZStack(alignment: .topLeading) {
       ForEach(vertices, id: \.id) { vertex in
   var body: some View {
     ZStack(alignment: .topLeading) {
       ForEach(vertices, id: \.id) { vertex in
-        getVertexShape(vertex).fill(Color.map.vertexColor)
-        Text(vertex.label.replacingOccurrences(of: "\\n", with: "\n")).font(.theme.vertexLabel)
+        ZStack(alignment: .topLeading) {
+          getVertexShape(vertex).fill(Color.map.vertexColor)
+          Text(vertex.label.replacingOccurrences(of: "\\n", with: "\n")).font(.theme.vertexLabel)
             .foregroundColor(.map.labelColor)
             .shadow(color: .white, radius: 0, x: -0.5, y: -0.5)
             .shadow(color: .white, radius: 0, x: 0.5, y: 0.5)
             .offset(
             .foregroundColor(.map.labelColor)
             .shadow(color: .white, radius: 0, x: -0.5, y: -0.5)
             .shadow(color: .white, radius: 0, x: 0.5, y: 0.5)
             .offset(
-          CGSize(
-            width: w(vertex.position.x) + vertexSize.width + padding,
-            height: h(vertex.position.y) + 7.0))
+              CGSize(
+                width: w(vertex.position.x) + vertexSize.width + padding,
+                height: h(vertex.position.y) + 7.0))
+        }.gesture(
+          DragGesture()
+            .onChanged { value in
+              let deltaX = value.startLocation.x - value.location.x
+              let deltaY = value.startLocation.y - value.location.y
+              onDragVertex(vertex, deltaX, deltaY)
+            }
+        )
       }
     }
   }
       }
     }
   }
@@ -78,15 +89,13 @@ struct MapVertices: View {
   }
 }
 
   }
 }
 
-struct MapVertices_Previews: PreviewProvider {
-  static var previews: some View {
-      MapVertices(
-        mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0),
-        vertices: [
-          Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)),
-          Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square),
-          Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle),
-          Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x),
-        ])
-  }
+#Preview {
+  MapVertices(
+    mapSize: CGSize(width: 400.0, height: 400.0), vertexSize: CGSize(width: 25.0, height: 25.0),
+    vertices: [
+      Vertex(id: 0, label: "A Circle", position: CGPoint(x: 50.0, y: 50.0)),
+      Vertex(id: 1, label: "A Square", position: CGPoint(x: 10.0, y: 20.0), shape: .square),
+      Vertex(id: 2, label: "A triangle", position: CGPoint(x: 25, y: 32.0), shape: .triangle),
+      Vertex(id: 3, label: "An X", position: CGPoint(x: 70.0, y: 70.0), shape: .x),
+    ])
 }
 }
index f3838a663aa83d63dc22bf688326b93176a704a5..d7aa9f86b18eb0cef014a3b7dbe40af2a1214abc 100644 (file)
@@ -3,7 +3,7 @@ import SwiftUI
 
 class MapTextEditorController: NSViewController {
 
 
 class MapTextEditorController: NSViewController {
 
-  @Binding var text: String
+  @Binding var document: MapDocument
   let onChange: () -> Void
 
   private let vertexRegex = MapParsingPatterns.vertex
   let onChange: () -> Void
 
   private let vertexRegex = MapParsingPatterns.vertex
@@ -12,11 +12,12 @@ class MapTextEditorController: NSViewController {
   private let opportunityRegex = MapParsingPatterns.opportunity
   private let noteRegex = MapParsingPatterns.note
   private let stageRegex = MapParsingPatterns.stage
   private let opportunityRegex = MapParsingPatterns.opportunity
   private let noteRegex = MapParsingPatterns.note
   private let stageRegex = MapParsingPatterns.stage
+  private let groupRegex = MapParsingPatterns.group
 
   private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
 
 
   private let changeDebouncer: Debouncer = Debouncer(seconds: 1)
 
-    init(text: Binding<String>, onChange: @escaping () -> Void) {
-    self._text = text
+  init(document: Binding<MapDocument>, onChange: @escaping () -> Void) {
+    self._document = document
     self.onChange = onChange
     super.init(nibName: nil, bundle: nil)
   }
     self.onChange = onChange
     super.init(nibName: nil, bundle: nil)
   }
@@ -31,10 +32,11 @@ class MapTextEditorController: NSViewController {
 
     scrollView.translatesAutoresizingMaskIntoConstraints = false
 
 
     scrollView.translatesAutoresizingMaskIntoConstraints = false
 
+    textView.backgroundColor = .ui.background
     textView.allowsUndo = true
     textView.delegate = self
     textView.textStorage?.delegate = self
     textView.allowsUndo = true
     textView.delegate = self
     textView.textStorage?.delegate = self
-    textView.string = self.text
+    textView.string = self.document.text
     textView.isEditable = true
     textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
     self.view = scrollView
     textView.isEditable = true
     textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular)
     self.view = scrollView
@@ -49,14 +51,13 @@ extension MapTextEditorController: NSTextViewDelegate {
 
   func textDidChange(_ obj: Notification) {
     if let textField = obj.object as? NSTextView {
 
   func textDidChange(_ obj: Notification) {
     if let textField = obj.object as? NSTextView {
-      self.text = textField.string
-        
-        
-        changeDebouncer.debounce {
-          DispatchQueue.main.async {
-            self.onChange()
-          }
+      self.document.text = textField.string
+
+      changeDebouncer.debounce {
+        DispatchQueue.main.async {
+          self.onChange()
         }
         }
+      }
     }
   }
 
     }
   }
 
@@ -86,65 +87,91 @@ extension MapTextEditorController: NSTextStorageDelegate {
     var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
     var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 4))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.option], range: match.range(at: 4))
     }
 
     matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
     }
 
     matches = edgeRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1))
       let arrowRange = match.range(at: 2)
       textStorage.addAttributes(
         [.foregroundColor: NSColor.syntax.symbol],
         range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
       let arrowRange = match.range(at: 2)
       textStorage.addAttributes(
         [.foregroundColor: NSColor.syntax.symbol],
         range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 3))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 3))
     }
 
     matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
     }
 
     matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.symbol], range: match.range(at: 3))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 4))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.symbol], range: match.range(at: 3))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.number], range: match.range(at: 4))
     }
 
     matches = blockerRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
     }
 
     matches = blockerRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
     }
     }
-    
+
     matches = noteRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
     matches = noteRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.number], range: match.range(at: 3))
     }
 
     matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
     }
 
     matches = stageRegex.matches(in: textStorage.string, options: [], range: range)
 
     for match in matches {
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
-      textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.number], range: match.range(at: 2))
+    }
+
+    matches = groupRegex.matches(in: textStorage.string, options: [], range: range)
+
+    for match in matches {
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.option], range: match.range(at: 1))
+      textStorage.addAttributes(
+        [.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2))
     }
   }
 }
 
 struct MapTextEditor: NSViewControllerRepresentable {
 
     }
   }
 }
 
 struct MapTextEditor: NSViewControllerRepresentable {
 
-  @Binding var text: String
+  @Binding var document: MapDocument
   var onChange: () -> Void = {}
 
   func makeNSViewController(
     context: NSViewControllerRepresentableContext<MapTextEditor>
   ) -> MapTextEditorController {
   var onChange: () -> Void = {}
 
   func makeNSViewController(
     context: NSViewControllerRepresentableContext<MapTextEditor>
   ) -> MapTextEditorController {
-    return MapTextEditorController(text: $text, onChange: onChange)
+    return MapTextEditorController(document: $document, onChange: onChange)
   }
 
   func updateNSViewController(
   }
 
   func updateNSViewController(
diff --git a/Map/Presentation/Commands/MapCommands.swift b/Map/Presentation/Commands/MapCommands.swift
new file mode 100644 (file)
index 0000000..32b4958
--- /dev/null
@@ -0,0 +1,44 @@
+import SwiftUI
+
+struct MapCommands: Commands {
+
+  @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
+  @AppStorage("zoom") var zoom = 1.0
+
+  var body: some Commands {
+
+    // View
+
+    CommandGroup(after: CommandGroupPlacement.toolbar) {
+      if viewStyle == .horizontal {
+        Button("Use Vertical Layout") {
+          viewStyle = .vertical
+        }.keyboardShortcut(
+          "l", modifiers: EventModifiers([.command])
+        )
+      } else {
+        Button("Use Horizontal Layout") {
+          viewStyle = .horizontal
+        }.keyboardShortcut(
+          "l", modifiers: EventModifiers([.command])
+        )
+      }
+      Divider()
+      Button("Zoom In") {
+        zoom = min(Constants.kMaxZoom, zoom + 0.1)
+      }.keyboardShortcut(
+        "+", modifiers: EventModifiers([.command])
+      )
+      Button("Zoom Out") {
+        zoom = max(Constants.kMinZoom, zoom - 0.1)
+      }.keyboardShortcut(
+        "-", modifiers: EventModifiers([.command])
+      )
+      Divider()
+    }
+
+    CommandGroup(replacing: CommandGroupPlacement.help) {
+      Button("Map Help") { NSWorkspace.shared.open(URL(string: "https://map.tranquil.systems")!) }
+    }
+  }
+}
index b12dabce12393e4de8752055b4939b0adbaac0f4..b256861f6ed8e36eb0a1c30ae8f89bbb5c81c90f 100644 (file)
@@ -5,20 +5,24 @@ import SwiftUI
 
 struct MapRenderView: View {
 
 
 struct MapRenderView: View {
 
-  @Binding var content: String
+  @Binding var document: MapDocument
   @Binding var evolution: StageType
   @Binding var evolution: StageType
-  
+
   var stage: Stage {
     Stage.stages(evolution)
   }
 
   var stage: Stage {
     Stage.stages(evolution)
   }
 
-  @State var parsedMap: ParsedMap = ParsedMap.empty
+  var parsedMap: ParsedMap {
+    MapParser.parse(content: document.text)
+  }
 
 
-  let mapSize = CGSize(width: 1300.0, height: 1000.0)
+  let mapSize = Dimensions.mapSize
+  let padding = Dimensions.mapPadding
 
   let lineWidth = CGFloat(0.5)
   let vertexSize = CGSize(width: 25.0, height: 25.0)
 
   let lineWidth = CGFloat(0.5)
   let vertexSize = CGSize(width: 25.0, height: 25.0)
-  let padding = CGFloat(30.0)
+
+  var onDragVertex: (Vertex, CGFloat, CGFloat) -> Void = { _, _, _ in }
 
   var body: some View {
     ZStack(alignment: .topLeading) {
 
   var body: some View {
     ZStack(alignment: .topLeading) {
@@ -36,28 +40,27 @@ struct MapRenderView: View {
       MapEdges(
         mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges)
       MapBlockers(mapSize: mapSize, vertexSize: vertexSize, blockers: parsedMap.blockers)
       MapEdges(
         mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: parsedMap.edges)
       MapBlockers(mapSize: mapSize, vertexSize: vertexSize, blockers: parsedMap.blockers)
-      MapVertices(mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices)
+      MapVertices(
+        mapSize: mapSize, vertexSize: vertexSize, vertices: parsedMap.vertices,
+        onDragVertex: onDragVertex)
       MapOpportunities(
         mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize,
         opportunities: parsedMap.opportunities)
       MapOpportunities(
         mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize,
         opportunities: parsedMap.opportunities)
+      MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: parsedMap.groups).drawingGroup(
+        opaque: true
+      ).opacity(0.1)
       MapNotes(
         mapSize: mapSize, lineWidth: lineWidth, notes: parsedMap.notes)
       MapNotes(
         mapSize: mapSize, lineWidth: lineWidth, notes: parsedMap.notes)
-    }.frame(
-      width: mapSize.width,
+    }.offset(x: padding, y: padding).frame(
+      width: mapSize.width + 2 * padding,
       height: mapSize.height + 2 * padding, alignment: .topLeading
       height: mapSize.height + 2 * padding, alignment: .topLeading
-    ).onAppear {
-      self.parsedMap = Map.parse(content: content)
-    }.padding(padding).onChange(of: content) { newState in
-      self.parsedMap = Map.parse(content: newState)
-    }
+    )
   }
 }
 
   }
 }
 
-struct MapRenderView_Previews: PreviewProvider {
-  static var previews: some View {
-    MapRenderView(
-      content: Binding.constant(""), evolution: Binding.constant(StageType.general)
-    ).environment(
-      \.managedObjectContext, PersistenceController.preview.container.viewContext)
-  }
+#Preview {
+  MapRenderView(
+    document: Binding.constant(MapDocument(text: "")),
+    evolution: Binding.constant(StageType.general)
+  )
 }
 }
diff --git a/Map/Presentation/EvolutionPicker.swift b/Map/Presentation/EvolutionPicker.swift
new file mode 100644 (file)
index 0000000..0f4c954
--- /dev/null
@@ -0,0 +1,41 @@
+import SwiftUI
+
+struct EvolutionPicker: View {
+
+  @EnvironmentObject private var store: AppStore
+
+  private var selectedEvolution: Binding<StageType> {
+    Binding(
+      get: { store.state.selectedEvolution },
+      set: { evolution in
+        store.send(.selectEvolution(evolution: evolution))
+      }
+    )
+  }
+
+  var body: some View {
+    Picker("Evolution", selection: selectedEvolution) {
+      ForEach(StageType.types) { stage in
+        Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
+      }
+      Divider()
+      ForEach(StageType.characteristics) { stage in
+        Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0).font(.theme.body)
+      }
+      Divider()
+      ForEach(StageType.properties) { stage in
+        Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
+      }
+      Divider()
+      ForEach(StageType.custom) { stage in
+        Text(Stage.title(stage)).font(.theme.body).tag(stage).padding(4.0)
+      }
+    }.font(.theme.body).padding(.horizontal, 8.0).padding(.vertical, 4.0)
+  }
+}
+
+struct EvolutionPicker_Previews: PreviewProvider {
+  static var previews: some View {
+    EvolutionPicker()
+  }
+}
diff --git a/Map/Presentation/MapEditor.swift b/Map/Presentation/MapEditor.swift
new file mode 100644 (file)
index 0000000..bf33f75
--- /dev/null
@@ -0,0 +1,134 @@
+import SwiftUI
+
+struct MapEditor: View {
+  @Binding var document: MapDocument
+  var url: URL?
+  @State var selectedEvolution: StageType = .behavior
+
+  @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
+
+  let zoomRange = Constants.kMinZoom...Constants.kMaxZoom
+  @AppStorage("zoom") var zoom = 1.0
+  @State var lastZoom = 1.0
+
+  var body: some View {
+    VStack(spacing: 0) {
+      adaptiveStack {
+        ZStack(alignment: .topLeading) {
+          MapTextEditor(document: $document)
+            .background(Color.ui.background)
+            .foregroundColor(Color.ui.foreground)
+            .frame(minHeight: 96.0)
+        }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.ui.background).cornerRadius(
+          5.0)
+        GeometryReader { geometry in
+          ScrollView([.horizontal, .vertical]) {
+            MapRenderView(
+              document: $document, evolution: $selectedEvolution, onDragVertex: onDragVertex
+            ).scaleEffect(zoom, anchor: .center).frame(
+              width: (Dimensions.mapSize.width + 2 * Dimensions.mapPadding) * zoom,
+              height: (Dimensions.mapSize.height + 2 * Dimensions.mapPadding) * zoom)
+          }.background(Color.ui.background)
+            .gesture(
+              MagnificationGesture()
+                .onChanged { value in
+                  let delta = value / lastZoom
+                  lastZoom = value
+                  zoom = min(max(zoom * delta, zoomRange.lowerBound), zoomRange.upperBound)
+                }
+                .onEnded { _ in
+                  lastZoom = 1.0
+                }
+            )
+        }
+      }
+      Divider()
+      HStack {
+        Spacer()
+        Slider(
+          value: $zoom, in: zoomRange, step: 0.1,
+          label: {
+            Text(formatZoom(zoom))
+              .font(.theme.smallControl)
+          },
+          minimumValueLabel: {
+            Image(systemName: "minus.magnifyingglass")
+              .font(.theme.smallControl)
+              .help("Zoom Out (⌘-)")
+          },
+          maximumValueLabel: {
+            Image(systemName: "plus.magnifyingglass")
+              .font(.theme.smallControl)
+              .help("Zoom In (⌘+)")
+          }
+        ).frame(width: 200).padding(.trailing, 10.0)
+      }.padding(4.0)
+    }.toolbar {
+      HStack {
+        Button(action: saveImage) {
+          Image(systemName: "photo")
+        }
+        .help("Export Image (⌘E)")
+        .padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0)
+      }
+      EvolutionPicker(selectedEvolution: $selectedEvolution)
+    }
+  }
+
+  @ViewBuilder
+  func adaptiveStack<Content: View>(@ViewBuilder content: () -> Content) -> some View {
+    if viewStyle == .horizontal {
+      VSplitView {
+        content()
+      }
+    } else {
+      HSplitView {
+        content()
+      }
+    }
+  }
+
+  private func formatZoom(_ number: CGFloat) -> String {
+    let formatter = NumberFormatter()
+    formatter.numberStyle = .decimal
+    formatter.maximumFractionDigits = 1
+    formatter.minimumFractionDigits = 1
+    return (formatter.string(from: NSNumber(value: number)) ?? "") + "x"
+  }
+
+  private func onDragVertex(vertex: Vertex, x: CGFloat, y: CGFloat) {
+    print("Dragging: \(vertex), \(x), \(y)")
+  }
+
+  private func saveImage() {
+    if let image = document.exportAsImage(withEvolution: selectedEvolution) {
+
+      let filename = url?.deletingPathExtension().lastPathComponent ?? "Untitled"
+
+      let savePanel = NSSavePanel()
+      savePanel.allowedContentTypes = [.png]
+      savePanel.canCreateDirectories = true
+      savePanel.isExtensionHidden = false
+      savePanel.title = "Save \(filename) as image"
+      savePanel.message = "Choose a location to save the image"
+      savePanel.nameFieldStringValue = "\(filename).png"
+      savePanel.begin { result in
+        if result == .OK, let url = savePanel.url {
+          if let tiffRepresentation = image.tiffRepresentation {
+            let bitmapImage = NSBitmapImageRep(data: tiffRepresentation)
+            let pngData = bitmapImage?.representation(using: .png, properties: [:])
+            do {
+              try pngData?.write(to: url)
+            } catch {
+              return
+            }
+          }
+        }
+      }
+    }
+  }
+}
+
+#Preview {
+  MapEditor(document: .constant(MapDocument()), url: URL(filePath: "test.png")!)
+}
diff --git a/Map/Presentation/Screens/EmptyMapDetailScreen.swift b/Map/Presentation/Screens/EmptyMapDetailScreen.swift
deleted file mode 100644 (file)
index f3c6d75..0000000
+++ /dev/null
@@ -1,16 +0,0 @@
-import CoreData
-import SwiftUI
-
-struct EmptyMapDetailScreen: View {
-
-  var body: some View {
-    Text("Select a map from the left hand side, or click on + to create one.")
-  }
-}
-
-struct DefaultMapView_Previews: PreviewProvider {
-  static var previews: some View {
-    EmptyMapDetailScreen().environment(
-      \.managedObjectContext, PersistenceController.preview.container.viewContext)
-  }
-}
diff --git a/Map/Presentation/Screens/MapDetailScreen.swift b/Map/Presentation/Screens/MapDetailScreen.swift
deleted file mode 100644 (file)
index a5c32fd..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-import Combine
-import CoreData
-import SwiftUI
-
-struct MapDetailScreen: View {
-  @Environment(\.managedObjectContext) private var viewContext
-
-  @EnvironmentObject var store: AppStore
-
-  @ObservedObject var map: Map
-
-  @State var title: String
-  @State var content: String
-
-  var body: some View {
-    if map.uuid != nil {
-      VSplitView {
-        VStack {
-          HStack {
-            TextField(
-                "Title", text: $title, onCommit: saveModel
-            ).font(.title2).textFieldStyle(PlainTextFieldStyle()).padding(.vertical, 4.0).padding(
-                .leading, 4.0)
-            Button(action: saveText) {
-              Image(systemName: "doc.text")
-            }.padding(.vertical, 4.0).padding(.leading, 4.0)
-            Button(action: saveImage) {
-              Image(systemName: "photo")
-            }.padding(.vertical, 4.0).padding(.leading, 4.0).padding(.trailing, 8.0)
-          }
-          EvolutionPicker()
-
-          ZStack(alignment: .topLeading) {
-            MapTextEditor(text: $content, onChange: saveModel)
-              .background(Color.ui.background)
-              .foregroundColor(Color.ui.foreground)
-              .frame(minHeight: 96.0)
-          }.padding(.top, 8.0).padding(.leading, 8.0).background(Color.ui.background).cornerRadius(
-            5.0)
-        }.padding(.horizontal, 8.0)
-        ScrollView([.horizontal, .vertical]) {
-          MapRenderView(content: $content, evolution:  .constant(store.state.selectedEvolution))
-        }
-      }.onDisappear {
-        saveModel()
-      }
-    } else {
-      EmptyMapDetailScreen()
-    }
-  }
-
-  private func saveModel() {
-    map.content = content
-    map.title = title
-    try? viewContext.save()
-  }
-
-  private func saveText() {
-    store.send(.exportMapAsText(map: map))
-  }
-
-  private func saveImage() {
-    store.send(.exportMapAsImage(map: map))
-  }
-}
-
-struct MapDetailView_Previews: PreviewProvider {
-  static var previews: some View {
-    MapDetailScreen(map: Map(), title: "", content: "").environment(
-      \.managedObjectContext, PersistenceController.preview.container.viewContext)
-  }
-}
similarity index 66%
rename from Map/Core Extensions/Color+Theme.swift
rename to Map/Presentation/Theme/Color+theme.swift
index ffbd224f0b97379f175e6beb29e00cbe73b4f2db..1bd9b8e85da0e075f9ab38b1510076a0cb3f8483 100644 (file)
@@ -5,11 +5,15 @@ extension Color {
     static let darkSlate = Color("Dark Slate")
     static let jasperRed = Color("Jasper Red")
     static let olympicBlue = Color("Olympic Blue")
     static let darkSlate = Color("Dark Slate")
     static let jasperRed = Color("Jasper Red")
     static let olympicBlue = Color("Olympic Blue")
+    static let lightPorcelainGreen = Color("Light Porcelain Green")
+    static let naplesYellow = Color("Naples Yellow")
+    static let hermosaPink = Color("Hermosa Pink")
     static let neutralGray = Color("Neutral Gray")
     static let lightNeutralGray = Color("Light Neutral Gray")
     static let darkNeutralGray = Color("Dark Neutral Gray")
     static let neutralGray = Color("Neutral Gray")
     static let lightNeutralGray = Color("Light Neutral Gray")
     static let darkNeutralGray = Color("Dark Neutral Gray")
+    static let darkerNeutralGray = Color("Darker Neutral Gray")
   }
   }
-  
+
   struct map {
     static let labelColor = Color.theme.darkSlate
     static let axisColor = Color.theme.darkSlate
   struct map {
     static let labelColor = Color.theme.darkSlate
     static let axisColor = Color.theme.darkSlate
@@ -18,8 +22,15 @@ extension Color {
     static let opportunityColor = Color.theme.olympicBlue
     static let stageForeground = Color.theme.lightNeutralGray
     static let stageBackground = Color.white
     static let opportunityColor = Color.theme.olympicBlue
     static let stageForeground = Color.theme.lightNeutralGray
     static let stageBackground = Color.white
+    static let groupColors = [
+      Color.theme.olympicBlue,
+      Color.theme.jasperRed,
+      Color.theme.lightPorcelainGreen,
+      Color.theme.naplesYellow,
+      Color.theme.hermosaPink,
+    ]
   }
   }
-  
+
   struct ui {
     static let foreground = Color("Foreground")
     static let background = Color("Background")
   struct ui {
     static let foreground = Color("Foreground")
     static let background = Color("Background")
diff --git a/Map/Presentation/Theme/Dimensions.swift b/Map/Presentation/Theme/Dimensions.swift
new file mode 100644 (file)
index 0000000..6a58d58
--- /dev/null
@@ -0,0 +1,6 @@
+import Foundation
+
+struct Dimensions {
+  static let mapSize = CGSize(width: 1300.0, height: 1000.0)
+  static let mapPadding: CGFloat = 42.0
+}
diff --git a/Map/Presentation/Theme/Font+theme.swift b/Map/Presentation/Theme/Font+theme.swift
new file mode 100644 (file)
index 0000000..07e3b9b
--- /dev/null
@@ -0,0 +1,15 @@
+import SwiftUI
+
+extension Font {
+  public struct theme {
+
+    // Map
+    static let note = Font.system(size: 12, design: .serif)
+    static let axisLabel = Font.system(size: 14, design: .serif)
+    static let vertexLabel = Font.system(size: 12, design: .serif)
+
+    // UI
+    static let smallControl = Font.system(size: 9)
+    static let body = Font.system(size: 13)
+  }
+}
similarity index 75%
rename from Map/Core Extensions/NSColor+Theme.swift
rename to Map/Presentation/Theme/NSColor+theme.swift
index 0ef1094b5a1465dbb3b6d5626e8b33c412f31efb..d30332a0efa39849330c1881e53eaaf4f638899a 100644 (file)
@@ -7,4 +7,8 @@ extension NSColor {
     static let option = NSColor(named: "Option") ?? .textColor
     static let symbol = NSColor(named: "Symbol") ?? .textColor
   }
     static let option = NSColor(named: "Option") ?? .textColor
     static let symbol = NSColor(named: "Symbol") ?? .textColor
   }
+
+  struct ui {
+    static let background = NSColor(named: "Background") ?? .windowBackgroundColor
+  }
 }
 }
diff --git a/Map/Presentation/ViewStyle.swift b/Map/Presentation/ViewStyle.swift
new file mode 100644 (file)
index 0000000..cea3113
--- /dev/null
@@ -0,0 +1,3 @@
+enum ViewStyle: String {
+  case vertical, horizontal
+}
diff --git a/Map/Presentation/Windows/MapEditorWindow.swift b/Map/Presentation/Windows/MapEditorWindow.swift
deleted file mode 100644 (file)
index 65316cc..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-import CoreData
-import SwiftUI
-
-struct MapEditorWindow: View {
-  @Environment(\.managedObjectContext) private var viewContext
-
-  @EnvironmentObject var store: AppStore
-
-  @FetchRequest(
-    sortDescriptors: [NSSortDescriptor(keyPath: \Map.createdAt, ascending: true)],
-    animation: .default)
-  private var maps: FetchedResults<Map>
-
-  var body: some View {
-    NavigationView {
-      List {
-        if maps.count == 0 {
-          EmptyMapDetailScreen()
-        }
-        ForEach(maps) { map in
-          NavigationLink(
-            destination: MapDetailScreen(map: map, title: map.title ?? "", content: map.content ?? "")
-          ) {
-            HStack {
-              Text(map.title ?? "Untitled Map")
-              Spacer()
-              Text(mapFormatter.string(from: (map.createdAt ?? Date())))
-                .font(.caption)
-                .padding(.vertical, 2.0)
-                .padding(.horizontal, 4.0)
-                .background(Color.accentColor)
-                .foregroundColor(Color.black)
-                .cornerRadius(2.0)
-            }.padding(.leading, 8.0)
-          }.contextMenu {
-            Button(
-              action: { store.send(.deleteMap(map: map)) },
-              label: {
-                Image(systemName: "trash")
-                Text("Delete")
-              })
-          }
-        }
-        .onDelete(perform: deleteMaps)
-      }.frame(minWidth: 250.0, alignment: .leading)
-        .toolbar {
-          HStack {
-            Button(action: toggleSidebar) {
-              Label("Toggle Sidebar", systemImage: "sidebar.left")
-            }
-            Button(action: addMap) {
-              Label("Add Map", systemImage: "plus")
-            }
-          }
-        }
-      EmptyMapDetailScreen()
-    }
-  }
-
-  private func toggleSidebar() {
-    NSApp.keyWindow?.firstResponder?.tryToPerform(
-      #selector(NSSplitViewController.toggleSidebar(_:)), with: nil)
-  }
-
-  private func addMap() {
-    withAnimation {
-      let newMap = Map(context: viewContext)
-      newMap.uuid = UUID()
-      newMap.createdAt = Date()
-      newMap.title = "Map \(newMap.createdAt!.format())"
-      newMap.content = ""
-
-      do {
-        try viewContext.save()
-      } catch {
-        let nsError = error as NSError
-        fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
-      }
-    }
-  }
-
-  private func deleteMaps(offsets: IndexSet) {
-
-    withAnimation {
-      offsets.map { maps[$0] }.forEach(viewContext.delete)
-
-      do {
-        try viewContext.save()
-      } catch {
-        let nsError = error as NSError
-        fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
-      }
-    }
-  }
-}
-
-private let mapFormatter: DateFormatter = {
-  let formatter = DateFormatter()
-  formatter.dateStyle = .short
-  formatter.timeStyle = .none
-  return formatter
-}()
-
-struct ContentView_Previews: PreviewProvider {
-  static var previews: some View {
-    MapEditorWindow().environment(
-      \.managedObjectContext, PersistenceController.preview.container.viewContext)
-  }
-}
diff --git a/MapTests/Info.plist b/MapTests/Info.plist
deleted file mode 100644 (file)
index 64d65ca..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-       <key>CFBundleDevelopmentRegion</key>
-       <string>$(DEVELOPMENT_LANGUAGE)</string>
-       <key>CFBundleExecutable</key>
-       <string>$(EXECUTABLE_NAME)</string>
-       <key>CFBundleIdentifier</key>
-       <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
-       <key>CFBundleInfoDictionaryVersion</key>
-       <string>6.0</string>
-       <key>CFBundleName</key>
-       <string>$(PRODUCT_NAME)</string>
-       <key>CFBundlePackageType</key>
-       <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
-       <key>CFBundleShortVersionString</key>
-       <string>1.0</string>
-       <key>CFBundleVersion</key>
-       <string>1</string>
-</dict>
-</plist>
index e425c00460dcf0b81fd4402da091e4d62359fe9c..5a8f7815c31562b315c9e19dd72d7a3ab0d64fa8 100644 (file)
@@ -1,34 +1,11 @@
-//
-//  MapTests.swift
-//  MapTests
-//
-//  Created by Ruben Beltran del Rio on 2/1/21.
-//
-
-import XCTest
+import Testing
 
 @testable import Map
 
 
 @testable import Map
 
-class MapTests: XCTestCase {
-
-  override func setUpWithError() throws {
-    // Put setup code here. This method is called before the invocation of each test method in the class.
-  }
-
-  override func tearDownWithError() throws {
-    // Put teardown code here. This method is called after the invocation of each test method in the class.
-  }
-
-  func testExample() throws {
-    // This is an example of a functional test case.
-    // Use XCTAssert and related functions to verify your tests produce the correct results.
-  }
+struct MapTests {
 
 
-  func testPerformanceExample() throws {
-    // This is an example of a performance test case.
-    self.measure {
-      // Put the code you want to measure the time of here.
-    }
+  @Test func testExample() async throws {
+    // Write your test here and use APIs like `#expect(...)` to check expected conditions.
   }
 
 }
   }
 
 }
diff --git a/MapUITests/Info.plist b/MapUITests/Info.plist
deleted file mode 100644 (file)
index 64d65ca..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
-<plist version="1.0">
-<dict>
-       <key>CFBundleDevelopmentRegion</key>
-       <string>$(DEVELOPMENT_LANGUAGE)</string>
-       <key>CFBundleExecutable</key>
-       <string>$(EXECUTABLE_NAME)</string>
-       <key>CFBundleIdentifier</key>
-       <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
-       <key>CFBundleInfoDictionaryVersion</key>
-       <string>6.0</string>
-       <key>CFBundleName</key>
-       <string>$(PRODUCT_NAME)</string>
-       <key>CFBundlePackageType</key>
-       <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
-       <key>CFBundleShortVersionString</key>
-       <string>1.0</string>
-       <key>CFBundleVersion</key>
-       <string>1</string>
-</dict>
-</plist>
index d2d523d4ccd8ccc2cb30e7be5dedebd4a260a108..2f6c35d7fe5ea1699516f1a1d885b10a89fc5f18 100644 (file)
@@ -1,13 +1,6 @@
-//
-//  MapUITests.swift
-//  MapUITests
-//
-//  Created by Ruben Beltran del Rio on 2/1/21.
-//
-
 import XCTest
 
 import XCTest
 
-class MapUITests: XCTestCase {
+final class MapUITests: XCTestCase {
 
   override func setUpWithError() throws {
     // Put setup code here. This method is called before the invocation of each test method in the class.
 
   override func setUpWithError() throws {
     // Put setup code here. This method is called before the invocation of each test method in the class.
@@ -22,17 +15,18 @@ class MapUITests: XCTestCase {
     // Put teardown code here. This method is called after the invocation of each test method in the class.
   }
 
     // Put teardown code here. This method is called after the invocation of each test method in the class.
   }
 
+  @MainActor
   func testExample() throws {
     // UI tests must launch the application that they test.
     let app = XCUIApplication()
     app.launch()
 
   func testExample() throws {
     // UI tests must launch the application that they test.
     let app = XCUIApplication()
     app.launch()
 
-    // Use recording to get started writing UI tests.
     // Use XCTAssert and related functions to verify your tests produce the correct results.
   }
 
     // Use XCTAssert and related functions to verify your tests produce the correct results.
   }
 
+  @MainActor
   func testLaunchPerformance() throws {
   func testLaunchPerformance() throws {
-    if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
+    if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
       // This measures how long it takes to launch your application.
       measure(metrics: [XCTApplicationLaunchMetric()]) {
         XCUIApplication().launch()
       // This measures how long it takes to launch your application.
       measure(metrics: [XCTApplicationLaunchMetric()]) {
         XCUIApplication().launch()
diff --git a/MapUITests/MapUITestsLaunchTests.swift b/MapUITests/MapUITestsLaunchTests.swift
new file mode 100644 (file)
index 0000000..cdcd1b9
--- /dev/null
@@ -0,0 +1,26 @@
+import XCTest
+
+final class MapUITestsLaunchTests: XCTestCase {
+
+  override class var runsForEachTargetApplicationUIConfiguration: Bool {
+    true
+  }
+
+  override func setUpWithError() throws {
+    continueAfterFailure = false
+  }
+
+  @MainActor
+  func testLaunch() throws {
+    let app = XCUIApplication()
+    app.launch()
+
+    // Insert steps here to perform after app launch but before taking a screenshot,
+    // such as logging into a test account or navigating somewhere in the app
+
+    let attachment = XCTAttachment(screenshot: app.screenshot())
+    attachment.name = "Launch Screen"
+    attachment.lifetime = .keepAlways
+    add(attachment)
+  }
+}