// Copyright (C) 2024 Rubén Beltrán del Río // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program. If not, see https://map.tranquil.systems. import Combine import CoreData import CoreGraphics import SwiftUI import WmapParser struct MapRenderView: View { var parsedMap: WmapParser.Map @Binding var evolution: StageType @AppStorage("showMapBackground") var showMapBackground: Bool = UserPreferences.Defaults .showMapBackground @AppStorage("useCustomFont") var useCustomFont: Bool = UserPreferences.Defaults.useCustomFont @AppStorage("customFontName") var customFontName: String = UserPreferences.Defaults.customFontName @AppStorage("useSmartLabelPositioning") private var useSmartLabelPositioning = UserPreferences .Defaults.useSmartLabelPositioning var stage: Stage { Stage.stages(evolution) } @State private var renderableMap: RenderableMap = .empty @State private var smartLabelPositions: [Int: CGPoint] = [:] @State private var parseTask: Task? let mapSize = Dimensions.Map.size let padding = Dimensions.Map.padding let lineWidth = CGFloat(0.5) let vertexSize = CGSize(width: 25.0, height: 25.0) var onDragVertex: (Vertex, CGFloat, CGFloat) -> Void = { _, _, _ in } private func convertEntitiesInBackground() { parseTask?.cancel() parseTask = Task { let parsed = await Task.detached(priority: .userInitiated) { RenderableMap.from(parsedMap) }.value let positions = await Task.detached(priority: .userInitiated) { var positions: [Int: CGPoint] = [:] guard await useSmartLabelPositioning else { return positions } let axisLines = [ Line(start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: mapSize.height)), Line( start: CGPoint(x: 0, y: mapSize.height), end: CGPoint(x: mapSize.width, y: mapSize.height)), ] for vertex in parsed.vertices { positions[vertex.id] = SmartLabelPositioning.calculateOptimalLabelPosition( for: vertex, mapSize: mapSize, vertexSize: vertexSize, edges: parsed.edges, opportunities: parsed.opportunities, inertias: parsed.inertias, axisLines: axisLines, allVertices: parsed.vertices, notes: parsed.notes ) } return positions }.value if !Task.isCancelled { await MainActor.run { renderableMap = parsed smartLabelPositions = positions } } } } var body: some View { ZStack(alignment: .topLeading) { Path { path in path.addRect( CGRect( x: -padding, y: -padding, width: mapSize.width + padding * 2, height: mapSize.height + padding * 4)) }.fill(.white) // The order Matters. Think of this as layers. if showMapBackground { MapBackground(mapSize: mapSize, stages: renderableMap.stages) } MapAxes( mapSize: mapSize, lineWidth: lineWidth, evolution: stage, stages: renderableMap.stages) Group { MapStages(mapSize: mapSize, lineWidth: lineWidth, stages: renderableMap.stages) MapEdges( mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, edges: renderableMap.edges) MapOpportunities( mapSize: mapSize, lineWidth: lineWidth, vertexSize: vertexSize, opportunities: renderableMap.opportunities) MapIntertias(mapSize: mapSize, vertexSize: vertexSize, inertias: renderableMap.inertias) }.mask { MapMask( mapSize: mapSize, vertexSize: vertexSize, vertices: renderableMap.vertices, labelPositions: smartLabelPositions) } MapVertices( mapSize: mapSize, vertexSize: vertexSize, vertices: renderableMap.vertices, labelPositions: smartLabelPositions, onDragVertex: onDragVertex) if #available(macOS 26.0, *) { MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: renderableMap.groups) .drawingGroup() .opacity(0.1) } else { MapGroups(mapSize: mapSize, vertexSize: vertexSize, groups: renderableMap.groups).drawingGroup( opaque: true ).opacity(0.1) } MapNotes( mapSize: mapSize, lineWidth: lineWidth, notes: renderableMap.notes) } .id("\(useCustomFont)-\(customFontName)") .offset(x: padding, y: padding).frame( width: mapSize.width + 2 * padding, height: mapSize.height + 2 * padding, alignment: .topLeading ) .onAppear { convertEntitiesInBackground() } .onChange(of: parsedMap) { _, _ in convertEntitiesInBackground() } .onChange(of: useSmartLabelPositioning) { convertEntitiesInBackground() } .onDisappear { parseTask?.cancel() } } } #Preview { MapRenderView( parsedMap: WmapParser.parse(""), evolution: Binding.constant(StageType.general) ) }