aboutsummaryrefslogtreecommitdiff
path: root/Map
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2024-09-16 11:10:32 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2024-09-16 11:10:32 +0200
commite2c37ac1dd2ad562e3d619d39b72a174a2212b67 (patch)
treed4da2c0a12db0473eb4030014a92045eebe38834 /Map
parentfdb4633d3e9158e457d57e820df6e1efb4df39c2 (diff)
Map 3 first commit: files, groups and layout
Diffstat (limited to 'Map')
-rw-r--r--Map/Assets.xcassets/Colors/Theme/Darker Neutral Gray.colorset/Contents.json20
-rw-r--r--Map/Assets.xcassets/Colors/Theme/Hermosa Pink.colorset/Contents.json20
-rw-r--r--Map/Assets.xcassets/Colors/Theme/Light Porcelain Green.colorset/Contents.json20
-rw-r--r--Map/Assets.xcassets/Colors/Theme/Naples Yellow.colorset/Contents.json20
-rw-r--r--Map/Core Extensions/Date+format.swift17
-rw-r--r--Map/Core Extensions/Font+Theme.swift8
-rw-r--r--Map/Core Extensions/NSImage+writePNG.swift25
-rw-r--r--Map/Data/AppState.swift103
-rw-r--r--Map/Data/MapDocument.swift42
-rw-r--r--Map/Data/Models/Map+parse.swift29
-rw-r--r--Map/Data/Persistence.swift42
-rw-r--r--Map/Data/Store.swift18
-rw-r--r--Map/Info.plist57
-rw-r--r--Map/Logic/Constants.swift4
-rw-r--r--Map/Logic/MapParser/MapParser.swift72
-rw-r--r--Map/Logic/MapParser/Strategies/GroupParserStrategy.swift34
-rw-r--r--Map/Map.entitlements18
-rw-r--r--Map/Map.xcdatamodeld/.xccurrentversion8
-rw-r--r--Map/Map.xcdatamodeld/Map.xcdatamodel/contents12
-rw-r--r--Map/MapApp.swift19
-rw-r--r--Map/Presentation/Base Components/EvolutionPicker.swift30
-rw-r--r--Map/Presentation/Base Components/MapRender/MapAxes.swift24
-rw-r--r--Map/Presentation/Base Components/MapRender/MapBlockers.swift16
-rw-r--r--Map/Presentation/Base Components/MapRender/MapEdges.swift20
-rw-r--r--Map/Presentation/Base Components/MapRender/MapGroup.swift83
-rw-r--r--Map/Presentation/Base Components/MapRender/MapGroups.swift32
-rw-r--r--Map/Presentation/Base Components/MapRender/MapNotes.swift22
-rw-r--r--Map/Presentation/Base Components/MapRender/MapOpportunities.swift19
-rw-r--r--Map/Presentation/Base Components/MapRender/MapStages.swift49
-rw-r--r--Map/Presentation/Base Components/MapRender/MapVertices.swift41
-rw-r--r--Map/Presentation/Base Components/MapTextEditor.swift89
-rw-r--r--Map/Presentation/Commands/MapCommands.swift44
-rw-r--r--Map/Presentation/Complex Components/MapRender/MapRenderView.swift43
-rw-r--r--Map/Presentation/EvolutionPicker.swift41
-rw-r--r--Map/Presentation/MapEditor.swift134
-rw-r--r--Map/Presentation/Screens/EmptyMapDetailScreen.swift16
-rw-r--r--Map/Presentation/Screens/MapDetailScreen.swift72
-rw-r--r--Map/Presentation/Theme/Color+theme.swift (renamed from Map/Core Extensions/Color+Theme.swift)15
-rw-r--r--Map/Presentation/Theme/Dimensions.swift6
-rw-r--r--Map/Presentation/Theme/Font+theme.swift15
-rw-r--r--Map/Presentation/Theme/NSColor+theme.swift (renamed from Map/Core Extensions/NSColor+Theme.swift)4
-rw-r--r--Map/Presentation/ViewStyle.swift3
-rw-r--r--Map/Presentation/Windows/MapEditorWindow.swift109
43 files changed, 833 insertions, 682 deletions
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
index 0000000..e890acd
--- /dev/null
+++ b/Map/Assets.xcassets/Colors/Theme/Darker Neutral Gray.colorset/Contents.json
@@ -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
index 0000000..b90aa6c
--- /dev/null
+++ b/Map/Assets.xcassets/Colors/Theme/Hermosa Pink.colorset/Contents.json
@@ -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
index 0000000..13a53a1
--- /dev/null
+++ b/Map/Assets.xcassets/Colors/Theme/Light Porcelain Green.colorset/Contents.json
@@ -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
index 0000000..1c11b82
--- /dev/null
+++ b/Map/Assets.xcassets/Colors/Theme/Naples Yellow.colorset/Contents.json
@@ -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
index d29ed0f..0000000
--- a/Map/Core Extensions/Date+format.swift
+++ /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
index 30bc6f9..0000000
--- a/Map/Core Extensions/Font+Theme.swift
+++ /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
index c24c0cf..0000000
--- a/Map/Core Extensions/NSImage+writePNG.swift
+++ /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
index d0d5670..0000000
--- a/Map/Data/AppState.swift
+++ /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
index 0000000..9340684
--- /dev/null
+++ b/Map/Data/MapDocument.swift
@@ -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
index 5181daf..0000000
--- a/Map/Data/Models/Map+parse.swift
+++ /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
index 1eb09e8..0000000
--- a/Map/Data/Persistence.swift
+++ /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
index 7860f33..0000000
--- a/Map/Data/Store.swift
+++ /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
diff --git a/Map/Info.plist b/Map/Info.plist
index 6ba8cf0..61150cf 100644
--- a/Map/Info.plist
+++ b/Map/Info.plist
@@ -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>
- <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>
diff --git a/Map/Logic/Constants.swift b/Map/Logic/Constants.swift
new file mode 100644
index 0000000..8cb95e5
--- /dev/null
+++ b/Map/Logic/Constants.swift
@@ -0,0 +1,4 @@
+struct Constants {
+ static let kMaxZoom = 2.0
+ static let kMinZoom = 0.1
+}
diff --git a/Map/Logic/MapParser/MapParser.swift b/Map/Logic/MapParser/MapParser.swift
index 5f78d5d..1a66f9a 100644
--- a/Map/Logic/MapParser/MapParser.swift
+++ b/Map/Logic/MapParser/MapParser.swift
@@ -1,25 +1,60 @@
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:
- "([^\\(\\[\\]]*?)[\\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(
- pattern: "(.+?)[\\s]*-([->])[\\s]*(.+)", options: .caseInsensitive)
+ pattern: "^(.+?)[\\s]*-([->])[\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines])
static let blocker = try! NSRegularExpression(
- pattern: "\\[(Blocker)\\][\\s]*(.+)", options: .caseInsensitive)
+ pattern: "^\\[(Blocker)\\][\\s]*(.+)", options: [.caseInsensitive, .anchorsMatchLines])
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(
- 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(
- 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 {
@@ -29,12 +64,14 @@ struct ParsedMap {
let opportunities: [Opportunity]
let notes: [Note]
let stages: [CGFloat]
+ let groups: [[Vertex]]
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
@@ -115,6 +152,7 @@ class MapBuilder {
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 {
@@ -136,7 +174,7 @@ class MapBuilder {
let opportunity = object as! Opportunity
opportunities.append(opportunity)
}
-
+
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
}
+
+ 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(
- 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
index 0000000..7924935
--- /dev/null
+++ b/Map/Logic/MapParser/Strategies/GroupParserStrategy.swift
@@ -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())
+ }
+}
diff --git a/Map/Map.entitlements b/Map/Map.entitlements
index 6060301..6d968ed 100644
--- a/Map/Map.entitlements
+++ b/Map/Map.entitlements
@@ -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>
- <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>
diff --git a/Map/Map.xcdatamodeld/.xccurrentversion b/Map/Map.xcdatamodeld/.xccurrentversion
deleted file mode 100644
index ee72e60..0000000
--- a/Map/Map.xcdatamodeld/.xccurrentversion
+++ /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
index ea8b276..0000000
--- a/Map/Map.xcdatamodeld/Map.xcdatamodel/contents
+++ /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
diff --git a/Map/MapApp.swift b/Map/MapApp.swift
index 14e5605..9aff010 100644
--- a/Map/MapApp.swift
+++ b/Map/MapApp.swift
@@ -1,23 +1,12 @@
-//
-// MapApp.swift
-// Map
-//
-// Created by Ruben Beltran del Rio on 2/1/21.
-//
-
import SwiftUI
@main
struct MapApp: App {
- let persistenceController = PersistenceController.shared
-
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()
}
}
}
diff --git a/Map/Presentation/Base Components/EvolutionPicker.swift b/Map/Presentation/Base Components/EvolutionPicker.swift
index c68e90c..c30798a 100644
--- a/Map/Presentation/Base Components/EvolutionPicker.swift
+++ b/Map/Presentation/Base Components/EvolutionPicker.swift
@@ -2,40 +2,30 @@ 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))
- }
- )
- }
+ @Binding var selectedEvolution: StageType
var body: some View {
- Picker("Evolution", selection: selectedEvolution) {
+ Picker("Evolution", selection: $selectedEvolution) {
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
- 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
- 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
- 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))
}
diff --git a/Map/Presentation/Base Components/MapRender/MapAxes.swift b/Map/Presentation/Base Components/MapRender/MapAxes.swift
index 6ba758f..1b9c226 100644
--- a/Map/Presentation/Base Components/MapRender/MapAxes.swift
+++ b/Map/Presentation/Base Components/MapRender/MapAxes.swift
@@ -22,10 +22,14 @@ struct MapAxes: View {
}.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
@@ -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)
}
diff --git a/Map/Presentation/Base Components/MapRender/MapBlockers.swift b/Map/Presentation/Base Components/MapRender/MapBlockers.swift
index d943ba8..efd84fa 100644
--- a/Map/Presentation/Base Components/MapRender/MapBlockers.swift
+++ b/Map/Presentation/Base Components/MapRender/MapBlockers.swift
@@ -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)),
+ ])
}
diff --git a/Map/Presentation/Base Components/MapRender/MapEdges.swift b/Map/Presentation/Base Components/MapRender/MapEdges.swift
index 3814495..2af7089 100644
--- a/Map/Presentation/Base Components/MapRender/MapEdges.swift
+++ b/Map/Presentation/Base Components/MapRender/MapEdges.swift
@@ -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
index 0000000..9df3338
--- /dev/null
+++ b/Map/Presentation/Base Components/MapRender/MapGroup.swift
@@ -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
index 0000000..84f7fb2
--- /dev/null
+++ b/Map/Presentation/Base Components/MapRender/MapGroups.swift
@@ -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),
+ ]
+ ])
+}
diff --git a/Map/Presentation/Base Components/MapRender/MapNotes.swift b/Map/Presentation/Base Components/MapRender/MapNotes.swift
index f35b3fe..bbb1aba 100644
--- a/Map/Presentation/Base Components/MapRender/MapNotes.swift
+++ b/Map/Presentation/Base Components/MapRender/MapNotes.swift
@@ -5,13 +5,12 @@ struct MapNotes: View {
let mapSize: CGSize
let lineWidth: CGFloat
let notes: [Note]
-
+
let maxWidth = 400.0
-
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)
@@ -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")
+ ])
}
diff --git a/Map/Presentation/Base Components/MapRender/MapOpportunities.swift b/Map/Presentation/Base Components/MapRender/MapOpportunities.swift
index 7fcadff..b3051ee 100644
--- a/Map/Presentation/Base Components/MapRender/MapOpportunities.swift
+++ b/Map/Presentation/Base Components/MapRender/MapOpportunities.swift
@@ -46,7 +46,8 @@ struct MapOpportunities: View {
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))
+ ])
}
diff --git a/Map/Presentation/Base Components/MapRender/MapStages.swift b/Map/Presentation/Base Components/MapRender/MapStages.swift
index 0fc8f48..fc3bfa1 100644
--- a/Map/Presentation/Base Components/MapRender/MapStages.swift
+++ b/Map/Presentation/Base Components/MapRender/MapStages.swift
@@ -1,5 +1,5 @@
-import SwiftUI
import Patterns
+import SwiftUI
struct MapStages: View {
@@ -10,17 +10,29 @@ struct MapStages: View {
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))
@@ -34,7 +46,8 @@ struct MapStages: View {
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])
}
diff --git a/Map/Presentation/Base Components/MapRender/MapVertices.swift b/Map/Presentation/Base Components/MapRender/MapVertices.swift
index 74cac6d..5bc0a96 100644
--- a/Map/Presentation/Base Components/MapRender/MapVertices.swift
+++ b/Map/Presentation/Base Components/MapRender/MapVertices.swift
@@ -7,18 +7,29 @@ struct MapVertices: View {
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
- 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(
- 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),
+ ])
}
diff --git a/Map/Presentation/Base Components/MapTextEditor.swift b/Map/Presentation/Base Components/MapTextEditor.swift
index f3838a6..d7aa9f8 100644
--- a/Map/Presentation/Base Components/MapTextEditor.swift
+++ b/Map/Presentation/Base Components/MapTextEditor.swift
@@ -3,7 +3,7 @@ import SwiftUI
class MapTextEditorController: NSViewController {
- @Binding var text: String
+ @Binding var document: MapDocument
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 groupRegex = MapParsingPatterns.group
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)
}
@@ -31,10 +32,11 @@ class MapTextEditorController: NSViewController {
scrollView.translatesAutoresizingMaskIntoConstraints = false
+ textView.backgroundColor = .ui.background
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
@@ -49,14 +51,13 @@ extension MapTextEditorController: NSTextViewDelegate {
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 {
- 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 {
- 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))
- 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 {
- 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 {
- 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 {
- 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 {
- 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 {
- @Binding var text: String
+ @Binding var document: MapDocument
var onChange: () -> Void = {}
func makeNSViewController(
context: NSViewControllerRepresentableContext<MapTextEditor>
) -> MapTextEditorController {
- return MapTextEditorController(text: $text, onChange: onChange)
+ return MapTextEditorController(document: $document, onChange: onChange)
}
func updateNSViewController(
diff --git a/Map/Presentation/Commands/MapCommands.swift b/Map/Presentation/Commands/MapCommands.swift
new file mode 100644
index 0000000..32b4958
--- /dev/null
+++ b/Map/Presentation/Commands/MapCommands.swift
@@ -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")!) }
+ }
+ }
+}
diff --git a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
index b12dabc..b256861 100644
--- a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
+++ b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
@@ -5,20 +5,24 @@ import SwiftUI
struct MapRenderView: View {
- @Binding var content: String
+ @Binding var document: MapDocument
@Binding var evolution: StageType
-
+
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 padding = CGFloat(30.0)
+
+ var onDragVertex: (Vertex, CGFloat, CGFloat) -> Void = { _, _, _ in }
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)
- 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)
+ MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: parsedMap.groups).drawingGroup(
+ opaque: true
+ ).opacity(0.1)
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
- ).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
index 0000000..0f4c954
--- /dev/null
+++ b/Map/Presentation/EvolutionPicker.swift
@@ -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
index 0000000..bf33f75
--- /dev/null
+++ b/Map/Presentation/MapEditor.swift
@@ -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
index f3c6d75..0000000
--- a/Map/Presentation/Screens/EmptyMapDetailScreen.swift
+++ /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
index a5c32fd..0000000
--- a/Map/Presentation/Screens/MapDetailScreen.swift
+++ /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)
- }
-}
diff --git a/Map/Core Extensions/Color+Theme.swift b/Map/Presentation/Theme/Color+theme.swift
index ffbd224..1bd9b8e 100644
--- a/Map/Core Extensions/Color+Theme.swift
+++ b/Map/Presentation/Theme/Color+theme.swift
@@ -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 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 darkerNeutralGray = Color("Darker Neutral Gray")
}
-
+
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 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")
diff --git a/Map/Presentation/Theme/Dimensions.swift b/Map/Presentation/Theme/Dimensions.swift
new file mode 100644
index 0000000..6a58d58
--- /dev/null
+++ b/Map/Presentation/Theme/Dimensions.swift
@@ -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
index 0000000..07e3b9b
--- /dev/null
+++ b/Map/Presentation/Theme/Font+theme.swift
@@ -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)
+ }
+}
diff --git a/Map/Core Extensions/NSColor+Theme.swift b/Map/Presentation/Theme/NSColor+theme.swift
index 0ef1094..d30332a 100644
--- a/Map/Core Extensions/NSColor+Theme.swift
+++ b/Map/Presentation/Theme/NSColor+theme.swift
@@ -7,4 +7,8 @@ extension NSColor {
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
index 0000000..cea3113
--- /dev/null
+++ b/Map/Presentation/ViewStyle.swift
@@ -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
index 65316cc..0000000
--- a/Map/Presentation/Windows/MapEditorWindow.swift
+++ /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)
- }
-}