diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Lattice.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Lattice.swift index 1a088fb..e0d223b 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Lattice.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Lattice.swift @@ -34,7 +34,7 @@ struct Lattice: View { var body: some View { ForceDirectedGraph($isRunning) { - Repeated(0..<(width*width)) { i in + Series(0..<(width*width)) { i in let _i = Double(i / width) / Double(width) let _j = Double(i % width) / Double(width) NodeMark(id: i, radius: 3.0) @@ -42,7 +42,7 @@ struct Lattice: View { .stroke() } - Repeated(edge) { + Series(edge) { LinkMark(from: $0.0, to: $0.1) } diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift index 6f4fd10..e6358a5 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift @@ -42,6 +42,7 @@ let mermaidLinkRegex = Regex { "<—" "->" "<-" + "→" } OneOrMore(.whitespace) @@ -73,31 +74,79 @@ func getInitialPosition(id: String, r: Double) -> SIMD2 { struct MermaidVisualization: View { @State private var text: String = """ - Alice --> Bob - Bob --> Cindy - Alice --> Dan - Alice --> Cindy - Tom --> Bob - Tom --> Kate - Kate --> Cindy - + Alice → Bob + Bob → Cindy + Cindy → David + David → Emily + Emily → Frank + Frank → Grace + Grace → Henry + Henry → Isabella + Isabella → Jack + Jack → Karen + Karen → Liam + Liam → Monica + Monica → Nathan + Nathan → Olivia + Olivia → Peter + Peter → Quinn + Quinn → Rachel + Rachel → Steve + Steve → Tiffany + Tiffany → Umar + Umar → Violet + Violet → William + William → Xavier + Xavier → Yolanda + Yolanda → Zack + Zack → Alice + Jack -> Rachel + Xavier -> José + José -> アキラ + アキラ -> Liam """ + @State private var tappedNode: String? = nil + + @ViewBuilder + func getLabel(_ text: String) -> some View { + + let accentColor = colors[Int(UInt(truncatingIfNeeded: text.hashValue) % UInt(colors.count))] + + Text(text) + .font(.caption) + .foregroundStyle(.foreground) + .padding(.vertical, 4.0) + .padding(.horizontal, 8.0) + .background(alignment: .center) { + ZStack { + RoundedRectangle(cornerSize: .init(width: 12, height: 12)) + .fill(.background) + .shadow(radius: 1.5, y: 1.0) + RoundedRectangle(cornerSize: .init(width: 12, height: 12)) + .stroke(accentColor, style: .init(lineWidth: 2.0)) + } + } + .padding() + } + var parsedGraph: ([String], [(String, String)]) { parseMermaid(text) } var body: some View { ForceDirectedGraph { - Repeated(parsedGraph.0) { node in + Series(parsedGraph.0) { node in + NodeMark(id: node) - .symbol(RoundedRectangle(cornerSize: CGSize(width: 3, height: 3))) - .symbolSize(radius: 6) - .label(alignment: .bottom, offset: [0, 4]) { - Text(node) + .symbol(.circle) + .symbolSize(radius: 8) + .foregroundStyle(Color(white: 1.0, opacity: 0.0)) + .richLabel(node, alignment: .center, offset: .zero) { + getLabel(node) } } - Repeated(parsedGraph.1) { link in + Series(parsedGraph.1) { link in LinkMark(from: link.0, to: link.1) } } force: { @@ -107,13 +156,20 @@ struct MermaidVisualization: View { } emittingNewNodesWithStates: { id in KineticState(position: getInitialPosition(id: id, r: 100)) } + .onNodeTapped { + tappedNode = $0 + } .inspector(isPresented: .constant(true)) { VStack { + Text("Tapped: \(tappedNode ?? "nil")") + .font(.title2) + Divider() + Text("Edit the mermaid syntaxes to update the graph") - .font(.title) + .font(.title2) TextEditor(text: $text) .fontDesign(.monospaced) - + }.padding(.top) } diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Miserables.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Miserables.swift index 6f45d36..7a6c794 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Miserables.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Miserables.swift @@ -19,11 +19,30 @@ struct MiserableGraph: View { @State private var opacity: Double = 0 @State private var inspectorPresented = false + @State private var modelTransform: ViewportTransform = .identity.scale(by: 2.0) + + @ViewBuilder + func getLabel(_ text: String) -> some View { + Text(text) + .font(.caption2) + .padding(.vertical, 2.0) + .padding(.horizontal, 6.0) + .background(alignment: .center) { + RoundedRectangle(cornerSize: .init(width: 12, height: 12)) + .fill(.white) + .shadow(radius: 1.5, y: 1.0) + } + .padding() + } + var body: some View { - ForceDirectedGraph($isRunning) { + ForceDirectedGraph( + $isRunning, + $modelTransform + ) { - Repeated(graphData.nodes) { node in + Series(graphData.nodes) { node in NodeMark(id: node.id) .symbol(.asterisk) .symbolSize(radius: 9.0) @@ -39,23 +58,22 @@ struct MiserableGraph: View { ) ) ) - .label(offset: [0.0, 12.0]) { - Text(node.id) - .font(.caption2) + .richLabel(node.id, offset: .zero) { + self.getLabel(node.id) } } - Repeated(graphData.links) { l in + Series(graphData.links) { l in LinkMark(from: l.source, to: l.target) } // } force: { ManyBodyForce(strength: -20) + CenterForce() LinkForce( originalLength: .constant(35.0), stiffness: .weightedByDegree(k: { _, _ in 1.0}) ) - CenterForce() } .onNodeTapped { node in inspectorPresented = true @@ -67,6 +85,7 @@ struct MiserableGraph: View { } .toolbar { + Text("\(modelTransform.scale)") Button { isRunning.toggle() if opacity < 1 { diff --git a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MyRing.swift b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MyRing.swift index db4f377..10de46c 100644 --- a/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MyRing.swift +++ b/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MyRing.swift @@ -35,7 +35,7 @@ struct MyRing: View { ForceDirectedGraph($isRunning) { - Repeated(0..<20) { i in + Series(0..<20) { i in NodeMark(id: 3 * i + 0) .symbol(.circle) .symbolSize(radius:4.0) diff --git a/README.md b/README.md index 462ea65..04ff4fe 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ struct MyGraph: View { NodeMark(id: 1).foregroundStyle(.blue) NodeMark(id: 2).foregroundStyle(.yellow) - Repeated(0..<2) { i in + Series(0..<2) { i in LinkMark(from: i, to: i+1) } diff --git a/Sources/Grape/Contents/GraphContentBuilder.swift b/Sources/Grape/Contents/GraphContentBuilder.swift index eac8168..3cb1b47 100644 --- a/Sources/Grape/Contents/GraphContentBuilder.swift +++ b/Sources/Grape/Contents/GraphContentBuilder.swift @@ -48,7 +48,7 @@ public struct GraphContentBuilder { /// MyComponent(i) /// } /// } - @available(*, deprecated, message: "Use `Repeated` instead. ") + @available(*, deprecated, message: "Use `Series` instead. ") @inlinable public static func buildArray(_ components: [T]) -> some Content where T: Content, T.NodeID == NodeID { diff --git a/Sources/Grape/Contents/Repeated.swift b/Sources/Grape/Contents/Repeated.swift index 11042c2..389a288 100644 --- a/Sources/Grape/Contents/Repeated.swift +++ b/Sources/Grape/Contents/Repeated.swift @@ -1,4 +1,4 @@ -public struct Repeated +public struct Series where Data: RandomAccessCollection, Content: GraphContent, NodeID: Hashable { @usableFromInline @@ -17,7 +17,7 @@ where Data: RandomAccessCollection, Content: GraphContent, NodeID: Hasha } } -extension Repeated: GraphContent { +extension Series: GraphContent { @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { self.data.forEach { element in diff --git a/Sources/Grape/Grape.docc/Documentation.md b/Sources/Grape/Grape.docc/Documentation.md index d736ed5..24c054c 100644 --- a/Sources/Grape/Grape.docc/Documentation.md +++ b/Sources/Grape/Grape.docc/Documentation.md @@ -27,8 +27,7 @@ If you’re looking for a more detailed control of force-directed layouts, pleas * ``GraphContent`` * ``NodeMark`` * ``LinkMark`` -* ``Repeated`` -* ``GraphContent/foregroundStyle(_:)`` +* ``Series`` ### Handling gestures and events diff --git a/Sources/Grape/Modifiers/Effects/GrapeEffect.Label.swift b/Sources/Grape/Modifiers/Effects/GrapeEffect.Label.swift index 25a3610..4786198 100644 --- a/Sources/Grape/Modifiers/Effects/GrapeEffect.Label.swift +++ b/Sources/Grape/Modifiers/Effects/GrapeEffect.Label.swift @@ -1,5 +1,6 @@ import SwiftUI + extension GraphContentEffect { @usableFromInline internal struct Label { @@ -31,6 +32,9 @@ extension GraphContentEffect { @usableFromInline let view: AnyView + @usableFromInline + let tag: String + @usableFromInline let alignment: Alignment @@ -39,15 +43,18 @@ extension GraphContentEffect { @inlinable public init( + _ tag: String, _ view: some View, alignment: Alignment = .bottom, offset: CGVector = .zero ) { + self.tag = tag self.view = .init(erasing: view) self.alignment = alignment self.offset = offset } } + } extension GraphContentEffect.Label: GraphContentModifier { @@ -83,7 +90,6 @@ extension GraphContentEffect.Label: GraphContentModifier { } } - extension GraphContentEffect.RichLabel: GraphContentModifier { @inlinable public func _into( @@ -96,7 +102,22 @@ extension GraphContentEffect.RichLabel: GraphContentModifier { @MainActor public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { - + if let currentID = context.states.currentID { + + context.resolvedViews[currentID] = .pending(self.view) + + switch currentID { + case .node(_): + if let currentSymbolSize = context.states.currentSymbolSize { + let anchorOffset = alignment.anchorOffset(for: currentSymbolSize) + context.textOffsets[currentID] = (alignment, offset.simd + anchorOffset) + } else { + context.textOffsets[currentID] = (alignment, offset.simd) + } + case .link(_, _): + context.textOffsets[currentID] = (alignment, offset.simd) + } + } } } @@ -113,7 +134,7 @@ extension Alignment { return SIMD2(Double(size.width) / 2, 0) case .trailing: return SIMD2(-Double(size.width) / 2, 0) - default: + default: return .zero } case .bottom: diff --git a/Sources/Grape/Modifiers/GraphContent+GraphContentModifiers.swift b/Sources/Grape/Modifiers/GraphContent+GraphContentModifiers.swift index e2a8f9d..e75a508 100644 --- a/Sources/Grape/Modifiers/GraphContent+GraphContentModifiers.swift +++ b/Sources/Grape/Modifiers/GraphContent+GraphContentModifiers.swift @@ -107,20 +107,24 @@ extension GraphContent { @inlinable public func richLabel( - _ alignment: Alignment = .bottom, offset: CGVector = .zero, + _ tag: String, + _ alignment: Alignment = .bottom, + offset: CGVector = .zero, @ViewBuilder _ content: () -> some View ) -> some GraphContent { - return ModifiedGraphContent( - self, GraphContentEffect.RichLabel(content(), alignment: alignment, offset: offset)) + self, GraphContentEffect.RichLabel(tag, content(), alignment: alignment, offset: offset) + ) } @inlinable public func richLabel( - alignment: Alignment = .bottom, offset: SIMD2 = .zero, + _ tag: String, + alignment: Alignment = .bottom, + offset: SIMD2 = .zero, @ViewBuilder _ content: () -> some View ) -> some GraphContent { - return richLabel(alignment, offset: offset.cgVector, content) + return richLabel(tag, alignment, offset: offset.cgVector, content) } /// Sets the stroke style for this graph content. diff --git a/Sources/Grape/Utils/RasterizedViewStore.swift b/Sources/Grape/Utils/RasterizedViewStore.swift new file mode 100644 index 0000000..ad337e8 --- /dev/null +++ b/Sources/Grape/Utils/RasterizedViewStore.swift @@ -0,0 +1,38 @@ +import SwiftUI + +@usableFromInline +struct ViewRasteriazationStore { + @usableFromInline + enum RasteriazationEntry { + case pending(V) + case resolved(V, CGImage?) + } + + @usableFromInline + internal var resolvedViews: [T: RasteriazationEntry] = [:] + + @inlinable + internal init() { + + } +} + +extension ViewRasteriazationStore { + @MainActor + @inlinable + func resolve( + _ key: T, + in environment: EnvironmentValues + ) -> CGImage? { + switch self.resolvedViews[key] { + case .pending(let view): + let cgImage = view.environment(\.self, environment).toCGImage(with: environment) + debugPrint("[RESOLVE VIEW]") + return cgImage + case .resolved(_, let cgImage): + return cgImage + case .none: + return nil + } + } +} diff --git a/Sources/Grape/Utils/Transform.swift b/Sources/Grape/Utils/Transform.swift index ed0e1c0..097d659 100644 --- a/Sources/Grape/Utils/Transform.swift +++ b/Sources/Grape/Utils/Transform.swift @@ -46,7 +46,6 @@ extension TransformProtocol { @inlinable public mutating func translating(by delta: Vector) { - // self = Self(translate: translate + delta, scale: scale) self.translate = translate + delta } @@ -57,13 +56,13 @@ extension TransformProtocol { } @inlinable - public func scale(by delta: Scalar) -> Self { - return Self(translate: translate, scale: scale + delta) + public func scale(by factor: Scalar) -> Self { + return Self(translate: translate, scale: scale * factor) } @inlinable - public func scale(to factor: Scalar) -> Self { - return Self(translate: translate, scale: factor) + public func scale(to newScale: Scalar) -> Self { + return Self(translate: translate, scale: newScale) } @inlinable diff --git a/Sources/Grape/Views/ForceDirectedGraph+View.swift b/Sources/Grape/Views/ForceDirectedGraph+View.swift index 8471a2d..6bd302d 100644 --- a/Sources/Grape/Views/ForceDirectedGraph+View.swift +++ b/Sources/Grape/Views/ForceDirectedGraph+View.swift @@ -16,7 +16,11 @@ extension ForceDirectedGraph: View { of: self._graphRenderingContextShadow, initial: false // Don't trigger on initial value, keep `changeMessage` as "N/A" ) { _, newValue in - self.model.revive(for: newValue, with: .init(self._forceDescriptors)) + self.model.revive( + for: newValue, + with: .init(self._forceDescriptors), + alpha: self.model.simulationContext.storage.kinetics.alpha + ) } .onChange(of: self.isRunning, initial: false) { oldValue, newValue in guard oldValue != newValue else { return } @@ -44,9 +48,9 @@ extension ForceDirectedGraph: View { Text(self.model.changeMessage) Divider() Button { - self.clickCount += 1 + // self.clickCount += 1 } label: { - Text("Click \(clickCount)") + Text("Click") } ScrollView { diff --git a/Sources/Grape/Views/ForceDirectedGraph.swift b/Sources/Grape/Views/ForceDirectedGraph.swift index 236e8c1..f96d4b2 100644 --- a/Sources/Grape/Views/ForceDirectedGraph.swift +++ b/Sources/Grape/Views/ForceDirectedGraph.swift @@ -7,36 +7,36 @@ public struct ForceDirectedGraph where @inlinable @Environment(\.self) - var environment: EnvironmentValues + internal var environment: EnvironmentValues @inlinable @Environment(\.graphForegroundScaleEnvironment) - var graphForegroundScale + internal var graphForegroundScale @inlinable @Environment(\.colorScheme) - var colorScheme + internal var colorScheme @inlinable @Environment(\.colorSchemeContrast) - var colorSchemeContrast + internal var colorSchemeContrast // the copy of the graph context to be used for comparison in `onChange` // should be not used for rendering @usableFromInline - let _graphRenderingContextShadow: _GraphRenderingContext + internal let _graphRenderingContextShadow: _GraphRenderingContext @usableFromInline - let _forceDescriptors: [SealedForce2D.ForceEntry] + internal let _forceDescriptors: [SealedForce2D.ForceEntry] - // TBD: Some state to be retained when the graph is updated - @State - @inlinable - var clickCount = 0 + // // TBD: Some state to be retained when the graph is updated + // @State + // @inlinable + // internal var clickCount = 0 // @State @inlinable - var model: ForceDirectedGraphModel + internal var model: ForceDirectedGraphModel { @storageRestrictions(initializes: _model) init(initialValue) { @@ -47,10 +47,10 @@ public struct ForceDirectedGraph where } @usableFromInline - var _model: State> + internal var _model: State> @inlinable - var isRunning: Bool { + internal var isRunning: Bool { get { _isRunning.wrappedValue } @@ -60,11 +60,22 @@ public struct ForceDirectedGraph where } @usableFromInline - var _isRunning: Binding + internal var _isRunning: Binding + + // @inlinable + // internal var modelTransform: ViewportTransform { + // get { + // _modelTransform.wrappedValue + // } + // set { + // _modelTransform.wrappedValue = newValue + // } + // } @inlinable public init( - _ _isRunning: Binding = .constant(true), + _ isRunning: Binding = .constant(true), + _ modelTransform: Binding = .constant(.identity), ticksPerSecond: Double = 60.0, initialViewportTransform: ViewportTransform = .identity, @GraphContentBuilder _ graph: () -> Content, @@ -76,14 +87,17 @@ public struct ForceDirectedGraph where var gctx = _GraphRenderingContext() graph()._attachToGraphRenderingContext(&gctx) - self._graphRenderingContextShadow = gctx - self._isRunning = _isRunning + self._graphRenderingContextShadow = gctx + self._isRunning = isRunning + self._forceDescriptors = force() + let force = SealedForce2D(self._forceDescriptors) self.model = .init( gctx, force, + modelTransform: modelTransform, emittingNewNodesWith: state, ticksPerSecond: ticksPerSecond ) diff --git a/Sources/Grape/Views/ForceDirectedGraphModel.swift b/Sources/Grape/Views/ForceDirectedGraphModel.swift index 3a7acb6..11fb74b 100644 --- a/Sources/Grape/Views/ForceDirectedGraphModel.swift +++ b/Sources/Grape/Views/ForceDirectedGraphModel.swift @@ -15,7 +15,27 @@ public final class ForceDirectedGraphModel { var simulationContext: SimulationContext @usableFromInline - var modelTransform: ViewportTransform = .identity + internal var _modelTransform: ViewportTransform + + @usableFromInline + internal var _modelTransformExtenalBinding: Binding + + @inlinable + internal var modelTransform: ViewportTransform { + @storageRestrictions(initializes: _modelTransform) + init(initialValue) { + _modelTransform = initialValue + } + + get { + return _modelTransform + } + + set { + _modelTransform = newValue + _modelTransformExtenalBinding.wrappedValue = newValue + } + } /// Moves the zero-centered simulation to final view @usableFromInline @@ -116,6 +136,7 @@ public final class ForceDirectedGraphModel { init( _ graphRenderingContext: _GraphRenderingContext, _ forceField: consuming SealedForce2D, + modelTransform: Binding, emittingNewNodesWith: @escaping (NodeID) -> KineticState = { _ in .init(position: .zero) }, @@ -140,12 +161,15 @@ public final class ForceDirectedGraphModel { count: self.simulationContext.storage.kinetics.position.count ) self.currentFrame = 0 + self._modelTransformExtenalBinding = modelTransform + self.modelTransform = modelTransform.wrappedValue } @inlinable convenience init( _ graphRenderingContext: _GraphRenderingContext, _ forceField: consuming SealedForce2D, + modelTransform: Binding, emittingNewNodesWith: @escaping (NodeID) -> KineticState = { _ in .init(position: .zero) }, @@ -154,6 +178,7 @@ public final class ForceDirectedGraphModel { self.init( graphRenderingContext, forceField, + modelTransform: modelTransform, emittingNewNodesWith: emittingNewNodesWith, ticksPerSecond: ticksPerSecond, velocityDecay: 30 / ticksPerSecond @@ -221,6 +246,7 @@ extension ForceDirectedGraphModel { self.scheduledTimer = nil } + @inlinable @MainActor func render( @@ -231,6 +257,7 @@ extension ForceDirectedGraphModel { // print("Rendering frame \(_$currentFrame.rawValue)") let transform = modelTransform.translate(by: size.simd / 2) + // debugPrint(transform.scale) // var viewportPositions = [SIMD2]() // viewportPositions.reserveCapacity(simulationContext.storage.kinetics.position.count) @@ -361,7 +388,8 @@ extension ForceDirectedGraphModel { antialias: Self.textRasterizationAntialias ) lastRasterizedScaleFactor = env.displayScale - graphRenderingContext.symbols[resolvedTextContent] = .resolved(consume text, cgImage) + graphRenderingContext.symbols[resolvedTextContent] = .resolved( + consume text, cgImage) rasterizedSymbol = cgImage case .resolved(_, let cgImage): rasterizedSymbol = cgImage @@ -431,7 +459,88 @@ extension ForceDirectedGraphModel { height: physicalHeight ) ) + } + } + } + + for (symbolID, viewResolvingResult) in graphRenderingContext.resolvedViews { + + // Look for rasterized symbol's image + var rasterizedSymbol: CGImage? = nil + switch viewResolvingResult { + case .pending(let view): + let resolved = viewResolvingResult.resolve(in: graphicsContext.environment) + graphRenderingContext.resolvedViews[symbolID] = .resolved(view, resolved) + rasterizedSymbol = resolved + case .resolved(_, let cgImage): + + rasterizedSymbol = cgImage + } + + guard let rasterizedSymbol = rasterizedSymbol else { + continue + } + + // Start drawing + switch symbolID { + case .node(let nodeID): + guard let id = simulationContext.nodeIndexLookup[nodeID] else { + continue + } + let pos = viewportPositions[id] + if let textOffsetParams = graphRenderingContext.textOffsets[symbolID] { + let offset = textOffsetParams.offset + + let physicalWidth = + Double(rasterizedSymbol.width) / lastRasterizedScaleFactor + / Self.textRasterizationAntialias + let physicalHeight = + Double(rasterizedSymbol.height) / lastRasterizedScaleFactor + / Self.textRasterizationAntialias + + let textImageOffset = textOffsetParams.alignment.textImageOffsetInCGContext( + width: physicalWidth, height: physicalHeight) + + cgContext.draw( + rasterizedSymbol, + in: .init( + x: pos.x + offset.x + textImageOffset.x, // - physicalWidth / 2, + y: -pos.y - offset.y - textImageOffset.y, // - physicalHeight + width: physicalWidth, + height: physicalHeight + ) + ) + } + + case .link(let fromID, let toID): + guard let from = simulationContext.nodeIndexLookup[fromID], + let to = simulationContext.nodeIndexLookup[toID] + else { + continue + } + let center = (viewportPositions[from] + viewportPositions[to]) / 2 + if let textOffsetParams = graphRenderingContext.textOffsets[symbolID] { + let offset = textOffsetParams.offset + + let physicalWidth = + Double(rasterizedSymbol.width) / lastRasterizedScaleFactor + / Self.textRasterizationAntialias + let physicalHeight = + Double(rasterizedSymbol.height) / lastRasterizedScaleFactor + / Self.textRasterizationAntialias + + let textImageOffset = textOffsetParams.alignment.textImageOffsetInCGContext( + width: physicalWidth, height: physicalHeight) + cgContext.draw( + rasterizedSymbol, + in: .init( + x: center.x + offset.x + textImageOffset.x, // - physicalWidth / 2, + y: -center.y - offset.y - textImageOffset.y, // - physicalHeight + width: physicalWidth, + height: physicalHeight + ) + ) } } } @@ -447,17 +556,36 @@ extension ForceDirectedGraphModel { @inlinable func revive( for newContext: _GraphRenderingContext, - with newForceField: consuming SealedForce2D + with newForceField: consuming SealedForce2D, + alpha: Double ) { - // self.changeMessage = - // "gctx \(graphRenderingContext.nodes.count) -> \(newContext.nodes.count)" - + var newContext = newContext self.simulationContext.revive( for: newContext, with: newForceField, velocityDecay: velocityDecay, emittingNewNodesWith: self._emittingNewNodesWith ) + self.simulationContext.storage.kinetics.alpha = alpha + + newContext.resolvedTexts = self.graphRenderingContext.resolvedTexts.merging( + newContext.resolvedTexts + ) { old, new in + new + } + + newContext.resolvedViews = self.graphRenderingContext.resolvedViews.merging( + newContext.resolvedViews + ) { old, new in + old + } + + newContext.symbols = self.graphRenderingContext.symbols.merging( + newContext.symbols + ) { old, new in + old + } + self.graphRenderingContext = newContext /// Resize diff --git a/Sources/Grape/Views/GraphRenderingContext.swift b/Sources/Grape/Views/GraphRenderingContext.swift index 21522b4..c3fc9a3 100644 --- a/Sources/Grape/Views/GraphRenderingContext.swift +++ b/Sources/Grape/Views/GraphRenderingContext.swift @@ -2,20 +2,24 @@ import SwiftUI public struct _GraphRenderingContext { @usableFromInline - enum TextResolvingStatus: Equatable { - case pending(Text) - case resolved(Text, CGImage?) + enum ViewResolvingState where V: View { + case pending(V) + case resolved(V, CGImage?) } @usableFromInline internal var resolvedTexts: [GraphRenderingStates.StateID: String] = [:] + @usableFromInline + internal var resolvedViews: + [GraphRenderingStates.StateID: ViewResolvingState] = [:] + @usableFromInline internal var textOffsets: [GraphRenderingStates.StateID: (alignment: Alignment, offset: SIMD2)] = [:] @usableFromInline - internal var symbols: [String: TextResolvingStatus] = [:] + internal var symbols: [String: ViewResolvingState] = [:] @usableFromInline internal var nodeOperations: [RenderOperation.Node] = [] @@ -34,18 +38,31 @@ public struct _GraphRenderingContext { @usableFromInline internal var states = GraphRenderingStates() - @inlinable func updateEnvironment(with newEnvironment: EnvironmentValues) { - + + } +} + +extension _GraphRenderingContext.ViewResolvingState { + @MainActor + @inlinable + func resolve(in environment: EnvironmentValues) -> CGImage? { + switch self { + case .pending(let view): + let cgImage = view.environment(\.self, environment).toCGImage(with: environment) + debugPrint("[RESOLVE VIEW]") + return cgImage + case .resolved(_, let cgImage): + return cgImage + } } } extension _GraphRenderingContext: Equatable { @inlinable public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.symbols == rhs.symbols - && lhs.nodeOperations == rhs.nodeOperations + lhs.nodeOperations == rhs.nodeOperations && lhs.linkOperations == rhs.linkOperations } } diff --git a/Tests/GrapeTests/ContentBuilderTests.swift b/Tests/GrapeTests/ContentBuilderTests.swift index 928eb40..d26b646 100644 --- a/Tests/GrapeTests/ContentBuilderTests.swift +++ b/Tests/GrapeTests/ContentBuilderTests.swift @@ -17,7 +17,7 @@ final class ContentBuilderTests: XCTestCase { func testForLoop() { let _ = buildGraph { - Repeated(0..<10) { i in + Series(0..<10) { i in NodeMark(id: i) } } diff --git a/Tests/GrapeTests/GraphContentBuilderTests.swift b/Tests/GrapeTests/GraphContentBuilderTests.swift index 9c7eecf..9530155 100644 --- a/Tests/GrapeTests/GraphContentBuilderTests.swift +++ b/Tests/GrapeTests/GraphContentBuilderTests.swift @@ -25,20 +25,20 @@ final class GraphContentBuilderTests: XCTestCase { ID(id: 2), ] - let a = Repeated(arr) { i in + let a = Series(arr) { i in NodeMark(id: i.id) } let b = buildGraph { NodeMark(id: 0) - Repeated(arr) { i in + Series(arr) { i in NodeMark(id: i.id) } } let c = buildGraph { NodeMark(id: 0) - Repeated(0..<10) { i in + Series(0..<10) { i in NodeMark(id: 0) } }