Repository: li3zhen1/Grape Branch: main Commit: bdfad750d2a9 Files: 140 Total size: 418.9 KB Directory structure: gitextract_qg8mm2ot/ ├── .github/ │ └── workflows/ │ └── swift.yml ├── .gitignore ├── .spi.yml ├── .swift-format ├── Assets/ │ └── Grape.blend ├── DocPostprocess.swift ├── Examples/ │ ├── ForceDirectedGraph3D/ │ │ ├── ForceDirectedGraph3D/ │ │ │ ├── Assets.xcassets/ │ │ │ │ ├── AppIcon.solidimagestack/ │ │ │ │ │ ├── Back.solidimagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── Front.solidimagestacklayer/ │ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Middle.solidimagestacklayer/ │ │ │ │ │ ├── Content.imageset/ │ │ │ │ │ │ └── Contents.json │ │ │ │ │ └── Contents.json │ │ │ │ └── Contents.json │ │ │ ├── ContentView.swift │ │ │ ├── Data.swift │ │ │ ├── ForceDirectedGraph3DApp.swift │ │ │ ├── Info.plist │ │ │ └── Preview Content/ │ │ │ └── Preview Assets.xcassets/ │ │ │ └── Contents.json │ │ ├── ForceDirectedGraph3D.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ └── project.xcworkspace/ │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata/ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── Packages/ │ │ └── RealityKitContent/ │ │ ├── .build/ │ │ │ └── workspace-state.json │ │ ├── Package.realitycomposerpro/ │ │ │ ├── ProjectData/ │ │ │ │ └── main.json │ │ │ └── WorkspaceData/ │ │ │ ├── SceneMetadataList.json │ │ │ └── Settings.rcprojectdata │ │ ├── Package.swift │ │ ├── README.md │ │ └── Sources/ │ │ └── RealityKitContent/ │ │ ├── RealityKitContent.rkassets/ │ │ │ ├── Materials/ │ │ │ │ └── GridMaterial.usda │ │ │ └── Scene.usda │ │ └── RealityKitContent.swift │ └── ForceDirectedGraphExample/ │ ├── ForceDirectedGraphExample/ │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Data.swift │ │ ├── ForceDirectedGraphExample.entitlements │ │ ├── ForceDirectedGraphExampleApp.swift │ │ ├── GraphStateToolbar.swift │ │ ├── Lattice.swift │ │ ├── MermaidVisualization.swift │ │ ├── Miserables.swift │ │ ├── MyRing.swift │ │ └── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ └── ForceDirectedGraphExample.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata/ │ └── xcschemes/ │ └── ForceDirectedGraphExample.xcscheme ├── LICENSE ├── Package.swift ├── README.md ├── Sources/ │ ├── ForceSimulation/ │ │ ├── ForceProtocol.swift │ │ ├── ForceSimulation.docc/ │ │ │ ├── CreatingASimulationWithBuiltinForces.md │ │ │ ├── Documentation.md │ │ │ └── theme-settings.json │ │ ├── Forces/ │ │ │ ├── CenterForce.swift │ │ │ ├── CollideForce.swift │ │ │ ├── CompositedForce.swift │ │ │ ├── EmptyForce.swift │ │ │ ├── KDTreeForce.swift │ │ │ ├── LinkForce.swift │ │ │ ├── ManyBodyForce.swift │ │ │ ├── PackedForce.swift │ │ │ ├── PositionForce.swift │ │ │ ├── RadialForce.swift │ │ │ ├── SealedForce2D.swift │ │ │ └── SealedForce3D.swift │ │ ├── KDTree/ │ │ │ ├── BufferedKDTree.swift │ │ │ ├── KDBox.swift │ │ │ ├── KDTree.swift │ │ │ ├── KDTreeDelegate.swift │ │ │ └── KDTreeNode.swift │ │ ├── Kinetics.swift │ │ ├── Simulation.swift │ │ └── Utils/ │ │ ├── AttributeDescriptor.swift │ │ ├── Disposable.swift │ │ ├── EdgeID.swift │ │ ├── LinearCongruentialGenerator.swift │ │ ├── SimulatableVector.swift │ │ └── UnsafeArray.swift │ └── Grape/ │ ├── Contents/ │ │ ├── AnyGraphContent.swift │ │ ├── GraphContent.swift │ │ ├── GraphContentBuilder.swift │ │ ├── LinkMark.swift │ │ ├── ModifiedGraphContent.swift │ │ ├── NodeMark.swift │ │ ├── Series.swift │ │ ├── _ArrayGraphContent.swift │ │ ├── _ConditionalGraphContent.swift │ │ ├── _EmptyGraphContent.swift │ │ ├── _IdentifiableNever.swift │ │ ├── _OptionalGraphContent.swift │ │ └── _PairedGraphContent.swift │ ├── Descriptors/ │ │ └── ForceDescriptor.swift │ ├── Gestures/ │ │ ├── GraphDragGesture.swift │ │ ├── GraphMagnifyGesture.swift │ │ └── GraphTapGesture.swift │ ├── Grape.docc/ │ │ ├── CreatingAForceDirectedGraph.md │ │ ├── Documentation.md │ │ ├── StateManagementAndEliminatingRedundantRerenders.md │ │ └── theme-settings.json │ ├── Modifiers/ │ │ ├── AnyGraphContentModifier.swift │ │ ├── Effects/ │ │ │ ├── GrapeEffect.ForegroundStyle.swift │ │ │ ├── GrapeEffect.Label.swift │ │ │ ├── GrapeEffect.Opacity.swift │ │ │ ├── GrapeEffect.Stroke.swift │ │ │ ├── GrapeEffect.Symbol.swift │ │ │ ├── GrapeEffect.SymbolSize.swift │ │ │ ├── GrapeEffect._LinkShape.swift │ │ │ └── GrapeEffect.swift │ │ ├── GraphContent+GraphContentModifiers.swift │ │ ├── GraphContentModifier.swift │ │ ├── GraphForegroundScale.swift │ │ └── GraphProxy.swift │ ├── Utils/ │ │ ├── CoreGraphics+SIMD.swift │ │ ├── GraphProtocol.swift │ │ ├── KeyFrame.swift │ │ ├── LinkShape.swift │ │ ├── RasterizedViewStore.swift │ │ ├── Transform.swift │ │ └── View+CGImage.swift │ └── Views/ │ ├── ForceDirectedGraph+View.swift │ ├── ForceDirectedGraph.swift │ ├── ForceDirectedGraphModel+Observation.swift │ ├── ForceDirectedGraphModel.findNode.swift │ ├── ForceDirectedGraphModel.swift │ ├── ForceDirectedGraphState.swift │ ├── GraphLayoutInputs.swift │ ├── GraphRenderingContext.swift │ ├── GraphRenderingStates.swift │ ├── RenderOperation.swift │ └── SimulationContext.swift └── Tests/ ├── ForceSimulationTests/ │ ├── ForceTests.swift │ ├── GKTreeCompareTest.swift │ ├── MiserableData.swift │ └── MiserableGraphTest.swift ├── GrapeTests/ │ ├── ContentBuilderTests.swift │ └── GraphContentBuilderTests.swift └── KDTreeTests/ ├── BufferedKDTreeTests.swift └── KDTreeTests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/swift.yml ================================================ name: Swift CI on: push: branches: [ "main" ] permissions: contents: read pages: write id-token: write jobs: deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: macos-14 steps: - name: Checkout 🛎️ uses: actions/checkout@v3 - name: Setup Xcode version uses: maxim-lobanov/setup-xcode@v1.6.0 with: xcode-version: 'latest-stable' - name: Build run: swift build - name: Run tests run: xcodebuild test -scheme Grape-Package -destination "platform=macOS" - name: Build DocC run: | # If you use docc-plugin, you might be able to use docc-plugin command instead mkdir -p docs && swift package --allow-writing-to-directory docs/ForceSimulation \ generate-documentation --target ForceSimulation \ --disable-indexing \ --transform-for-static-hosting \ --hosting-base-path Grape/ForceSimulation \ --output-path docs/ForceSimulation && swift package --allow-writing-to-directory docs/Grape \ generate-documentation --target Grape \ --disable-indexing \ --transform-for-static-hosting \ --hosting-base-path Grape/Grape \ --output-path docs/Grape && swift ./DocPostprocess.swift - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: path: 'docs' - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v1 ================================================ FILE: .gitignore ================================================ .DS_Store /.build /Packages xcuserdata/ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc *.ts /.vscode /.swiftpm /Sources/_ForceSimulation /docs *.kts Package.resolved /.index-build ================================================ FILE: .spi.yml ================================================ version: 1 builder: configs: - documentation_targets: [ForceSimulation, Grape] ================================================ FILE: .swift-format ================================================ { "fileScopedDeclarationPrivacy": { "accessLevel": "private" }, "indentation": { "spaces": 4 }, "indentConditionalCompilationBlocks": true, "indentSwitchCaseLabels": false, "lineBreakAroundMultilineExpressionChainComponents": false, "lineBreakBeforeControlFlowKeywords": false, "lineBreakBeforeEachArgument": false, "lineBreakBeforeEachGenericRequirement": false, "lineLength": 120, "maximumBlankLines": 1, "multiElementCollectionTrailingCommas": true, "noAssignmentInExpressions": { "allowedFunctions": [ "XCTAssertNoThrow" ] }, "prioritizeKeepingFunctionOutputTogether": false, "respectsExistingLineBreaks": true, "rules": { "AllPublicDeclarationsHaveDocumentation": false, "AlwaysUseLiteralForEmptyCollectionInit": false, "AlwaysUseLowerCamelCase": true, "AmbiguousTrailingClosureOverload": true, "BeginDocumentationCommentWithOneLineSummary": false, "DoNotUseSemicolons": true, "DontRepeatTypeInStaticProperties": true, "FileScopedDeclarationPrivacy": true, "FullyIndirectEnum": true, "GroupNumericLiterals": true, "IdentifiersMustBeASCII": true, "NeverForceUnwrap": false, "NeverUseForceTry": false, "NeverUseImplicitlyUnwrappedOptionals": false, "NoAccessLevelOnExtensionDeclaration": true, "NoAssignmentInExpressions": true, "NoBlockComments": true, "NoCasesWithOnlyFallthrough": true, "NoEmptyTrailingClosureParentheses": true, "NoLabelsInCasePatterns": true, "NoLeadingUnderscores": false, "NoParensAroundConditions": true, "NoPlaygroundLiterals": true, "NoVoidReturnOnFunctionSignature": true, "OmitExplicitReturns": false, "OneCasePerLine": true, "OneVariableDeclarationPerLine": true, "OnlyOneTrailingClosureArgument": true, "OrderedImports": true, "ReplaceForEachWithForLoop": true, "ReturnVoidInsteadOfEmptyTuple": true, "TypeNamesShouldBeCapitalized": true, "UseEarlyExits": false, "UseLetInEveryBoundCaseVariable": true, "UseShorthandTypeNames": true, "UseSingleLinePropertyGetter": true, "UseSynthesizedInitializer": true, "UseTripleSlashForDocumentationComments": true, "UseWhereClausesInForLoops": false, "ValidateDocumentationComments": false }, "spacesAroundRangeFormationOperators": false, "tabWidth": 4, "version": 1 } ================================================ FILE: DocPostprocess.swift ================================================ import Foundation // Define the paths for the files let docsDirectoryPath = "./docs" let iconSourcePath = "./assets/grape_icon_256.png" let iconDestPath = "./docs/favicon.png" let moduleNames = [ "Grape", "ForceSimulation", ] do { let fileManager = FileManager.default // Check if docs directory exists var isDir: ObjCBool = false if fileManager.fileExists(atPath: docsDirectoryPath, isDirectory: &isDir) { if isDir.boolValue { // Docs directory exists, proceed with enumeration let enumerator = fileManager.enumerator(atPath: docsDirectoryPath) while let element = enumerator?.nextObject() as? String { if element.hasSuffix("index.html") { // checks the extension print(element) let indexPath = "\(docsDirectoryPath)/\(element)" var htmlString = try String(contentsOfFile: indexPath, encoding: .utf8) for moduleName in moduleNames { htmlString = htmlString.replacingOccurrences( of: """ """, with: """ """) htmlString = htmlString.replacingOccurrences( of: """ """, with: """ """) } try htmlString.write(toFile: indexPath, atomically: false, encoding: .utf8) } } } } // Copy the icon file if it doesn't exist at the destination if !fileManager.fileExists(atPath: iconDestPath) { try fileManager.copyItem(atPath: iconSourcePath, toPath: iconDestPath) } for moduleName in moduleNames { let iconDestPath = "./docs/\(moduleName)/favicon.png" if !fileManager.fileExists(atPath: iconDestPath) { try fileManager.copyItem(atPath: iconSourcePath, toPath: iconDestPath) } } } catch { // Handle errors by printing to the console for now print("An error occurred: \(error)") } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "vision", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Assets.xcassets/AppIcon.solidimagestack/Back.solidimagestacklayer/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Assets.xcassets/AppIcon.solidimagestack/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 }, "layers" : [ { "filename" : "Front.solidimagestacklayer" }, { "filename" : "Middle.solidimagestacklayer" }, { "filename" : "Back.solidimagestacklayer" } ] } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "vision", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Assets.xcassets/AppIcon.solidimagestack/Front.solidimagestacklayer/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Content.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "vision", "scale" : "2x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Assets.xcassets/AppIcon.solidimagestack/Middle.solidimagestacklayer/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/ContentView.swift ================================================ // // ContentView.swift // ForceDirectedGraph3D // // Created by li3zhen1 on 10/20/23. // import SwiftUI import RealityKit import RealityKitContent import simd import ForceSimulation import Grape struct My3DForce: ForceField3D { typealias Vector = SIMD3 var force = CompositedForce { Kinetics3D.CenterForce(center: .zero, strength: 1) Kinetics3D.ManyBodyForce(strength: -1) Kinetics3D.LinkForce(stiffness: .constant(0.5)) } } func buildSimulation() -> Simulation3D { let data = getData(miserables) let links = data.links.map { l in let fromID = data.nodes.firstIndex { mn in mn.id == l.source }! let toID = data.nodes.firstIndex { mn in mn.id == l.target }! return EdgeID(source: fromID, target: toID) } let sim = Simulation( nodeCount: data.nodes.count, links: links, forceField: My3DForce() ) for _ in 0..<720 { sim.tick() } return sim } func getLinkIndices() -> [(Int, Int)] { let data = getData(miserables) let linkIds = data.links.map { l in (data.nodes.firstIndex{l.source==$0.id}!, data.nodes.firstIndex{l.target==$0.id}!) } return linkIds } let scaleRatio: Float = 0.0027 let materialColors: [UIColor] = [ UIColor(red: 17.0/255, green: 181.0/255, blue: 174.0/255, alpha: 1.0), UIColor(red: 64.0/255, green: 70.0/255, blue: 201.0/255, alpha: 1.0), UIColor(red: 246.0/255, green: 133.0/255, blue: 18.0/255, alpha: 1.0), UIColor(red: 222.0/255, green: 60.0/255, blue: 130.0/255, alpha: 1.0), UIColor(red: 17.0/255, green: 181.0/255, blue: 174.0/255, alpha: 1.0), UIColor(red: 114.0/255, green: 224.0/255, blue: 106.0/255, alpha: 1.0), UIColor(red: 22.0/255, green: 124.0/255, blue: 243.0/255, alpha: 1.0), UIColor(red: 115.0/255, green: 38.0/255, blue: 211.0/255, alpha: 1.0), UIColor(red: 232.0/255, green: 198.0/255, blue: 0.0/255, alpha: 1.0), UIColor(red: 203.0/255, green: 93.0/255, blue: 2.0/255, alpha: 1.0), UIColor(red: 0.0/255, green: 143.0/255, blue: 93.0/255, alpha: 1.0), UIColor(red: 188.0/255, green: 233.0/255, blue: 49.0/255, alpha: 1.0), ] struct ContentView: View { @State var test = false var body: some View { VStack { RealityView { content in var material = PhysicallyBasedMaterial() material.baseColor = PhysicallyBasedMaterial.BaseColor(tint: UIColor(white: 1.0, alpha: 0.2)) material.roughness = PhysicallyBasedMaterial.Roughness(floatLiteral: 0.8) material.metallic = PhysicallyBasedMaterial.Metallic(floatLiteral: 0.2) let nodeMaterials = materialColors.map { c in var material = PhysicallyBasedMaterial() material.baseColor = PhysicallyBasedMaterial.BaseColor(tint: c) material.roughness = PhysicallyBasedMaterial.Roughness(floatLiteral: 1.0) material.metallic = PhysicallyBasedMaterial.Metallic(floatLiteral: 0.01) material.emissiveColor = PhysicallyBasedMaterial.EmissiveColor(color: c) material.emissiveIntensity = 0.4 return material } let sim = buildSimulation() let positions = sim.kinetics.position.asArray().map { pos in simd_float3( (pos[1]) * scaleRatio, -(pos[0]) * scaleRatio, (pos[2]) * scaleRatio + 0.25 )} for i in positions.indices { let gid = getData(miserables).nodes[i].group let sphere = MeshResource.generateSphere(radius: 0.005) let sphereEntity = ModelEntity(mesh: sphere, materials: [ nodeMaterials[gid%nodeMaterials.count] ]) sphereEntity.position = positions[i] content.add(sphereEntity) } let linkIds = getLinkIndices() for (f, t) in linkIds { content.add( withCylinder( from: positions[f], to: positions[t], material: material ) ) } } update: { content in guard let animationResource = try? AnimationResource.generate(with: OrbitAnimation(trimDuration: 1)) else {return} content.entities.forEach { e in e.playAnimation(animationResource, transitionDuration: 1) } } .frame(depth: 10.0) }.ornament(attachmentAnchor: .scene(.bottom)) { Button { } label: { Text("Force Directed Graph Example for visionOS") } } } private func withCylinder( from fromPosition: simd_float3, to toPosition: simd_float3, material: PhysicallyBasedMaterial ) -> ModelEntity { let cylinderVector = toPosition - fromPosition // calculate the height of the cylinder as the distance between the two points let height = simd_length(cylinderVector) let direction = simd_normalize(cylinderVector) // calculate the midpoint position let midpoint = SIMD3((fromPosition.x + toPosition.x) / 2, (fromPosition.y + toPosition.y) / 2, (fromPosition.z + toPosition.z) / 2) // create the cylinder let cylinder = MeshResource.generateCylinder(height: height, radius: 0.0005) let cylinderEntity = ModelEntity(mesh: cylinder, materials: [material]) // The default cylinder is aligned along the y-axis. Assuming the 'direction' is not parallel to the y-axis, // calculate the quaternion to rotate from the y-axis to the desired direction. let yAxis = SIMD3(0, 1, 0) // default cylinder orientation let dotProduct = simd_dot(yAxis, direction) let crossProduct = simd_cross(yAxis, direction) // Using the dot product (cosine of angle) and the cross product (axis of rotation) // to create a quaternion representing the rotation let quaternion = simd_quatf(ix: crossProduct.x, iy: crossProduct.y, iz: crossProduct.z, r: 1 + dotProduct) // Normalize the quaternion to ensure valid rotation let rotation = simd_normalize(quaternion) // Apply the transformations cylinderEntity.transform = Transform(scale: SIMD3(1, 1, 1), rotation: rotation, translation: midpoint) return cylinderEntity } } #Preview(windowStyle: .automatic) { ContentView() } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Data.swift ================================================ // // miserables.swift // GrapeView // // Created by li3zhen1 on 10/8/23. // import Foundation let miserables3 = """ { "nodes": [ {"id": "Myriel", "group": 1}, {"id": "Napoleon", "group": 1}, ], "links": [ {"source": "Myriel", "target": "Napoleon", "value": 3}, ] } """ let miserables2 = """ { "nodes": [ {"id": "Myriel", "group": 1}, {"id": "Napoleon", "group": 1}, {"id": "Mlle.Baptistine", "group": 1}, {"id": "Valjean", "group": 2}, {"id": "Marguerite", "group": 3}, {"id": "Mme.deR", "group": 2}, ], "links": [ {"source": "Myriel", "target": "Napoleon", "value": 3}, {"source": "Myriel", "target": "Mlle.Baptistine", "value": 3}, {"source": "Napoleon", "target": "Mme.deR", "value": 3}, {"source": "Mlle.Baptistine", "target": "Valjean", "value": 3} ] } """ let miserables = """ { "nodes": [ {"id": "Myriel", "group": 1}, {"id": "Napoleon", "group": 1}, {"id": "Mlle.Baptistine", "group": 1}, {"id": "Mme.Magloire", "group": 1}, {"id": "CountessdeLo", "group": 1}, {"id": "Geborand", "group": 1}, {"id": "Champtercier", "group": 1}, {"id": "Cravatte", "group": 1}, {"id": "Count", "group": 1}, {"id": "OldMan", "group": 1}, {"id": "Labarre", "group": 2}, {"id": "Valjean", "group": 2}, {"id": "Marguerite", "group": 3}, {"id": "Mme.deR", "group": 2}, {"id": "Isabeau", "group": 2}, {"id": "Gervais", "group": 2}, {"id": "Tholomyes", "group": 3}, {"id": "Listolier", "group": 3}, {"id": "Fameuil", "group": 3}, {"id": "Blacheville", "group": 3}, {"id": "Favourite", "group": 3}, {"id": "Dahlia", "group": 3}, {"id": "Zephine", "group": 3}, {"id": "Fantine", "group": 3}, {"id": "Mme.Thenardier", "group": 4}, {"id": "Thenardier", "group": 4}, {"id": "Cosette", "group": 5}, {"id": "Javert", "group": 4}, {"id": "Fauchelevent", "group": 0}, {"id": "Bamatabois", "group": 2}, {"id": "Perpetue", "group": 3}, {"id": "Simplice", "group": 2}, {"id": "Scaufflaire", "group": 2}, {"id": "Woman1", "group": 2}, {"id": "Judge", "group": 2}, {"id": "Champmathieu", "group": 2}, {"id": "Brevet", "group": 2}, {"id": "Chenildieu", "group": 2}, {"id": "Cochepaille", "group": 2}, {"id": "Pontmercy", "group": 4}, {"id": "Boulatruelle", "group": 6}, {"id": "Eponine", "group": 4}, {"id": "Anzelma", "group": 4}, {"id": "Woman2", "group": 5}, {"id": "MotherInnocent", "group": 0}, {"id": "Gribier", "group": 0}, {"id": "Jondrette", "group": 7}, {"id": "Mme.Burgon", "group": 7}, {"id": "Gavroche", "group": 8}, {"id": "Gillenormand", "group": 5}, {"id": "Magnon", "group": 5}, {"id": "Mlle.Gillenormand", "group": 5}, {"id": "Mme.Pontmercy", "group": 5}, {"id": "Mlle.Vaubois", "group": 5}, {"id": "Lt.Gillenormand", "group": 5}, {"id": "Marius", "group": 8}, {"id": "BaronessT", "group": 5}, {"id": "Mabeuf", "group": 8}, {"id": "Enjolras", "group": 8}, {"id": "Combeferre", "group": 8}, {"id": "Prouvaire", "group": 8}, {"id": "Feuilly", "group": 8}, {"id": "Courfeyrac", "group": 8}, {"id": "Bahorel", "group": 8}, {"id": "Bossuet", "group": 8}, {"id": "Joly", "group": 8}, {"id": "Grantaire", "group": 8}, {"id": "MotherPlutarch", "group": 9}, {"id": "Gueulemer", "group": 4}, {"id": "Babet", "group": 4}, {"id": "Claquesous", "group": 4}, {"id": "Montparnasse", "group": 4}, {"id": "Toussaint", "group": 5}, {"id": "Child1", "group": 10}, {"id": "Child2", "group": 10}, {"id": "Brujon", "group": 4}, {"id": "Mme.Hucheloup", "group": 8} ], "links": [ {"source": "Napoleon", "target": "Myriel", "value": 1}, {"source": "Mlle.Baptistine", "target": "Myriel", "value": 8}, {"source": "Mme.Magloire", "target": "Myriel", "value": 10}, {"source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6}, {"source": "CountessdeLo", "target": "Myriel", "value": 1}, {"source": "Geborand", "target": "Myriel", "value": 1}, {"source": "Champtercier", "target": "Myriel", "value": 1}, {"source": "Cravatte", "target": "Myriel", "value": 1}, {"source": "Count", "target": "Myriel", "value": 2}, {"source": "OldMan", "target": "Myriel", "value": 1}, {"source": "Valjean", "target": "Labarre", "value": 1}, {"source": "Valjean", "target": "Mme.Magloire", "value": 3}, {"source": "Valjean", "target": "Mlle.Baptistine", "value": 3}, {"source": "Valjean", "target": "Myriel", "value": 5}, {"source": "Marguerite", "target": "Valjean", "value": 1}, {"source": "Mme.deR", "target": "Valjean", "value": 1}, {"source": "Isabeau", "target": "Valjean", "value": 1}, {"source": "Gervais", "target": "Valjean", "value": 1}, {"source": "Listolier", "target": "Tholomyes", "value": 4}, {"source": "Fameuil", "target": "Tholomyes", "value": 4}, {"source": "Fameuil", "target": "Listolier", "value": 4}, {"source": "Blacheville", "target": "Tholomyes", "value": 4}, {"source": "Blacheville", "target": "Listolier", "value": 4}, {"source": "Blacheville", "target": "Fameuil", "value": 4}, {"source": "Favourite", "target": "Tholomyes", "value": 3}, {"source": "Favourite", "target": "Listolier", "value": 3}, {"source": "Favourite", "target": "Fameuil", "value": 3}, {"source": "Favourite", "target": "Blacheville", "value": 4}, {"source": "Dahlia", "target": "Tholomyes", "value": 3}, {"source": "Dahlia", "target": "Listolier", "value": 3}, {"source": "Dahlia", "target": "Fameuil", "value": 3}, {"source": "Dahlia", "target": "Blacheville", "value": 3}, {"source": "Dahlia", "target": "Favourite", "value": 5}, {"source": "Zephine", "target": "Tholomyes", "value": 3}, {"source": "Zephine", "target": "Listolier", "value": 3}, {"source": "Zephine", "target": "Fameuil", "value": 3}, {"source": "Zephine", "target": "Blacheville", "value": 3}, {"source": "Zephine", "target": "Favourite", "value": 4}, {"source": "Zephine", "target": "Dahlia", "value": 4}, {"source": "Fantine", "target": "Tholomyes", "value": 3}, {"source": "Fantine", "target": "Listolier", "value": 3}, {"source": "Fantine", "target": "Fameuil", "value": 3}, {"source": "Fantine", "target": "Blacheville", "value": 3}, {"source": "Fantine", "target": "Favourite", "value": 4}, {"source": "Fantine", "target": "Dahlia", "value": 4}, {"source": "Fantine", "target": "Zephine", "value": 4}, {"source": "Fantine", "target": "Marguerite", "value": 2}, {"source": "Fantine", "target": "Valjean", "value": 9}, {"source": "Mme.Thenardier", "target": "Fantine", "value": 2}, {"source": "Mme.Thenardier", "target": "Valjean", "value": 7}, {"source": "Thenardier", "target": "Mme.Thenardier", "value": 13}, {"source": "Thenardier", "target": "Fantine", "value": 1}, {"source": "Thenardier", "target": "Valjean", "value": 12}, {"source": "Cosette", "target": "Mme.Thenardier", "value": 4}, {"source": "Cosette", "target": "Valjean", "value": 31}, {"source": "Cosette", "target": "Tholomyes", "value": 1}, {"source": "Cosette", "target": "Thenardier", "value": 1}, {"source": "Javert", "target": "Valjean", "value": 17}, {"source": "Javert", "target": "Fantine", "value": 5}, {"source": "Javert", "target": "Thenardier", "value": 5}, {"source": "Javert", "target": "Mme.Thenardier", "value": 1}, {"source": "Javert", "target": "Cosette", "value": 1}, {"source": "Fauchelevent", "target": "Valjean", "value": 8}, {"source": "Fauchelevent", "target": "Javert", "value": 1}, {"source": "Bamatabois", "target": "Fantine", "value": 1}, {"source": "Bamatabois", "target": "Javert", "value": 1}, {"source": "Bamatabois", "target": "Valjean", "value": 2}, {"source": "Perpetue", "target": "Fantine", "value": 1}, {"source": "Simplice", "target": "Perpetue", "value": 2}, {"source": "Simplice", "target": "Valjean", "value": 3}, {"source": "Simplice", "target": "Fantine", "value": 2}, {"source": "Simplice", "target": "Javert", "value": 1}, {"source": "Scaufflaire", "target": "Valjean", "value": 1}, {"source": "Woman1", "target": "Valjean", "value": 2}, {"source": "Woman1", "target": "Javert", "value": 1}, {"source": "Judge", "target": "Valjean", "value": 3}, {"source": "Judge", "target": "Bamatabois", "value": 2}, {"source": "Champmathieu", "target": "Valjean", "value": 3}, {"source": "Champmathieu", "target": "Judge", "value": 3}, {"source": "Champmathieu", "target": "Bamatabois", "value": 2}, {"source": "Brevet", "target": "Judge", "value": 2}, {"source": "Brevet", "target": "Champmathieu", "value": 2}, {"source": "Brevet", "target": "Valjean", "value": 2}, {"source": "Brevet", "target": "Bamatabois", "value": 1}, {"source": "Chenildieu", "target": "Judge", "value": 2}, {"source": "Chenildieu", "target": "Champmathieu", "value": 2}, {"source": "Chenildieu", "target": "Brevet", "value": 2}, {"source": "Chenildieu", "target": "Valjean", "value": 2}, {"source": "Chenildieu", "target": "Bamatabois", "value": 1}, {"source": "Cochepaille", "target": "Judge", "value": 2}, {"source": "Cochepaille", "target": "Champmathieu", "value": 2}, {"source": "Cochepaille", "target": "Brevet", "value": 2}, {"source": "Cochepaille", "target": "Chenildieu", "value": 2}, {"source": "Cochepaille", "target": "Valjean", "value": 2}, {"source": "Cochepaille", "target": "Bamatabois", "value": 1}, {"source": "Pontmercy", "target": "Thenardier", "value": 1}, {"source": "Boulatruelle", "target": "Thenardier", "value": 1}, {"source": "Eponine", "target": "Mme.Thenardier", "value": 2}, {"source": "Eponine", "target": "Thenardier", "value": 3}, {"source": "Anzelma", "target": "Eponine", "value": 2}, {"source": "Anzelma", "target": "Thenardier", "value": 2}, {"source": "Anzelma", "target": "Mme.Thenardier", "value": 1}, {"source": "Woman2", "target": "Valjean", "value": 3}, {"source": "Woman2", "target": "Cosette", "value": 1}, {"source": "Woman2", "target": "Javert", "value": 1}, {"source": "MotherInnocent", "target": "Fauchelevent", "value": 3}, {"source": "MotherInnocent", "target": "Valjean", "value": 1}, {"source": "Gribier", "target": "Fauchelevent", "value": 2}, {"source": "Mme.Burgon", "target": "Jondrette", "value": 1}, {"source": "Gavroche", "target": "Mme.Burgon", "value": 2}, {"source": "Gavroche", "target": "Thenardier", "value": 1}, {"source": "Gavroche", "target": "Javert", "value": 1}, {"source": "Gavroche", "target": "Valjean", "value": 1}, {"source": "Gillenormand", "target": "Cosette", "value": 3}, {"source": "Gillenormand", "target": "Valjean", "value": 2}, {"source": "Magnon", "target": "Gillenormand", "value": 1}, {"source": "Magnon", "target": "Mme.Thenardier", "value": 1}, {"source": "Mlle.Gillenormand", "target": "Gillenormand", "value": 9}, {"source": "Mlle.Gillenormand", "target": "Cosette", "value": 2}, {"source": "Mlle.Gillenormand", "target": "Valjean", "value": 2}, {"source": "Mme.Pontmercy", "target": "Mlle.Gillenormand", "value": 1}, {"source": "Mme.Pontmercy", "target": "Pontmercy", "value": 1}, {"source": "Mlle.Vaubois", "target": "Mlle.Gillenormand", "value": 1}, {"source": "Lt.Gillenormand", "target": "Mlle.Gillenormand", "value": 2}, {"source": "Lt.Gillenormand", "target": "Gillenormand", "value": 1}, {"source": "Lt.Gillenormand", "target": "Cosette", "value": 1}, {"source": "Marius", "target": "Mlle.Gillenormand", "value": 6}, {"source": "Marius", "target": "Gillenormand", "value": 12}, {"source": "Marius", "target": "Pontmercy", "value": 1}, {"source": "Marius", "target": "Lt.Gillenormand", "value": 1}, {"source": "Marius", "target": "Cosette", "value": 21}, {"source": "Marius", "target": "Valjean", "value": 19}, {"source": "Marius", "target": "Tholomyes", "value": 1}, {"source": "Marius", "target": "Thenardier", "value": 2}, {"source": "Marius", "target": "Eponine", "value": 5}, {"source": "Marius", "target": "Gavroche", "value": 4}, {"source": "BaronessT", "target": "Gillenormand", "value": 1}, {"source": "BaronessT", "target": "Marius", "value": 1}, {"source": "Mabeuf", "target": "Marius", "value": 1}, {"source": "Mabeuf", "target": "Eponine", "value": 1}, {"source": "Mabeuf", "target": "Gavroche", "value": 1}, {"source": "Enjolras", "target": "Marius", "value": 7}, {"source": "Enjolras", "target": "Gavroche", "value": 7}, {"source": "Enjolras", "target": "Javert", "value": 6}, {"source": "Enjolras", "target": "Mabeuf", "value": 1}, {"source": "Enjolras", "target": "Valjean", "value": 4}, {"source": "Combeferre", "target": "Enjolras", "value": 15}, {"source": "Combeferre", "target": "Marius", "value": 5}, {"source": "Combeferre", "target": "Gavroche", "value": 6}, {"source": "Combeferre", "target": "Mabeuf", "value": 2}, {"source": "Prouvaire", "target": "Gavroche", "value": 1}, {"source": "Prouvaire", "target": "Enjolras", "value": 4}, {"source": "Prouvaire", "target": "Combeferre", "value": 2}, {"source": "Feuilly", "target": "Gavroche", "value": 2}, {"source": "Feuilly", "target": "Enjolras", "value": 6}, {"source": "Feuilly", "target": "Prouvaire", "value": 2}, {"source": "Feuilly", "target": "Combeferre", "value": 5}, {"source": "Feuilly", "target": "Mabeuf", "value": 1}, {"source": "Feuilly", "target": "Marius", "value": 1}, {"source": "Courfeyrac", "target": "Marius", "value": 9}, {"source": "Courfeyrac", "target": "Enjolras", "value": 17}, {"source": "Courfeyrac", "target": "Combeferre", "value": 13}, {"source": "Courfeyrac", "target": "Gavroche", "value": 7}, {"source": "Courfeyrac", "target": "Mabeuf", "value": 2}, {"source": "Courfeyrac", "target": "Eponine", "value": 1}, {"source": "Courfeyrac", "target": "Feuilly", "value": 6}, {"source": "Courfeyrac", "target": "Prouvaire", "value": 3}, {"source": "Bahorel", "target": "Combeferre", "value": 5}, {"source": "Bahorel", "target": "Gavroche", "value": 5}, {"source": "Bahorel", "target": "Courfeyrac", "value": 6}, {"source": "Bahorel", "target": "Mabeuf", "value": 2}, {"source": "Bahorel", "target": "Enjolras", "value": 4}, {"source": "Bahorel", "target": "Feuilly", "value": 3}, {"source": "Bahorel", "target": "Prouvaire", "value": 2}, {"source": "Bahorel", "target": "Marius", "value": 1}, {"source": "Bossuet", "target": "Marius", "value": 5}, {"source": "Bossuet", "target": "Courfeyrac", "value": 12}, {"source": "Bossuet", "target": "Gavroche", "value": 5}, {"source": "Bossuet", "target": "Bahorel", "value": 4}, {"source": "Bossuet", "target": "Enjolras", "value": 10}, {"source": "Bossuet", "target": "Feuilly", "value": 6}, {"source": "Bossuet", "target": "Prouvaire", "value": 2}, {"source": "Bossuet", "target": "Combeferre", "value": 9}, {"source": "Bossuet", "target": "Mabeuf", "value": 1}, {"source": "Bossuet", "target": "Valjean", "value": 1}, {"source": "Joly", "target": "Bahorel", "value": 5}, {"source": "Joly", "target": "Bossuet", "value": 7}, {"source": "Joly", "target": "Gavroche", "value": 3}, {"source": "Joly", "target": "Courfeyrac", "value": 5}, {"source": "Joly", "target": "Enjolras", "value": 5}, {"source": "Joly", "target": "Feuilly", "value": 5}, {"source": "Joly", "target": "Prouvaire", "value": 2}, {"source": "Joly", "target": "Combeferre", "value": 5}, {"source": "Joly", "target": "Mabeuf", "value": 1}, {"source": "Joly", "target": "Marius", "value": 2}, {"source": "Grantaire", "target": "Bossuet", "value": 3}, {"source": "Grantaire", "target": "Enjolras", "value": 3}, {"source": "Grantaire", "target": "Combeferre", "value": 1}, {"source": "Grantaire", "target": "Courfeyrac", "value": 2}, {"source": "Grantaire", "target": "Joly", "value": 2}, {"source": "Grantaire", "target": "Gavroche", "value": 1}, {"source": "Grantaire", "target": "Bahorel", "value": 1}, {"source": "Grantaire", "target": "Feuilly", "value": 1}, {"source": "Grantaire", "target": "Prouvaire", "value": 1}, {"source": "MotherPlutarch", "target": "Mabeuf", "value": 3}, {"source": "Gueulemer", "target": "Thenardier", "value": 5}, {"source": "Gueulemer", "target": "Valjean", "value": 1}, {"source": "Gueulemer", "target": "Mme.Thenardier", "value": 1}, {"source": "Gueulemer", "target": "Javert", "value": 1}, {"source": "Gueulemer", "target": "Gavroche", "value": 1}, {"source": "Gueulemer", "target": "Eponine", "value": 1}, {"source": "Babet", "target": "Thenardier", "value": 6}, {"source": "Babet", "target": "Gueulemer", "value": 6}, {"source": "Babet", "target": "Valjean", "value": 1}, {"source": "Babet", "target": "Mme.Thenardier", "value": 1}, {"source": "Babet", "target": "Javert", "value": 2}, {"source": "Babet", "target": "Gavroche", "value": 1}, {"source": "Babet", "target": "Eponine", "value": 1}, {"source": "Claquesous", "target": "Thenardier", "value": 4}, {"source": "Claquesous", "target": "Babet", "value": 4}, {"source": "Claquesous", "target": "Gueulemer", "value": 4}, {"source": "Claquesous", "target": "Valjean", "value": 1}, {"source": "Claquesous", "target": "Mme.Thenardier", "value": 1}, {"source": "Claquesous", "target": "Javert", "value": 1}, {"source": "Claquesous", "target": "Eponine", "value": 1}, {"source": "Claquesous", "target": "Enjolras", "value": 1}, {"source": "Montparnasse", "target": "Javert", "value": 1}, {"source": "Montparnasse", "target": "Babet", "value": 2}, {"source": "Montparnasse", "target": "Gueulemer", "value": 2}, {"source": "Montparnasse", "target": "Claquesous", "value": 2}, {"source": "Montparnasse", "target": "Valjean", "value": 1}, {"source": "Montparnasse", "target": "Gavroche", "value": 1}, {"source": "Montparnasse", "target": "Eponine", "value": 1}, {"source": "Montparnasse", "target": "Thenardier", "value": 1}, {"source": "Toussaint", "target": "Cosette", "value": 2}, {"source": "Toussaint", "target": "Javert", "value": 1}, {"source": "Toussaint", "target": "Valjean", "value": 1}, {"source": "Child1", "target": "Gavroche", "value": 2}, {"source": "Child2", "target": "Gavroche", "value": 2}, {"source": "Child2", "target": "Child1", "value": 3}, {"source": "Brujon", "target": "Babet", "value": 3}, {"source": "Brujon", "target": "Gueulemer", "value": 3}, {"source": "Brujon", "target": "Thenardier", "value": 3}, {"source": "Brujon", "target": "Gavroche", "value": 1}, {"source": "Brujon", "target": "Eponine", "value": 1}, {"source": "Brujon", "target": "Claquesous", "value": 1}, {"source": "Brujon", "target": "Montparnasse", "value": 1}, {"source": "Mme.Hucheloup", "target": "Bossuet", "value": 1}, {"source": "Mme.Hucheloup", "target": "Joly", "value": 1}, {"source": "Mme.Hucheloup", "target": "Grantaire", "value": 1}, {"source": "Mme.Hucheloup", "target": "Bahorel", "value": 1}, {"source": "Mme.Hucheloup", "target": "Courfeyrac", "value": 1}, {"source": "Mme.Hucheloup", "target": "Gavroche", "value": 1}, {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1} ] } """ struct Miserable: Codable { struct Node: Codable, Identifiable { let id: String let group: Int } struct Edge: Codable { let source: String let target: String let value: Int } let nodes: [Node] let links: [Edge] } func getData(_ strSource: String) -> Miserable { let jd = JSONDecoder() return try! jd.decode(Miserable.self, from: strSource.data(using: .utf8)!) } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/ForceDirectedGraph3DApp.swift ================================================ // // ForceDirectedGraph3DApp.swift // ForceDirectedGraph3D // // Created by li3zhen1 on 10/20/23. // import SwiftUI @main struct ForceDirectedGraph3DApp: App { var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Info.plist ================================================ UIApplicationSceneManifest UIApplicationPreferredDefaultSceneSessionRole UIWindowSceneSessionRoleApplication UIApplicationSupportsMultipleScenes UISceneConfigurations ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 60; objects = { /* Begin PBXBuildFile section */ B7786A1C2AE2DE7800FF7CA8 /* ForceSimulation in Frameworks */ = {isa = PBXBuildFile; productRef = B7786A1B2AE2DE7800FF7CA8 /* ForceSimulation */; }; B7786A202AE2DEA000FF7CA8 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7786A1F2AE2DEA000FF7CA8 /* Data.swift */; }; B783A12C2AE2DA4900EC828F /* RealityKitContent in Frameworks */ = {isa = PBXBuildFile; productRef = B783A12B2AE2DA4900EC828F /* RealityKitContent */; }; B783A12E2AE2DA4900EC828F /* ForceDirectedGraph3DApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B783A12D2AE2DA4900EC828F /* ForceDirectedGraph3DApp.swift */; }; B783A1302AE2DA4900EC828F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B783A12F2AE2DA4900EC828F /* ContentView.swift */; }; B783A1322AE2DA4A00EC828F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B783A1312AE2DA4A00EC828F /* Assets.xcassets */; }; B783A1352AE2DA4A00EC828F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B783A1342AE2DA4A00EC828F /* Preview Assets.xcassets */; }; B7FEF0092AFD824000E3BD07 /* ForceSimulation in Frameworks */ = {isa = PBXBuildFile; productRef = B7FEF0082AFD824000E3BD07 /* ForceSimulation */; }; B7FEF00B2AFD824000E3BD07 /* Grape in Frameworks */ = {isa = PBXBuildFile; productRef = B7FEF00A2AFD824000E3BD07 /* Grape */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ B7786A1F2AE2DEA000FF7CA8 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; B783A1262AE2DA4900EC828F /* ForceDirectedGraph3D.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ForceDirectedGraph3D.app; sourceTree = BUILT_PRODUCTS_DIR; }; B783A12A2AE2DA4900EC828F /* RealityKitContent */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = RealityKitContent; sourceTree = ""; }; B783A12D2AE2DA4900EC828F /* ForceDirectedGraph3DApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceDirectedGraph3DApp.swift; sourceTree = ""; }; B783A12F2AE2DA4900EC828F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; B783A1312AE2DA4A00EC828F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B783A1342AE2DA4A00EC828F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; B783A1362AE2DA4A00EC828F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ B783A1232AE2DA4900EC828F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( B7FEF00B2AFD824000E3BD07 /* Grape in Frameworks */, B783A12C2AE2DA4900EC828F /* RealityKitContent in Frameworks */, B7786A1C2AE2DE7800FF7CA8 /* ForceSimulation in Frameworks */, B7FEF0092AFD824000E3BD07 /* ForceSimulation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ B783A11D2AE2DA4900EC828F = { isa = PBXGroup; children = ( B783A1282AE2DA4900EC828F /* ForceDirectedGraph3D */, B783A1292AE2DA4900EC828F /* Packages */, B783A1272AE2DA4900EC828F /* Products */, ); sourceTree = ""; }; B783A1272AE2DA4900EC828F /* Products */ = { isa = PBXGroup; children = ( B783A1262AE2DA4900EC828F /* ForceDirectedGraph3D.app */, ); name = Products; sourceTree = ""; }; B783A1282AE2DA4900EC828F /* ForceDirectedGraph3D */ = { isa = PBXGroup; children = ( B7786A1F2AE2DEA000FF7CA8 /* Data.swift */, B783A12D2AE2DA4900EC828F /* ForceDirectedGraph3DApp.swift */, B783A12F2AE2DA4900EC828F /* ContentView.swift */, B783A1312AE2DA4A00EC828F /* Assets.xcassets */, B783A1362AE2DA4A00EC828F /* Info.plist */, B783A1332AE2DA4A00EC828F /* Preview Content */, ); path = ForceDirectedGraph3D; sourceTree = ""; }; B783A1292AE2DA4900EC828F /* Packages */ = { isa = PBXGroup; children = ( B783A12A2AE2DA4900EC828F /* RealityKitContent */, ); path = Packages; sourceTree = ""; }; B783A1332AE2DA4A00EC828F /* Preview Content */ = { isa = PBXGroup; children = ( B783A1342AE2DA4A00EC828F /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ B783A1252AE2DA4900EC828F /* ForceDirectedGraph3D */ = { isa = PBXNativeTarget; buildConfigurationList = B783A1392AE2DA4A00EC828F /* Build configuration list for PBXNativeTarget "ForceDirectedGraph3D" */; buildPhases = ( B783A1222AE2DA4900EC828F /* Sources */, B783A1232AE2DA4900EC828F /* Frameworks */, B783A1242AE2DA4900EC828F /* Resources */, ); buildRules = ( ); dependencies = ( ); name = ForceDirectedGraph3D; packageProductDependencies = ( B783A12B2AE2DA4900EC828F /* RealityKitContent */, B7786A1B2AE2DE7800FF7CA8 /* ForceSimulation */, B7FEF0082AFD824000E3BD07 /* ForceSimulation */, B7FEF00A2AFD824000E3BD07 /* Grape */, ); productName = ForceDirectedGraph3D; productReference = B783A1262AE2DA4900EC828F /* ForceDirectedGraph3D.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ B783A11E2AE2DA4900EC828F /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1510; LastUpgradeCheck = 1510; TargetAttributes = { B783A1252AE2DA4900EC828F = { CreatedOnToolsVersion = 15.1; }; }; }; buildConfigurationList = B783A1212AE2DA4900EC828F /* Build configuration list for PBXProject "ForceDirectedGraph3D" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = B783A11D2AE2DA4900EC828F; packageReferences = ( B7786A1A2AE2DE7800FF7CA8 /* XCLocalSwiftPackageReference "../.." */, ); productRefGroup = B783A1272AE2DA4900EC828F /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( B783A1252AE2DA4900EC828F /* ForceDirectedGraph3D */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ B783A1242AE2DA4900EC828F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( B783A1352AE2DA4A00EC828F /* Preview Assets.xcassets in Resources */, B783A1322AE2DA4A00EC828F /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ B783A1222AE2DA4900EC828F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( B783A1302AE2DA4900EC828F /* ContentView.swift in Sources */, B7786A202AE2DEA000FF7CA8 /* Data.swift in Sources */, B783A12E2AE2DA4900EC828F /* ForceDirectedGraph3DApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ B783A1372AE2DA4A00EC828F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = xros; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; XROS_DEPLOYMENT_TARGET = 1.0; }; name = Debug; }; B783A1382AE2DA4A00EC828F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = xros; SWIFT_COMPILATION_MODE = wholemodule; VALIDATE_PRODUCT = YES; XROS_DEPLOYMENT_TARGET = 1.0; }; name = Release; }; B783A13A2AE2DA4A00EC828F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"ForceDirectedGraph3D/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(TARGET_NAME)/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = me.lizhen.ForceDirectedGraph3D; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "xros xrsimulator"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Debug; }; B783A13B2AE2DA4A00EC828F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = "\"ForceDirectedGraph3D/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "$(TARGET_NAME)/Info.plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = me.lizhen.ForceDirectedGraph3D; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "xros xrsimulator"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,7"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ B783A1212AE2DA4900EC828F /* Build configuration list for PBXProject "ForceDirectedGraph3D" */ = { isa = XCConfigurationList; buildConfigurations = ( B783A1372AE2DA4A00EC828F /* Debug */, B783A1382AE2DA4A00EC828F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; B783A1392AE2DA4A00EC828F /* Build configuration list for PBXNativeTarget "ForceDirectedGraph3D" */ = { isa = XCConfigurationList; buildConfigurations = ( B783A13A2AE2DA4A00EC828F /* Debug */, B783A13B2AE2DA4A00EC828F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ B7786A1A2AE2DE7800FF7CA8 /* XCLocalSwiftPackageReference "../.." */ = { isa = XCLocalSwiftPackageReference; relativePath = ../..; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ B7786A1B2AE2DE7800FF7CA8 /* ForceSimulation */ = { isa = XCSwiftPackageProductDependency; productName = ForceSimulation; }; B783A12B2AE2DA4900EC828F /* RealityKitContent */ = { isa = XCSwiftPackageProductDependency; productName = RealityKitContent; }; B7FEF0082AFD824000E3BD07 /* ForceSimulation */ = { isa = XCSwiftPackageProductDependency; productName = ForceSimulation; }; B7FEF00A2AFD824000E3BD07 /* Grape */ = { isa = XCSwiftPackageProductDependency; productName = Grape; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = B783A11E2AE2DA4900EC828F /* Project object */; } ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Examples/ForceDirectedGraph3D/ForceDirectedGraph3D.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/.build/workspace-state.json ================================================ { "object" : { "artifacts" : [ ], "dependencies" : [ ] }, "version" : 6 } ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/Package.realitycomposerpro/ProjectData/main.json ================================================ { "pathsToIds" : { "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/GridMaterial.usda" : "440DE5B4-E4E4-459B-AABF-9ACE96319272", "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/procedural_sphere_grid.usda" : "34C460AE-CA1B-4348-BD05-621ACBDFFE97", "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Scene.usda" : "0A9B4653-B11E-4D6A-850E-C6FCB621626C", "\/RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Untitled Scene.usda" : "03E02005-EFA6-48D6-8A76-05B2822A74E9", "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/GridMaterial.usda" : "FBD8436F-6B8B-4B82-99B5-995D538B4704", "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/procedural_sphere_grid.usda" : "1CBF3893-ABFD-408C-8B91-045BFD257808", "RealityKitContent\/Sources\/RealityKitContent\/RealityKitContent.rkassets\/Scene.usda" : "26DBAE76-5DD8-47B6-A085-1B4ADA111097" } } ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/SceneMetadataList.json ================================================ { "0A9B4653-B11E-4D6A-850E-C6FCB621626C" : { "cameraTransform" : [ 0.9807314, -1.9820146e-10, -0.195361, 0, -0.10051192, 0.85749435, -0.5045798, 0, 0.16752096, 0.51449335, 0.84097165, 0, 0.09084191, 0.05849296, 0.13903293, 1 ], "objectMetadataList" : [ [ "0A9B4653-B11E-4D6A-850E-C6FCB621626C", "Root" ], { "isExpanded" : true, "isLocked" : false }, [ "0A9B4653-B11E-4D6A-850E-C6FCB621626C", "Root", "GridMaterial" ], { "isExpanded" : true, "isLocked" : false }, [ "0A9B4653-B11E-4D6A-850E-C6FCB621626C", "Root", "Sphere" ], { "isExpanded" : true, "isLocked" : false } ] }, "1CBF3893-ABFD-408C-8B91-045BFD257808" : { "cameraTransform" : [ 0.99999994, 0, -0, 0, -0, 0.8660255, -0.49999988, 0, 0, 0.49999988, 0.8660255, 0, 0, 0.27093542, 0.46927398, 1 ], "objectMetadataList" : [ ] }, "03E02005-EFA6-48D6-8A76-05B2822A74E9" : { "cameraTransform" : [ 0.99999994, 0, -0, 0, -0, 0.8660254, -0.49999994, 0, 0, 0.49999994, 0.8660254, 0, 0, 0.5981957, 1.0361054, 1 ], "objectMetadataList" : [ ] }, "26DBAE76-5DD8-47B6-A085-1B4ADA111097" : { "cameraTransform" : [ 1, 0, -0, 0, -0, 0.7071069, -0.7071067, 0, 0, 0.7071067, 0.7071069, 0, 0, 0.2681068, 0.26850593, 1 ], "objectMetadataList" : [ [ "26DBAE76-5DD8-47B6-A085-1B4ADA111097", "Root" ], { "isExpanded" : true, "isLocked" : false } ] }, "34C460AE-CA1B-4348-BD05-621ACBDFFE97" : { "cameraTransform" : [ 0.99999994, 0, -0, 0, -0, 0.8660255, -0.49999988, 0, 0, 0.49999988, 0.8660255, 0, 0, 0.27093542, 0.46927398, 1 ], "objectMetadataList" : [ ] }, "440DE5B4-E4E4-459B-AABF-9ACE96319272" : { "cameraTransform" : [ 0.99999994, 0, -0, 0, -0, 0.8660254, -0.49999994, 0, 0, 0.49999994, 0.8660254, 0, 0, 0.5981957, 1.0361054, 1 ], "objectMetadataList" : [ [ "440DE5B4-E4E4-459B-AABF-9ACE96319272", "Root" ], { "isExpanded" : true, "isLocked" : false } ] }, "FBD8436F-6B8B-4B82-99B5-995D538B4704" : { "cameraTransform" : [ 0.99999994, 0, -0, 0, -0, 0.8660254, -0.49999994, 0, 0, 0.49999994, 0.8660254, 0, 0, 0.5981957, 1.0361054, 1 ], "objectMetadataList" : [ [ "FBD8436F-6B8B-4B82-99B5-995D538B4704", "Root" ], { "isExpanded" : true, "isLocked" : false } ] } } ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/Package.realitycomposerpro/WorkspaceData/Settings.rcprojectdata ================================================ { "cameraPresets" : { }, "secondaryToolbarData" : { "isGridVisible" : true, "sceneReverbPreset" : -1 }, "unitDefaults" : { "°" : "°", "kg" : "g", "m" : "cm", "m\/s" : "m\/s", "m\/s²" : "m\/s²", "s" : "s" } } ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/Package.swift ================================================ // swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "RealityKitContent", products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "RealityKitContent", targets: ["RealityKitContent"]), ], dependencies: [ // Dependencies declare other packages that this package depends on. // .package(url: /* package url */, from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "RealityKitContent", dependencies: []), ] ) ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/README.md ================================================ # RealityKitContent A description of this package. ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Materials/GridMaterial.usda ================================================ #usda 1.0 ( defaultPrim = "Root" metersPerUnit = 1 upAxis = "Y" ) def Xform "Root" { def Material "GridMaterial" { reorder nameChildren = ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "DefaultSurfaceShader", "MaterialXPreviewSurface", "Texcoord", "Add", "Multiply", "Fractional", "LineCounts", "Multiply_1", "Separate2", "Separate2_1", "Ifgreater", "Ifgreater_1", "Max", "Background_Color"] token outputs:mtlx:surface.connect = token outputs:realitykit:vertex token outputs:surface float2 ui:nodegraph:realitykit:subgraphOutputs:pos = (2222, 300.5) float2 ui:nodegraph:realitykit:subgraphOutputs:size = (182, 89) int ui:nodegraph:realitykit:subgraphOutputs:stackingOrder = 749 def Shader "DefaultSurfaceShader" { uniform token info:id = "UsdPreviewSurface" color3f inputs:diffuseColor = (1, 1, 1) float inputs:roughness = 0.75 token outputs:surface } def Shader "MaterialXPreviewSurface" { uniform token info:id = "ND_UsdPreviewSurface_surfaceshader" float inputs:clearcoat float inputs:clearcoatRoughness color3f inputs:diffuseColor.connect = color3f inputs:emissiveColor float inputs:ior float inputs:metallic = 0.15 float3 inputs:normal float inputs:occlusion float inputs:opacity float inputs:opacityThreshold float inputs:roughness = 0.5 token outputs:out float2 ui:nodegraph:node:pos = (1967, 300.5) float2 ui:nodegraph:node:size = (208, 297) int ui:nodegraph:node:stackingOrder = 870 string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["Advanced"] } def Shader "Texcoord" { uniform token info:id = "ND_texcoord_vector2" float2 outputs:out float2 ui:nodegraph:node:pos = (94.14453, 35.29297) float2 ui:nodegraph:node:size = (182, 43) int ui:nodegraph:node:stackingOrder = 1358 } def Shader "Multiply" { uniform token info:id = "ND_multiply_vector2" float2 inputs:in1.connect = float2 inputs:in2 = (32, 15) float2 inputs:in2.connect = float2 outputs:out float2 ui:nodegraph:node:pos = (275.64453, 47.29297) float2 ui:nodegraph:node:size = (61, 36) int ui:nodegraph:node:stackingOrder = 1348 string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["inputs:in2"] } def Shader "Fractional" { uniform token info:id = "ND_realitykit_fractional_vector2" float2 inputs:in.connect = float2 outputs:out float2 ui:nodegraph:node:pos = (440.5, 49.5) float2 ui:nodegraph:node:size = (155, 99) int ui:nodegraph:node:stackingOrder = 1345 } def Shader "BaseColor" { uniform token info:id = "ND_constant_color3" color3f inputs:value = (0.89737034, 0.89737034, 0.89737034) ( colorSpace = "Input - Texture - sRGB - sRGB" ) color3f inputs:value.connect = None color3f outputs:out float2 ui:nodegraph:node:pos = (1537.5977, 363.07812) float2 ui:nodegraph:node:size = (150, 43) int ui:nodegraph:node:stackingOrder = 1353 } def Shader "LineColor" { uniform token info:id = "ND_constant_color3" color3f inputs:value = (0.55945957, 0.55945957, 0.55945957) ( colorSpace = "Input - Texture - sRGB - sRGB" ) color3f inputs:value.connect = None color3f outputs:out float2 ui:nodegraph:node:pos = (1536.9844, 287.86328) float2 ui:nodegraph:node:size = (146, 43) int ui:nodegraph:node:stackingOrder = 1355 } def Shader "LineWidths" { uniform token info:id = "ND_combine2_vector2" float inputs:in1 = 0.1 float inputs:in2 = 0.1 float2 outputs:out float2 ui:nodegraph:node:pos = (443.64453, 233.79297) float2 ui:nodegraph:node:size = (151, 43) int ui:nodegraph:node:stackingOrder = 1361 } def Shader "LineCounts" { uniform token info:id = "ND_combine2_vector2" float inputs:in1 = 24 float inputs:in2 = 12 float2 outputs:out float2 ui:nodegraph:node:pos = (94.14453, 138.29297) float2 ui:nodegraph:node:size = (153, 43) int ui:nodegraph:node:stackingOrder = 1359 } def Shader "Remap" { uniform token info:id = "ND_remap_color3" color3f inputs:in.connect = color3f inputs:inhigh.connect = None color3f inputs:inlow.connect = None color3f inputs:outhigh.connect = color3f inputs:outlow.connect = color3f outputs:out float2 ui:nodegraph:node:pos = (1755.5, 300.5) float2 ui:nodegraph:node:size = (95, 171) int ui:nodegraph:node:stackingOrder = 1282 string[] ui:nodegraph:realitykit:node:attributesShowingChildren = ["inputs:outlow"] } def Shader "Separate2" { uniform token info:id = "ND_separate2_vector2" float2 inputs:in.connect = float outputs:outx float outputs:outy float2 ui:nodegraph:node:pos = (1212.6445, 128.91797) float2 ui:nodegraph:node:size = (116, 117) int ui:nodegraph:node:stackingOrder = 1363 } def Shader "Combine3" { uniform token info:id = "ND_combine3_color3" float inputs:in1.connect = float inputs:in2.connect = float inputs:in3.connect = color3f outputs:out float2 ui:nodegraph:node:pos = (1578.1445, 128.91797) float2 ui:nodegraph:node:size = (146, 54) int ui:nodegraph:node:stackingOrder = 1348 } def Shader "Range" { uniform token info:id = "ND_range_vector2" bool inputs:doclamp = 1 float2 inputs:gamma = (2, 2) float2 inputs:in.connect = float2 inputs:inhigh.connect = float2 inputs:inlow = (0.02, 0.02) float2 inputs:outhigh float2 inputs:outlow float2 outputs:out float2 ui:nodegraph:node:pos = (990.64453, 128.91797) float2 ui:nodegraph:node:size = (98, 207) int ui:nodegraph:node:stackingOrder = 1364 } def Shader "Subtract" { uniform token info:id = "ND_subtract_vector2" float2 inputs:in1.connect = float2 inputs:in2.connect = float2 outputs:out float2 ui:nodegraph:node:pos = (612.64453, 87.04297) float2 ui:nodegraph:node:size = (63, 36) int ui:nodegraph:node:stackingOrder = 1348 } def Shader "Absval" { uniform token info:id = "ND_absval_vector2" float2 inputs:in.connect = float2 outputs:out float2 ui:nodegraph:node:pos = (765.64453, 87.04297) float2 ui:nodegraph:node:size = (123, 43) int ui:nodegraph:node:stackingOrder = 1348 } def Shader "Min" { uniform token info:id = "ND_min_float" float inputs:in1.connect = float inputs:in2.connect = float outputs:out float2 ui:nodegraph:node:pos = (1388.1445, 128.91797) float2 ui:nodegraph:node:size = (114, 36) int ui:nodegraph:node:stackingOrder = 1363 } } } ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.rkassets/Scene.usda ================================================ #usda 1.0 ( defaultPrim = "Root" metersPerUnit = 1 upAxis = "Y" ) def Xform "Root" { reorder nameChildren = ["GridMaterial", "Sphere"] rel material:binding = None ( bindMaterialAs = "weakerThanDescendants" ) def Sphere "Sphere" ( active = true prepend apiSchemas = ["MaterialBindingAPI"] ) { rel material:binding = ( bindMaterialAs = "weakerThanDescendants" ) double radius = 0.05 quatf xformOp:orient = (1, 0, 0, 0) float3 xformOp:scale = (1, 1, 1) float3 xformOp:translate = (0, 0, 0.0004) uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] def RealityKitComponent "Collider" { uint group = 1 uniform token info:id = "RealityKit.Collider" uint mask = 4294967295 token type = "Default" def RealityKitStruct "Shape" { float3 extent = (0.2, 0.2, 0.2) float radius = 0.05 token shapeType = "Sphere" } } def RealityKitComponent "InputTarget" { uniform token info:id = "RealityKit.InputTarget" } } def "GridMaterial" ( active = true prepend references = @Materials/GridMaterial.usda@ ) { float3 xformOp:scale = (1, 1, 1) uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"] } } ================================================ FILE: Examples/ForceDirectedGraph3D/Packages/RealityKitContent/Sources/RealityKitContent/RealityKitContent.swift ================================================ import Foundation /// Bundle for the RealityKitContent project public let realityKitContentBundle = Bundle.module ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ContentView.swift ================================================ // // ContentView.swift // GrapeView // // Created by li3zhen1 on 10/8/23. // import Grape import SwiftUI let colors: [Color] = [ .init(red: 17.0/255, green: 181.0/255, blue: 174.0/255), .init(red: 64.0/255, green: 70.0/255, blue: 201.0/255), .init(red: 246.0/255, green: 133.0/255, blue: 18.0/255), .init(red: 222.0/255, green: 60.0/255, blue: 130.0/255), .init(red: 17.0/255, green: 181.0/255, blue: 174.0/255), .init(red: 114.0/255, green: 224.0/255, blue: 106.0/255), .init(red: 22.0/255, green: 124.0/255, blue: 243.0/255), .init(red: 115.0/255, green: 38.0/255, blue: 211.0/255), .init(red: 232.0/255, green: 198.0/255, blue: 0.0/255), .init(red: 203.0/255, green: 93.0/255, blue: 2.0/255), .init(red: 0.0/255, green: 143.0/255, blue: 93.0/255), .init(red: 188.0/255, green: 233.0/255, blue: 49.0/255), ] enum ExampleKind: Identifiable, Hashable { case ring case classicMiserable case lattice case mermaid var id: ExampleKind { self } static let list: [ExampleKind] = [.ring, .classicMiserable, .lattice, .mermaid] } extension ExampleKind { var description: String { switch self { case .ring: return "My Ring" case .mermaid: return "Mermaid visualization" case .classicMiserable: return "Les Misérables" case .lattice: return "Lattice" } } } struct ContentView: View { @State var selection: ExampleKind? = .ring var body: some View { NavigationSplitView { List(ExampleKind.list, selection: $selection) { kind in Text(kind.description) } } detail: { switch selection { case .ring: MyRing() case .classicMiserable: MiserableGraph() case .lattice: Lattice() case .mermaid: MermaidVisualization() case .none: MermaidVisualization() } } } } #Preview { ContentView() } struct MyGraph: View { let myNodes = ["A", "B", "C"] let myLinks = [("A", "B"), ("B", "C")] var body: some View { ForceDirectedGraph { Series(myNodes) { id in NodeMark(id: id) } Series(myLinks) { from, to in LinkMark(from: from, to: to) } } } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Data.swift ================================================ // // miserables.swift // GrapeView // // Created by li3zhen1 on 10/8/23. // import Foundation let miserables3 = """ { "nodes": [ {"id": "Myriel", "group": 1}, {"id": "Napoleon", "group": 1}, ], "links": [ {"source": "Myriel", "target": "Napoleon", "value": 3}, ] } """ let miserables2 = """ { "nodes": [ {"id": "Myriel", "group": 1}, {"id": "Napoleon", "group": 1}, {"id": "Mlle.Baptistine", "group": 1}, {"id": "Valjean", "group": 2}, {"id": "Marguerite", "group": 3}, {"id": "Mme.deR", "group": 2}, ], "links": [ {"source": "Myriel", "target": "Napoleon", "value": 3}, {"source": "Myriel", "target": "Mlle.Baptistine", "value": 3}, {"source": "Napoleon", "target": "Mme.deR", "value": 3}, {"source": "Mlle.Baptistine", "target": "Valjean", "value": 3} ] } """ let miserables = """ { "nodes": [ {"id": "Myriel", "group": 1}, {"id": "Napoleon", "group": 1}, {"id": "Mlle.Baptistine", "group": 1}, {"id": "Mme.Magloire", "group": 1}, {"id": "CountessdeLo", "group": 1}, {"id": "Geborand", "group": 1}, {"id": "Champtercier", "group": 1}, {"id": "Cravatte", "group": 1}, {"id": "Count", "group": 1}, {"id": "OldMan", "group": 1}, {"id": "Labarre", "group": 2}, {"id": "Valjean", "group": 2}, {"id": "Marguerite", "group": 3}, {"id": "Mme.deR", "group": 2}, {"id": "Isabeau", "group": 2}, {"id": "Gervais", "group": 2}, {"id": "Tholomyes", "group": 3}, {"id": "Listolier", "group": 3}, {"id": "Fameuil", "group": 3}, {"id": "Blacheville", "group": 3}, {"id": "Favourite", "group": 3}, {"id": "Dahlia", "group": 3}, {"id": "Zephine", "group": 3}, {"id": "Fantine", "group": 3}, {"id": "Mme.Thenardier", "group": 4}, {"id": "Thenardier", "group": 4}, {"id": "Cosette", "group": 5}, {"id": "Javert", "group": 4}, {"id": "Fauchelevent", "group": 0}, {"id": "Bamatabois", "group": 2}, {"id": "Perpetue", "group": 3}, {"id": "Simplice", "group": 2}, {"id": "Scaufflaire", "group": 2}, {"id": "Woman1", "group": 2}, {"id": "Judge", "group": 2}, {"id": "Champmathieu", "group": 2}, {"id": "Brevet", "group": 2}, {"id": "Chenildieu", "group": 2}, {"id": "Cochepaille", "group": 2}, {"id": "Pontmercy", "group": 4}, {"id": "Boulatruelle", "group": 6}, {"id": "Eponine", "group": 4}, {"id": "Anzelma", "group": 4}, {"id": "Woman2", "group": 5}, {"id": "MotherInnocent", "group": 0}, {"id": "Gribier", "group": 0}, {"id": "Jondrette", "group": 7}, {"id": "Mme.Burgon", "group": 7}, {"id": "Gavroche", "group": 8}, {"id": "Gillenormand", "group": 5}, {"id": "Magnon", "group": 5}, {"id": "Mlle.Gillenormand", "group": 5}, {"id": "Mme.Pontmercy", "group": 5}, {"id": "Mlle.Vaubois", "group": 5}, {"id": "Lt.Gillenormand", "group": 5}, {"id": "Marius", "group": 8}, {"id": "BaronessT", "group": 5}, {"id": "Mabeuf", "group": 8}, {"id": "Enjolras", "group": 8}, {"id": "Combeferre", "group": 8}, {"id": "Prouvaire", "group": 8}, {"id": "Feuilly", "group": 8}, {"id": "Courfeyrac", "group": 8}, {"id": "Bahorel", "group": 8}, {"id": "Bossuet", "group": 8}, {"id": "Joly", "group": 8}, {"id": "Grantaire", "group": 8}, {"id": "MotherPlutarch", "group": 9}, {"id": "Gueulemer", "group": 4}, {"id": "Babet", "group": 4}, {"id": "Claquesous", "group": 4}, {"id": "Montparnasse", "group": 4}, {"id": "Toussaint", "group": 5}, {"id": "Child1", "group": 10}, {"id": "Child2", "group": 10}, {"id": "Brujon", "group": 4}, {"id": "Mme.Hucheloup", "group": 8} ], "links": [ {"source": "Napoleon", "target": "Myriel", "value": 1}, {"source": "Mlle.Baptistine", "target": "Myriel", "value": 8}, {"source": "Mme.Magloire", "target": "Myriel", "value": 10}, {"source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6}, {"source": "CountessdeLo", "target": "Myriel", "value": 1}, {"source": "Geborand", "target": "Myriel", "value": 1}, {"source": "Champtercier", "target": "Myriel", "value": 1}, {"source": "Cravatte", "target": "Myriel", "value": 1}, {"source": "Count", "target": "Myriel", "value": 2}, {"source": "OldMan", "target": "Myriel", "value": 1}, {"source": "Valjean", "target": "Labarre", "value": 1}, {"source": "Valjean", "target": "Mme.Magloire", "value": 3}, {"source": "Valjean", "target": "Mlle.Baptistine", "value": 3}, {"source": "Valjean", "target": "Myriel", "value": 5}, {"source": "Marguerite", "target": "Valjean", "value": 1}, {"source": "Mme.deR", "target": "Valjean", "value": 1}, {"source": "Isabeau", "target": "Valjean", "value": 1}, {"source": "Gervais", "target": "Valjean", "value": 1}, {"source": "Listolier", "target": "Tholomyes", "value": 4}, {"source": "Fameuil", "target": "Tholomyes", "value": 4}, {"source": "Fameuil", "target": "Listolier", "value": 4}, {"source": "Blacheville", "target": "Tholomyes", "value": 4}, {"source": "Blacheville", "target": "Listolier", "value": 4}, {"source": "Blacheville", "target": "Fameuil", "value": 4}, {"source": "Favourite", "target": "Tholomyes", "value": 3}, {"source": "Favourite", "target": "Listolier", "value": 3}, {"source": "Favourite", "target": "Fameuil", "value": 3}, {"source": "Favourite", "target": "Blacheville", "value": 4}, {"source": "Dahlia", "target": "Tholomyes", "value": 3}, {"source": "Dahlia", "target": "Listolier", "value": 3}, {"source": "Dahlia", "target": "Fameuil", "value": 3}, {"source": "Dahlia", "target": "Blacheville", "value": 3}, {"source": "Dahlia", "target": "Favourite", "value": 5}, {"source": "Zephine", "target": "Tholomyes", "value": 3}, {"source": "Zephine", "target": "Listolier", "value": 3}, {"source": "Zephine", "target": "Fameuil", "value": 3}, {"source": "Zephine", "target": "Blacheville", "value": 3}, {"source": "Zephine", "target": "Favourite", "value": 4}, {"source": "Zephine", "target": "Dahlia", "value": 4}, {"source": "Fantine", "target": "Tholomyes", "value": 3}, {"source": "Fantine", "target": "Listolier", "value": 3}, {"source": "Fantine", "target": "Fameuil", "value": 3}, {"source": "Fantine", "target": "Blacheville", "value": 3}, {"source": "Fantine", "target": "Favourite", "value": 4}, {"source": "Fantine", "target": "Dahlia", "value": 4}, {"source": "Fantine", "target": "Zephine", "value": 4}, {"source": "Fantine", "target": "Marguerite", "value": 2}, {"source": "Fantine", "target": "Valjean", "value": 9}, {"source": "Mme.Thenardier", "target": "Fantine", "value": 2}, {"source": "Mme.Thenardier", "target": "Valjean", "value": 7}, {"source": "Thenardier", "target": "Mme.Thenardier", "value": 13}, {"source": "Thenardier", "target": "Fantine", "value": 1}, {"source": "Thenardier", "target": "Valjean", "value": 12}, {"source": "Cosette", "target": "Mme.Thenardier", "value": 4}, {"source": "Cosette", "target": "Valjean", "value": 31}, {"source": "Cosette", "target": "Tholomyes", "value": 1}, {"source": "Cosette", "target": "Thenardier", "value": 1}, {"source": "Javert", "target": "Valjean", "value": 17}, {"source": "Javert", "target": "Fantine", "value": 5}, {"source": "Javert", "target": "Thenardier", "value": 5}, {"source": "Javert", "target": "Mme.Thenardier", "value": 1}, {"source": "Javert", "target": "Cosette", "value": 1}, {"source": "Fauchelevent", "target": "Valjean", "value": 8}, {"source": "Fauchelevent", "target": "Javert", "value": 1}, {"source": "Bamatabois", "target": "Fantine", "value": 1}, {"source": "Bamatabois", "target": "Javert", "value": 1}, {"source": "Bamatabois", "target": "Valjean", "value": 2}, {"source": "Perpetue", "target": "Fantine", "value": 1}, {"source": "Simplice", "target": "Perpetue", "value": 2}, {"source": "Simplice", "target": "Valjean", "value": 3}, {"source": "Simplice", "target": "Fantine", "value": 2}, {"source": "Simplice", "target": "Javert", "value": 1}, {"source": "Scaufflaire", "target": "Valjean", "value": 1}, {"source": "Woman1", "target": "Valjean", "value": 2}, {"source": "Woman1", "target": "Javert", "value": 1}, {"source": "Judge", "target": "Valjean", "value": 3}, {"source": "Judge", "target": "Bamatabois", "value": 2}, {"source": "Champmathieu", "target": "Valjean", "value": 3}, {"source": "Champmathieu", "target": "Judge", "value": 3}, {"source": "Champmathieu", "target": "Bamatabois", "value": 2}, {"source": "Brevet", "target": "Judge", "value": 2}, {"source": "Brevet", "target": "Champmathieu", "value": 2}, {"source": "Brevet", "target": "Valjean", "value": 2}, {"source": "Brevet", "target": "Bamatabois", "value": 1}, {"source": "Chenildieu", "target": "Judge", "value": 2}, {"source": "Chenildieu", "target": "Champmathieu", "value": 2}, {"source": "Chenildieu", "target": "Brevet", "value": 2}, {"source": "Chenildieu", "target": "Valjean", "value": 2}, {"source": "Chenildieu", "target": "Bamatabois", "value": 1}, {"source": "Cochepaille", "target": "Judge", "value": 2}, {"source": "Cochepaille", "target": "Champmathieu", "value": 2}, {"source": "Cochepaille", "target": "Brevet", "value": 2}, {"source": "Cochepaille", "target": "Chenildieu", "value": 2}, {"source": "Cochepaille", "target": "Valjean", "value": 2}, {"source": "Cochepaille", "target": "Bamatabois", "value": 1}, {"source": "Pontmercy", "target": "Thenardier", "value": 1}, {"source": "Boulatruelle", "target": "Thenardier", "value": 1}, {"source": "Eponine", "target": "Mme.Thenardier", "value": 2}, {"source": "Eponine", "target": "Thenardier", "value": 3}, {"source": "Anzelma", "target": "Eponine", "value": 2}, {"source": "Anzelma", "target": "Thenardier", "value": 2}, {"source": "Anzelma", "target": "Mme.Thenardier", "value": 1}, {"source": "Woman2", "target": "Valjean", "value": 3}, {"source": "Woman2", "target": "Cosette", "value": 1}, {"source": "Woman2", "target": "Javert", "value": 1}, {"source": "MotherInnocent", "target": "Fauchelevent", "value": 3}, {"source": "MotherInnocent", "target": "Valjean", "value": 1}, {"source": "Gribier", "target": "Fauchelevent", "value": 2}, {"source": "Mme.Burgon", "target": "Jondrette", "value": 1}, {"source": "Gavroche", "target": "Mme.Burgon", "value": 2}, {"source": "Gavroche", "target": "Thenardier", "value": 1}, {"source": "Gavroche", "target": "Javert", "value": 1}, {"source": "Gavroche", "target": "Valjean", "value": 1}, {"source": "Gillenormand", "target": "Cosette", "value": 3}, {"source": "Gillenormand", "target": "Valjean", "value": 2}, {"source": "Magnon", "target": "Gillenormand", "value": 1}, {"source": "Magnon", "target": "Mme.Thenardier", "value": 1}, {"source": "Mlle.Gillenormand", "target": "Gillenormand", "value": 9}, {"source": "Mlle.Gillenormand", "target": "Cosette", "value": 2}, {"source": "Mlle.Gillenormand", "target": "Valjean", "value": 2}, {"source": "Mme.Pontmercy", "target": "Mlle.Gillenormand", "value": 1}, {"source": "Mme.Pontmercy", "target": "Pontmercy", "value": 1}, {"source": "Mlle.Vaubois", "target": "Mlle.Gillenormand", "value": 1}, {"source": "Lt.Gillenormand", "target": "Mlle.Gillenormand", "value": 2}, {"source": "Lt.Gillenormand", "target": "Gillenormand", "value": 1}, {"source": "Lt.Gillenormand", "target": "Cosette", "value": 1}, {"source": "Marius", "target": "Mlle.Gillenormand", "value": 6}, {"source": "Marius", "target": "Gillenormand", "value": 12}, {"source": "Marius", "target": "Pontmercy", "value": 1}, {"source": "Marius", "target": "Lt.Gillenormand", "value": 1}, {"source": "Marius", "target": "Cosette", "value": 21}, {"source": "Marius", "target": "Valjean", "value": 19}, {"source": "Marius", "target": "Tholomyes", "value": 1}, {"source": "Marius", "target": "Thenardier", "value": 2}, {"source": "Marius", "target": "Eponine", "value": 5}, {"source": "Marius", "target": "Gavroche", "value": 4}, {"source": "BaronessT", "target": "Gillenormand", "value": 1}, {"source": "BaronessT", "target": "Marius", "value": 1}, {"source": "Mabeuf", "target": "Marius", "value": 1}, {"source": "Mabeuf", "target": "Eponine", "value": 1}, {"source": "Mabeuf", "target": "Gavroche", "value": 1}, {"source": "Enjolras", "target": "Marius", "value": 7}, {"source": "Enjolras", "target": "Gavroche", "value": 7}, {"source": "Enjolras", "target": "Javert", "value": 6}, {"source": "Enjolras", "target": "Mabeuf", "value": 1}, {"source": "Enjolras", "target": "Valjean", "value": 4}, {"source": "Combeferre", "target": "Enjolras", "value": 15}, {"source": "Combeferre", "target": "Marius", "value": 5}, {"source": "Combeferre", "target": "Gavroche", "value": 6}, {"source": "Combeferre", "target": "Mabeuf", "value": 2}, {"source": "Prouvaire", "target": "Gavroche", "value": 1}, {"source": "Prouvaire", "target": "Enjolras", "value": 4}, {"source": "Prouvaire", "target": "Combeferre", "value": 2}, {"source": "Feuilly", "target": "Gavroche", "value": 2}, {"source": "Feuilly", "target": "Enjolras", "value": 6}, {"source": "Feuilly", "target": "Prouvaire", "value": 2}, {"source": "Feuilly", "target": "Combeferre", "value": 5}, {"source": "Feuilly", "target": "Mabeuf", "value": 1}, {"source": "Feuilly", "target": "Marius", "value": 1}, {"source": "Courfeyrac", "target": "Marius", "value": 9}, {"source": "Courfeyrac", "target": "Enjolras", "value": 17}, {"source": "Courfeyrac", "target": "Combeferre", "value": 13}, {"source": "Courfeyrac", "target": "Gavroche", "value": 7}, {"source": "Courfeyrac", "target": "Mabeuf", "value": 2}, {"source": "Courfeyrac", "target": "Eponine", "value": 1}, {"source": "Courfeyrac", "target": "Feuilly", "value": 6}, {"source": "Courfeyrac", "target": "Prouvaire", "value": 3}, {"source": "Bahorel", "target": "Combeferre", "value": 5}, {"source": "Bahorel", "target": "Gavroche", "value": 5}, {"source": "Bahorel", "target": "Courfeyrac", "value": 6}, {"source": "Bahorel", "target": "Mabeuf", "value": 2}, {"source": "Bahorel", "target": "Enjolras", "value": 4}, {"source": "Bahorel", "target": "Feuilly", "value": 3}, {"source": "Bahorel", "target": "Prouvaire", "value": 2}, {"source": "Bahorel", "target": "Marius", "value": 1}, {"source": "Bossuet", "target": "Marius", "value": 5}, {"source": "Bossuet", "target": "Courfeyrac", "value": 12}, {"source": "Bossuet", "target": "Gavroche", "value": 5}, {"source": "Bossuet", "target": "Bahorel", "value": 4}, {"source": "Bossuet", "target": "Enjolras", "value": 10}, {"source": "Bossuet", "target": "Feuilly", "value": 6}, {"source": "Bossuet", "target": "Prouvaire", "value": 2}, {"source": "Bossuet", "target": "Combeferre", "value": 9}, {"source": "Bossuet", "target": "Mabeuf", "value": 1}, {"source": "Bossuet", "target": "Valjean", "value": 1}, {"source": "Joly", "target": "Bahorel", "value": 5}, {"source": "Joly", "target": "Bossuet", "value": 7}, {"source": "Joly", "target": "Gavroche", "value": 3}, {"source": "Joly", "target": "Courfeyrac", "value": 5}, {"source": "Joly", "target": "Enjolras", "value": 5}, {"source": "Joly", "target": "Feuilly", "value": 5}, {"source": "Joly", "target": "Prouvaire", "value": 2}, {"source": "Joly", "target": "Combeferre", "value": 5}, {"source": "Joly", "target": "Mabeuf", "value": 1}, {"source": "Joly", "target": "Marius", "value": 2}, {"source": "Grantaire", "target": "Bossuet", "value": 3}, {"source": "Grantaire", "target": "Enjolras", "value": 3}, {"source": "Grantaire", "target": "Combeferre", "value": 1}, {"source": "Grantaire", "target": "Courfeyrac", "value": 2}, {"source": "Grantaire", "target": "Joly", "value": 2}, {"source": "Grantaire", "target": "Gavroche", "value": 1}, {"source": "Grantaire", "target": "Bahorel", "value": 1}, {"source": "Grantaire", "target": "Feuilly", "value": 1}, {"source": "Grantaire", "target": "Prouvaire", "value": 1}, {"source": "MotherPlutarch", "target": "Mabeuf", "value": 3}, {"source": "Gueulemer", "target": "Thenardier", "value": 5}, {"source": "Gueulemer", "target": "Valjean", "value": 1}, {"source": "Gueulemer", "target": "Mme.Thenardier", "value": 1}, {"source": "Gueulemer", "target": "Javert", "value": 1}, {"source": "Gueulemer", "target": "Gavroche", "value": 1}, {"source": "Gueulemer", "target": "Eponine", "value": 1}, {"source": "Babet", "target": "Thenardier", "value": 6}, {"source": "Babet", "target": "Gueulemer", "value": 6}, {"source": "Babet", "target": "Valjean", "value": 1}, {"source": "Babet", "target": "Mme.Thenardier", "value": 1}, {"source": "Babet", "target": "Javert", "value": 2}, {"source": "Babet", "target": "Gavroche", "value": 1}, {"source": "Babet", "target": "Eponine", "value": 1}, {"source": "Claquesous", "target": "Thenardier", "value": 4}, {"source": "Claquesous", "target": "Babet", "value": 4}, {"source": "Claquesous", "target": "Gueulemer", "value": 4}, {"source": "Claquesous", "target": "Valjean", "value": 1}, {"source": "Claquesous", "target": "Mme.Thenardier", "value": 1}, {"source": "Claquesous", "target": "Javert", "value": 1}, {"source": "Claquesous", "target": "Eponine", "value": 1}, {"source": "Claquesous", "target": "Enjolras", "value": 1}, {"source": "Montparnasse", "target": "Javert", "value": 1}, {"source": "Montparnasse", "target": "Babet", "value": 2}, {"source": "Montparnasse", "target": "Gueulemer", "value": 2}, {"source": "Montparnasse", "target": "Claquesous", "value": 2}, {"source": "Montparnasse", "target": "Valjean", "value": 1}, {"source": "Montparnasse", "target": "Gavroche", "value": 1}, {"source": "Montparnasse", "target": "Eponine", "value": 1}, {"source": "Montparnasse", "target": "Thenardier", "value": 1}, {"source": "Toussaint", "target": "Cosette", "value": 2}, {"source": "Toussaint", "target": "Javert", "value": 1}, {"source": "Toussaint", "target": "Valjean", "value": 1}, {"source": "Child1", "target": "Gavroche", "value": 2}, {"source": "Child2", "target": "Gavroche", "value": 2}, {"source": "Child2", "target": "Child1", "value": 3}, {"source": "Brujon", "target": "Babet", "value": 3}, {"source": "Brujon", "target": "Gueulemer", "value": 3}, {"source": "Brujon", "target": "Thenardier", "value": 3}, {"source": "Brujon", "target": "Gavroche", "value": 1}, {"source": "Brujon", "target": "Eponine", "value": 1}, {"source": "Brujon", "target": "Claquesous", "value": 1}, {"source": "Brujon", "target": "Montparnasse", "value": 1}, {"source": "Mme.Hucheloup", "target": "Bossuet", "value": 1}, {"source": "Mme.Hucheloup", "target": "Joly", "value": 1}, {"source": "Mme.Hucheloup", "target": "Grantaire", "value": 1}, {"source": "Mme.Hucheloup", "target": "Bahorel", "value": 1}, {"source": "Mme.Hucheloup", "target": "Courfeyrac", "value": 1}, {"source": "Mme.Hucheloup", "target": "Gavroche", "value": 1}, {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1} ] } """ struct Miserable: Codable { struct Node: Codable, Identifiable { let id: String let group: Int } struct Edge: Codable { let source: String let target: String let value: Int } let nodes: [Node] let links: [Edge] } func getData(_ strSource: String) -> Miserable { let jd = JSONDecoder() return try! jd.decode(Miserable.self, from: strSource.data(using: .utf8)!) } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphExample.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/ForceDirectedGraphExampleApp.swift ================================================ // // ForceDirectedGraphExampleApp.swift // ForceDirectedGraphExample // // Created by li3zhen1 on 10/17/23. // import SwiftUI @main struct ForceDirectedGraphExampleApp: App { var body: some Scene { WindowGroup { ContentView() } } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/GraphStateToolbar.swift ================================================ // // GraphStateToolbar.swift // ForceDirectedGraphExample // // Created by li3zhen1 on 2/22/24. // import Foundation import SwiftUI import Grape struct GraphStateToggle: View { @Bindable var graphStates: ForceDirectedGraphState var body: some View { Group { Button { graphStates.modelTransform.scaling(by: 0.9) } label: { Image(systemName: "minus") } Text(String(format:"Scale: %.2f", graphStates.modelTransform.scale)) .fontDesign(.monospaced) Button { graphStates.modelTransform.scaling(by: 1.1) } label: { Image(systemName: "plus") } } Button { graphStates.isRunning.toggle() } label: { Image(systemName: graphStates.isRunning ? "pause.fill" : "play.fill") Text(graphStates.isRunning ? "Pause" : "Start") } } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Lattice.swift ================================================ // // Lattice.swift // ForceDirectedGraphExample // // Created by li3zhen1 on 11/8/23. // import SwiftUI import Grape struct Lattice: View { let width = 30 let edge: [(Int, Int)] @State var graphStates = ForceDirectedGraphState( initialIsRunning: true ) init() { var edge = [(Int, Int)]() for i in 0.. Rachel Xavier -> José José -> アキラ アキラ -> Liam """ var tappedNode: String? = nil var parsedGraph: ([String], [(String, String)]) { parseMermaid(graphSyntax) } } struct MermaidVisualization: View { @State private var model: MermaidModel = .init() // the view for label @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 body: some View { let parsedGraph = model.parsedGraph ForceDirectedGraph { Series(parsedGraph.0) { node in AnnotationNodeMark(id: node, radius: 16) { getLabel(node) } } Series(parsedGraph.1) { link in LinkMark(from: link.0, to: link.1) } .linkShape(.arrow) .stroke(.black, StrokeStyle(lineWidth: 2.0, lineCap: .round, lineJoin: .round)) } force: { .manyBody() .link(originalLength: 50.0) .center() } emittingNewNodesWithStates: { id in KineticState(position: getInitialPosition(id: id, r: 100)) } .graphOverlay(content: { proxy in Rectangle().fill(.clear).contentShape(Rectangle()) .withGraphDragGesture(proxy, of: String.self) .onTapGesture { value in if let nodeID = proxy.node(of: String.self, at: value) { model.tappedNode = nodeID } } }) .ignoresSafeArea() #if !os(visionOS) .inspector(isPresented: .constant(true)) { MermaidInspector(model: model) } #endif } } struct MermaidInspector: View { @State var model: MermaidModel init(model: MermaidModel) { self.model = model } var body: some View { VStack { Text("Tapped: \(model.tappedNode ?? "nil")") .font(.title2) Divider() Text("Edit the mermaid syntaxes to update the graph") .font(.title2) TextEditor(text: $model.graphSyntax) .fontDesign(.monospaced) }.padding(.top) } } let multipleNodeRegex = Regex { "{" ZeroOrMore(.whitespace) ZeroOrMore { Capture (OneOrMore(.word)) ZeroOrMore(.whitespace) "," ZeroOrMore(.whitespace) } Capture (OneOrMore(.word)) ZeroOrMore(.whitespace) "}" } let singleNodeRegex = Regex { Capture( OneOrMore(.word) ) } let mermaidLinkRegex = Regex { singleNodeRegex OneOrMore(.whitespace) ChoiceOf { "-->" "<--" "—>" "<—" "->" "<-" "→" } OneOrMore(.whitespace) singleNodeRegex } func parseMermaid( _ text: String ) -> ([String], [(String, String)]) { let links = text.split(separator: "\n") .compactMap { if let results = $0.matches(of: mermaidLinkRegex).first { return (String(results.output.1), String(results.output.2)) } return nil } let nodes = Array(Set(links.flatMap { [$0.0, $0.1] })) return (nodes, links) } func getInitialPosition(id: String, r: Double) -> SIMD2 { if let firstLetter = id.first?.unicodeScalars.first { let deg = Double(firstLetter.value % 26) / 26 * 2 * .pi return [cos(deg) * r, sin(deg) * r] } return .zero } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Miserables.swift ================================================ // // Miserables.swift // ForceDirectedGraphExample // // Created by li3zhen1 on 11/5/23. // import Foundation import Grape import SwiftUI import Charts struct MiserableGraph: View { private let graphData = getData(miserables) @State private var inspectorPresented = false @State private var stateMixin = ForceDirectedGraphState( initialIsRunning: true, initialModelTransform: .identity.scale(by: 1.4) ) // @State private var opacity = 0.0 @ViewBuilder func getLabel(_ text: String) -> some View { Text(text) .foregroundStyle(.background) .font(.caption2) .padding(.vertical, 2.0) .padding(.horizontal, 6.0) .background(alignment: .center) { RoundedRectangle(cornerSize: .init(width: 12, height: 12)) .fill(.foreground) .shadow(radius: 1.5, y: 1.0) } .padding() } var body: some View { ForceDirectedGraph( states: stateMixin ) { Series(graphData.nodes) { node in NodeMark(id: node.id) .symbol(.circle) .symbolSize(radius: 8.0) .foregroundStyle(colors[node.group % colors.count]) .stroke() .annotation(node.id, offset: .zero) { let connections = graphData.links.count { $0.source == node.id || $0.target == node.id } if connections > 12 { self.getLabel(node.id) } } } Series(graphData.links) { l in LinkMark(from: l.source, to: l.target) } } force: { .manyBody(strength: -20) .center() .link( originalLength: 35.0, stiffness: .weightedByDegree { _, _ in 1.0} ) } .graphOverlay(content: { proxy in Rectangle().fill(.clear).contentShape(Rectangle()) .withGraphDragGesture(proxy, of: String.self) }) .ignoresSafeArea() .toolbar { GraphStateToggle(graphStates: stateMixin) } } } struct MiserableToolbarContent: View { @Bindable var stateMixin: ForceDirectedGraphState @Binding var opacity: Double var body: some View { Group { Button { stateMixin.modelTransform.scaling(by: 0.9) } label: { Image(systemName: "minus") } Button { stateMixin.modelTransform.scaling(by: 1.1) } label: { Text(String(format:"Scale: %.2f", stateMixin.modelTransform.scale)) .fontDesign(.monospaced) } Button { stateMixin.modelTransform.scaling(by: 1.1) } label: { Image(systemName: "plus") } } Button { stateMixin.isRunning.toggle() if opacity < 1 { opacity = 1 } } label: { Image(systemName: stateMixin.isRunning ? "pause.fill" : "play.fill") Text(stateMixin.isRunning ? "Pause" : "Start") } } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MyRing.swift ================================================ // // ForceDirectedGraphSwiftUIExample.swift // ForceDirectedGraphExample // // Created by li3zhen1 on 11/5/23. // import Foundation import Grape import SwiftUI import ForceSimulation struct MyRing: View { @State var graphStates = ForceDirectedGraphState( ticksOnAppear: .untilStable ) @State var draggingNodeID: Int? = nil static let storkeStyle = StrokeStyle(lineWidth: 1.5, lineCap: .round, lineJoin: .round) var body: some View { ForceDirectedGraph(states: graphStates) { Series(0..<20) { i in NodeMark(id: 3 * i + 0) .symbolSize(radius: 6.0) .foregroundStyle(.green) .stroke(3*i+0 == draggingNodeID ? .secondary : .clear, Self.storkeStyle) NodeMark(id: 3 * i + 1) .symbol(.pentagon) .symbolSize(radius:10) .foregroundStyle(.blue) .stroke(3*i+1 == draggingNodeID ? .secondary : .clear, Self.storkeStyle) NodeMark(id: 3 * i + 2) .symbol(.circle) .symbolSize(radius:6.0) .foregroundStyle(.yellow) .stroke(3*i+2 == draggingNodeID ? .secondary : .clear, Self.storkeStyle) LinkMark(from: 3 * i + 0, to: 3 * i + 1) LinkMark(from: 3 * i + 1, to: 3 * i + 2) LinkMark(from: 3 * i + 0, to: 3 * ((i + 1) % 20) + 0) LinkMark(from: 3 * i + 1, to: 3 * ((i + 1) % 20) + 1) LinkMark(from: 3 * i + 2, to: 3 * ((i + 1) % 20) + 2) } .stroke(.secondary, Self.storkeStyle) } force: { .manyBody(strength: -15) .link( originalLength: 30.0, stiffness: .weightedByDegree { _, _ in 1.0 } ) .center() // .collide() } .graphOverlay { proxy in Rectangle().fill(.clear).contentShape(Rectangle()) .withGraphDragGesture(proxy, of: Int.self, action: describe) .withGraphMagnifyGesture(proxy) } .toolbar { GraphStateToggle(graphStates: graphStates) } } func describe(_ state: GraphDragState?) { switch state { case .node(let id): if draggingNodeID != id { draggingNodeID = id print("Dragging \(id)") } case .background(let start): draggingNodeID = nil print("Dragging \(start)") case nil: draggingNodeID = nil print("Drag ended") } } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 60; objects = { /* Begin PBXBuildFile section */ B70B52AD2AF822FF00A1E6CD /* ForceSimulation in Frameworks */ = {isa = PBXBuildFile; productRef = B70B52AC2AF822FF00A1E6CD /* ForceSimulation */; }; B70B52AF2AF822FF00A1E6CD /* Grape in Frameworks */ = {isa = PBXBuildFile; productRef = B70B52AE2AF822FF00A1E6CD /* Grape */; }; B71759592AFBFC4B000DF006 /* Miserables.swift in Sources */ = {isa = PBXBuildFile; fileRef = B71759582AFBFC4B000DF006 /* Miserables.swift */; }; B717595B2AFBFDBD000DF006 /* Lattice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B717595A2AFBFDBD000DF006 /* Lattice.swift */; }; B762092F2B49FCD000476B93 /* MermaidVisualization.swift in Sources */ = {isa = PBXBuildFile; fileRef = B762092E2B49FCD000476B93 /* MermaidVisualization.swift */; }; B780DD7A2AF84ECB001C605F /* MyRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B780DD792AF84ECB001C605F /* MyRing.swift */; }; B79012AE2B88474F008F4C03 /* GraphStateToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79012AD2B88474F008F4C03 /* GraphStateToolbar.swift */; }; B7AFA55B2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */; }; B7AFA55D2ADF4997009C7154 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA55C2ADF4997009C7154 /* ContentView.swift */; }; B7AFA55F2ADF4999009C7154 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B7AFA55E2ADF4999009C7154 /* Assets.xcassets */; }; B7AFA5622ADF4999009C7154 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B7AFA5612ADF4999009C7154 /* Preview Assets.xcassets */; }; B7AFA56B2ADF49AA009C7154 /* ForceSimulation in Frameworks */ = {isa = PBXBuildFile; productRef = B7AFA56A2ADF49AA009C7154 /* ForceSimulation */; }; B7AFA56F2ADF49D6009C7154 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA56E2ADF49D6009C7154 /* Data.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ B71759582AFBFC4B000DF006 /* Miserables.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Miserables.swift; sourceTree = ""; }; B717595A2AFBFDBD000DF006 /* Lattice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lattice.swift; sourceTree = ""; }; B762092E2B49FCD000476B93 /* MermaidVisualization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MermaidVisualization.swift; sourceTree = ""; }; B780DD792AF84ECB001C605F /* MyRing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyRing.swift; sourceTree = ""; }; B79012AD2B88474F008F4C03 /* GraphStateToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphStateToolbar.swift; sourceTree = ""; }; B7AFA5572ADF4997009C7154 /* ForceDirectedGraphExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ForceDirectedGraphExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceDirectedGraphExampleApp.swift; sourceTree = ""; }; B7AFA55C2ADF4997009C7154 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; B7AFA55E2ADF4999009C7154 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; B7AFA5612ADF4999009C7154 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; B7AFA5632ADF4999009C7154 /* ForceDirectedGraphExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ForceDirectedGraphExample.entitlements; sourceTree = ""; }; B7AFA56E2ADF49D6009C7154 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ B7AFA5542ADF4997009C7154 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( B70B52AF2AF822FF00A1E6CD /* Grape in Frameworks */, B7AFA56B2ADF49AA009C7154 /* ForceSimulation in Frameworks */, B70B52AD2AF822FF00A1E6CD /* ForceSimulation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ B7AFA54E2ADF4997009C7154 = { isa = PBXGroup; children = ( B7AFA5592ADF4997009C7154 /* ForceDirectedGraphExample */, B7AFA5582ADF4997009C7154 /* Products */, ); sourceTree = ""; }; B7AFA5582ADF4997009C7154 /* Products */ = { isa = PBXGroup; children = ( B7AFA5572ADF4997009C7154 /* ForceDirectedGraphExample.app */, ); name = Products; sourceTree = ""; }; B7AFA5592ADF4997009C7154 /* ForceDirectedGraphExample */ = { isa = PBXGroup; children = ( B780DD792AF84ECB001C605F /* MyRing.swift */, B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */, B7AFA55C2ADF4997009C7154 /* ContentView.swift */, B7AFA55E2ADF4999009C7154 /* Assets.xcassets */, B7AFA5632ADF4999009C7154 /* ForceDirectedGraphExample.entitlements */, B7AFA5602ADF4999009C7154 /* Preview Content */, B7AFA56E2ADF49D6009C7154 /* Data.swift */, B71759582AFBFC4B000DF006 /* Miserables.swift */, B717595A2AFBFDBD000DF006 /* Lattice.swift */, B762092E2B49FCD000476B93 /* MermaidVisualization.swift */, B79012AD2B88474F008F4C03 /* GraphStateToolbar.swift */, ); path = ForceDirectedGraphExample; sourceTree = ""; }; B7AFA5602ADF4999009C7154 /* Preview Content */ = { isa = PBXGroup; children = ( B7AFA5612ADF4999009C7154 /* Preview Assets.xcassets */, ); path = "Preview Content"; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ B7AFA5562ADF4997009C7154 /* ForceDirectedGraphExample */ = { isa = PBXNativeTarget; buildConfigurationList = B7AFA5662ADF4999009C7154 /* Build configuration list for PBXNativeTarget "ForceDirectedGraphExample" */; buildPhases = ( B7AFA5532ADF4997009C7154 /* Sources */, B7AFA5542ADF4997009C7154 /* Frameworks */, B7AFA5552ADF4997009C7154 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = ForceDirectedGraphExample; packageProductDependencies = ( B7AFA56A2ADF49AA009C7154 /* ForceSimulation */, B70B52AC2AF822FF00A1E6CD /* ForceSimulation */, B70B52AE2AF822FF00A1E6CD /* Grape */, ); productName = ForceDirectedGraphExample; productReference = B7AFA5572ADF4997009C7154 /* ForceDirectedGraphExample.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ B7AFA54F2ADF4997009C7154 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1500; LastUpgradeCheck = 1530; TargetAttributes = { B7AFA5562ADF4997009C7154 = { CreatedOnToolsVersion = 15.0; }; }; }; buildConfigurationList = B7AFA5522ADF4997009C7154 /* Build configuration list for PBXProject "ForceDirectedGraphExample" */; compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = B7AFA54E2ADF4997009C7154; packageReferences = ( B7AFA5692ADF49AA009C7154 /* XCLocalSwiftPackageReference "../.." */, ); productRefGroup = B7AFA5582ADF4997009C7154 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( B7AFA5562ADF4997009C7154 /* ForceDirectedGraphExample */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ B7AFA5552ADF4997009C7154 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( B7AFA5622ADF4999009C7154 /* Preview Assets.xcassets in Resources */, B7AFA55F2ADF4999009C7154 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ B7AFA5532ADF4997009C7154 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( B79012AE2B88474F008F4C03 /* GraphStateToolbar.swift in Sources */, B717595B2AFBFDBD000DF006 /* Lattice.swift in Sources */, B780DD7A2AF84ECB001C605F /* MyRing.swift in Sources */, B7AFA55D2ADF4997009C7154 /* ContentView.swift in Sources */, B7AFA56F2ADF49D6009C7154 /* Data.swift in Sources */, B762092F2B49FCD000476B93 /* MermaidVisualization.swift in Sources */, B71759592AFBFC4B000DF006 /* Miserables.swift in Sources */, B7AFA55B2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ B7AFA5642ADF4999009C7154 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; XROS_DEPLOYMENT_TARGET = 1.0; }; name = Debug; }; B7AFA5652ADF4999009C7154 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; IPHONEOS_DEPLOYMENT_TARGET = 17.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; XROS_DEPLOYMENT_TARGET = 1.0; }; name = Release; }; B7AFA5672ADF4999009C7154 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ForceDirectedGraphExample/ForceDirectedGraphExample.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"ForceDirectedGraphExample/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = me.lizhen.ForceDirectedGraphExample; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,7"; }; name = Debug; }; B7AFA5682ADF4999009C7154 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = ForceDirectedGraphExample/ForceDirectedGraphExample.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"ForceDirectedGraphExample/Preview Content\""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = me.lizhen.ForceDirectedGraphExample; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,7"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ B7AFA5522ADF4997009C7154 /* Build configuration list for PBXProject "ForceDirectedGraphExample" */ = { isa = XCConfigurationList; buildConfigurations = ( B7AFA5642ADF4999009C7154 /* Debug */, B7AFA5652ADF4999009C7154 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; B7AFA5662ADF4999009C7154 /* Build configuration list for PBXNativeTarget "ForceDirectedGraphExample" */ = { isa = XCConfigurationList; buildConfigurations = ( B7AFA5672ADF4999009C7154 /* Debug */, B7AFA5682ADF4999009C7154 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ B7AFA5692ADF49AA009C7154 /* XCLocalSwiftPackageReference "../.." */ = { isa = XCLocalSwiftPackageReference; relativePath = ../..; }; /* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ B70B52AC2AF822FF00A1E6CD /* ForceSimulation */ = { isa = XCSwiftPackageProductDependency; productName = ForceSimulation; }; B70B52AE2AF822FF00A1E6CD /* Grape */ = { isa = XCSwiftPackageProductDependency; productName = Grape; }; B7AFA56A2ADF49AA009C7154 /* ForceSimulation */ = { isa = XCSwiftPackageProductDependency; productName = ForceSimulation; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = B7AFA54F2ADF4997009C7154 /* Project object */; } ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Examples/ForceDirectedGraphExample/ForceDirectedGraphExample.xcodeproj/xcshareddata/xcschemes/ForceDirectedGraphExample.xcscheme ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 Zhen Li Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version: 5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Grape", platforms: [ .macOS(.v14), .iOS(.v17), .watchOS(.v10), ], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "ForceSimulation", targets: ["ForceSimulation"] ), .library( name: "Grape", targets: ["Grape"] ), ], dependencies: [ .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.4.3") ], targets: [ .target( name: "ForceSimulation", path: "Sources/ForceSimulation", swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") ] ), .target( name: "Grape", dependencies: ["ForceSimulation"], path: "Sources/Grape", swiftSettings: [ .enableExperimentalFeature("StrictConcurrency") ] // link ForceSimulation in release mode // swiftSettings: [.unsafeFlags(["-Xfrontend", "-disable-availability-checking"])] ), .testTarget( name: "KDTreeTests", dependencies: ["ForceSimulation"] ), .testTarget( name: "ForceSimulationTests", dependencies: ["ForceSimulation"] ), .testTarget( name: "GrapeTests", dependencies: ["Grape"] ), ] ) ================================================ FILE: README.md ================================================
grape-icon

Grape

swift package indexswift package index

A Swift library for graph visualization and efficient force simulation.



## Examples ### Force Directed Graph This is a force directed graph visualizing [the network of character co-occurence in _Les Misérables_](https://observablehq.com/@d3/force-directed-graph-component). Take a closer look at the animation: https://github.com/swiftgraphs/Grape/assets/45376537/d80dc797-1980-4755-85b9-18ee26e2a7ff Source code: [Miserables.swift](https://github.com/swiftgraphs/Grape/blob/main/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Miserables.swift).
### Force Directed Graph in visionOS This is the same graph as the first example, rendered in `RealityView`: https://github.com/swiftgraphs/Grape/assets/45376537/4585471e-2339-4aee-8f39-0c11fdfb6901 Source code: [ForceDirectedGraph3D/ContentView.swift](https://github.com/swiftgraphs/Grape/blob/main/Examples/ForceDirectedGraph3D/ForceDirectedGraph3D/ContentView.swift).
### Mermaid Visualization Dynamical graph structure based on your input, with tap and drag gesture supports, all within 100 lines of view body. https://github.com/swiftgraphs/Grape/assets/45376537/7c75d367-d5a8-4316-813b-288b375f513b Source code: [MermaidVisualization.swift](https://github.com/swiftgraphs/Grape/blob/main/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift)
### Lattice Simulation A 36x36 force directed lattice. https://github.com/swiftgraphs/Grape/assets/45376537/5b76fddc-dd5c-4d35-bced-29c01269dd2b Source code: [Lattice.swift](https://github.com/swiftgraphs/Grape/blob/main/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/Lattice.swift)
### Dragging Gesture An example rendering a ring with 60 vertices, with dragging gesture enabled. https://github.com/swiftgraphs/Grape/assets/45376537/73213e7f-73ee-44f3-9b3e-7e58355045d2 Source code: [MyRing.swift](https://github.com/swiftgraphs/Grape/blob/main/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MyRing.swift)


## Installation To use Grape in an Xcode project by adding it to your project as a package: ``` https://github.com/swiftgraphs/Grape ``` To use Grape in a [SwiftPM](https://swift.org/package-manager/) project, add this to your `Package.swift`: ``` swift dependencies: [ .package(url: "https://github.com/swiftgraphs/Grape", from: "1.1.0") ] ``` ```swift .product(name: "Grape", package: "Grape"), ``` > [!NOTE] > The `Grape` module relies on the [`Observation` framework](https://developer.apple.com/documentation/observation). It’s possible to backdeploy with community shims like [`swift-perception`](https://github.com/pointfreeco/swift-perception). > > The `Grape` module may introduce breaking API changes in minor version changes before 1.0 release. > > The `ForceSimulation` module is stable in terms of public API now.

## Get started Grape ships 2 modules: - The `Grape` module allows you to create force-directed graphs in SwiftUI Views. - The `ForceSimulation` module is the underlying mechanism of `Grape`, and it helps you to create more complicated or customized force simulations. It also contains a `KDTree` data structure built with performance in mind, which can be useful for spatial partitioning tasks.
### The `Grape` module For detailed usage, please refer to [documentation](https://swiftgraphs.github.io/Grape/Grape/documentation/grape). A quick example here: ```swift import Grape struct MyGraph: View { // States including running status, transformation, etc. // Gives you a handle to control the states. @State var graphStates = ForceDirectedGraphState() var body: some View { ForceDirectedGraph(states: graphStates) { // Declare nodes and links like you would do in Swift Charts. NodeMark(id: 0).foregroundStyle(.green) NodeMark(id: 1).foregroundStyle(.blue) NodeMark(id: 2).foregroundStyle(.yellow) Series(0..<2) { i in LinkMark(from: i, to: i+1) } } force: { .link() .center() .manyBody() } } } ```
### The `ForceSimulation` module
Refer to the documentation or expand this section to find more about this module. `ForceSimulation` module mainly contains 3 concepts, `Kinetics`, `ForceProtocol` and `Simulation`.

A diagram showing the relationships of `Kinetics`, `ForceProtocol` and `Simulation`. A `Simulation` contains a `Kinetics` and a `ForceProtocol`.

- `Kinetics` describes all kinetic states of your system, i.e. position, velocity, link connections, and the variable `alpha` that describes how "active" your system is. - Forces are any types that conforms to `ForceProtocol`. This module provides most of the forces you will use in force directed graphs. And you can also create your own forces. They should be responsible for 2 tasks: - `bindKinetics(_ kinetics: Kinetics)`: binding to a `Kinetics`. In most cases the force should keep a reference of the `Kinetics` so they know what to mutate when `apply` is called. - `apply()`: Mutating the states of `Kinetics`. For example, a gravity force should add velocities on each node in this function. - `Simulation` is a shell class you interact with, which enables you to create any dimensional simulation with velocity Verlet integration. It manages a `Kinetics` and a force conforming to `ForceProtocol`. Since `Simulation` only stores one force, you are responsible for compositing multiple forces into one. - Another data structure `KDTree` is used to accelerate the force simulation with [Barnes-Hut Approximation](https://jheer.github.io/barnes-hut/).
The basic concepts of simulations and forces can be found here: [Force simulations - D3](https://d3js.org/d3-force/simulation). You can simply create simulations by using `Simulation` like this: ```swift import simd import ForceSimulation // assuming you’re simulating 4 nodes let nodeCount = 4 // Connect them let links = [(0, 1), (1, 2), (2, 3), (3, 0)] /// Create a 2D force composited with 4 primitive forces. let myForce = SealedForce2D { // Forces are namespaced under `Kinetics` // here we only use `Kinetics>`, i.e. `Kinetics2D` Kinetics2D.ManyBodyForce(strength: -30) Kinetics2D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(35) ) Kinetics2D.CenterForce(center: .zero, strength: 1) Kinetics2D.CollideForce(radius: .constant(3)) } /// Create a simulation, the dimension is inferred from the force. let mySimulation = Simulation( nodeCount: nodeCount, links: links.map { EdgeID(source: $0.0, target: $0.1) }, forceField: myForce ) /// Force is ready to start! run `tick` to iterate the simulation. for mySimulation in 0..<120 { mySimulation.tick() let positions = mySimulation.kinetics.position.asArray() /// Do something with the positions. } ``` See [Example](https://github.com/swiftgraphs/Grape/tree/main/Examples/ForceDirectedGraphExample) for more details.


## Roadmap | | 2D simd | ND simd | Metal | | --- | --- | --- | --- | | **NdTree** | ✅ | ✅ | | | **Simulation** | ✅ | ✅ | | |  LinkForce | ✅ | ✅ | | |  ManyBodyForce | ✅ | ✅ | | |  CenterForce | ✅ | ✅ | | |  CollideForce | ✅ | ✅ | | |  PositionForce | ✅ | ✅ | | |  RadialForce | ✅ | ✅ | | | **SwiftUI View** | ✅ | | | |  Basic Visualization | ✅ | | | |  Gestures | ✅ | | | |  Node Styling | ✅ | | | |  Link Styling | 🚧 | | | |  Animatable Transition | 🚧 | | |

## Performance
#### Simulation Grape uses simd to calculate position and velocity. Currently it takes **~0.005** seconds to iterate 120 times over the example graph(2D). (77 vertices, 254 edges, with manybody, center, collide and link forces. Release build on a M1 Max, [tested](https://github.com/swiftgraphs/Grape/blob/main/Tests/ForceSimulationTests/MiserableGraphTest.swift) with command `swift test -c release`) For 3D simulation, it takes **~0.008** seconds for the same graph and same configs. > [!IMPORTANT] > Due to heavy use of generics (some are not specialized in Debug mode), the performance in Debug build is ~100x slower than Release build.
#### KDTree The `BufferedKDTree` from this package is **~22x** faster than `GKQuadtree` from Apple’s GameKit, according to this [test case](https://github.com/swiftgraphs/Grape/blob/main/Tests/ForceSimulationTests/GKTreeCompareTest.swift). However, please note that comparing Swift structs with NSObjects is unfair, and their behaviors are different.
## Credits This library has been greatly influenced by the outstanding work done by [D3.js (Data-Driven Documents)](https://d3js.org). ================================================ FILE: Sources/ForceSimulation/ForceProtocol.swift ================================================ /// A protocol that represents a force. /// /// A force takes a simulation state and modifies its node positions and velocities. public protocol ForceProtocol { associatedtype Vector where Vector: SimulatableVector & L2NormCalculatable /// Takes a simulation state and modifies its node positions and velocities. /// This is executed in each tick of the simulation. @inlinable func apply(to kinetics: inout Kinetics) /// Bind to a kinetic system that describes the state of all nodes in your simulation. /// This has to be called before `apply` is called. @inlinable mutating func bindKinetics(_ kinetics: Kinetics) /// Deinitialize the tree and deallocate the memory. /// Called when `Simulation` is deinitialized. @inlinable func dispose() } public protocol Force2D: ForceProtocol where Vector == SIMD2 {} public protocol Force3D: ForceProtocol where Vector == SIMD3 {} extension Kinetics2D.LinkForce: Force2D {} extension Kinetics2D.ManyBodyForce: Force2D {} extension Kinetics2D.CenterForce: Force2D {} extension Kinetics2D.CollideForce: Force2D {} extension Kinetics2D.PositionForce: Force2D {} extension Kinetics2D.RadialForce: Force2D {} extension Kinetics2D.EmptyForce: Force2D {} extension CompositedForce: Force2D where Vector == SIMD2 {} extension Kinetics3D.LinkForce: Force3D {} extension Kinetics3D.ManyBodyForce: Force3D {} extension Kinetics3D.CenterForce: Force3D {} extension Kinetics3D.CollideForce: Force3D {} extension Kinetics3D.PositionForce: Force3D {} extension Kinetics3D.RadialForce: Force3D {} extension Kinetics3D.EmptyForce: Force3D {} extension CompositedForce: Force3D where Vector == SIMD3 {} public protocol ForceDescriptor: Equatable { associatedtype ConcreteForce: ForceProtocol func createForce() -> ConcreteForce } /// A helper force for hiding long type signatures you composed with `CompositedForce`. /// You can easily build a force with a result builder. public protocol ForceField: ForceProtocol where Vector: SimulatableVector & L2NormCalculatable { associatedtype F: ForceProtocol where F.Vector == Vector @inlinable @ForceBuilder var force: F { get set } } extension ForceField { // @inlinable // public func apply() { // self.force.apply() // } @inlinable public func apply(to kinetics: inout Kinetics) { self.force.apply(to: &kinetics) } @inlinable public func dispose() { self.force.dispose() } @inlinable public mutating func bindKinetics(_ kinetics: Kinetics) { self.force.bindKinetics(kinetics) } } public protocol ForceField2D: ForceField & Force2D {} public protocol ForceField3D: ForceField & Force3D {} ================================================ FILE: Sources/ForceSimulation/ForceSimulation.docc/CreatingASimulationWithBuiltinForces.md ================================================ # Creating a Simulation with Built-in Forces ## Overview You can simply create simulations by using Simulation like this: ```swift import simd import ForceSimulation // assuming you’re simulating 4 nodes let nodeCount = 4 // Connect them let links = [(0, 1), (1, 2), (2, 3), (3, 0)] /// Create a 2D force composited with 4 primitive forces. let myForce = SealedForce2D { // Forces are namespaced under `Kinetics` // here we only use `Kinetics>`, i.e. `Kinetics2D` Kinetics2D.ManyBodyForce(strength: -30) Kinetics2D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(35) ) Kinetics2D.CenterForce(center: .zero, strength: 1) Kinetics2D.CollideForce(radius: .constant(3)) } /// Create a simulation, the dimension is inferred from the force. let mySimulation = Simulation( nodeCount: nodeCount, links: links.map { EdgeID(source: $0.0, target: $0.1) }, forceField: myForce ) /// Force is ready to start! run `tick` to iterate the simulation. for mySimulation in 0..<120 { mySimulation.tick() let positions = mySimulation.kinetics.position.asArray() /// Do something with the positions. } ``` ForceSimulation module mainly contains 3 concepts, `Kinetics`, `ForceProtocol` and `Simulation`. @Image(source: "SimulationDiagram.svg", alt: "A diagram showing the relationships of `Kinetics`, `ForceProtocol` and `Simulation`. A `Simulation` contains a `Kinetics` and a `ForceProtocol`.") A diagram showing the relationships of `Kinetics`, `ForceProtocol` and `Simulation`. A `Simulation` contains a `Kinetics` and a `ForceProtocol`. - `Kinetics` describes all kinetic states of your system, i.e. position, velocity, link connections, and the variable alpha that describes how "active" your system is. `Vector` tells simulation how you decribe a coordinate in this space, it can be `SIMD2` or `SIMD3` or any other types conforming to `SimulatableVector`. - Forces are any types that conforms to `ForceProtocol`. This module provides most of the forces you will use in force directed graphs. And you can also create your own forces. They should be responsible for 2 tasks: - `bindKinetics(_ kinetics: Kinetics)`: binding to a Kinetics. In most cases the force should keep a reference of the Kinetics so they know what to mutate when apply is called. - `apply()`: Mutating the states of Kinetics. For example, a gravity force should add velocities on each node in this function. - Simulation is a shell class you interact with, which enables you to create any dimensional simulation with velocity Verlet integration. It manages a Kinetics and a force conforming to ``ForceProtocol``. Since Simulation only stores one force, you are responsible for compositing multiple forces into one. - Another data structure ``KDTree`` is used to accelerate the force simulation with Barnes-Hut Approximation. In this example, we run our simulation in a 2D space (`SIMD2`). We explicitly create a ``SealedForce2D`` to make sure the force is in the same dimension as the Kinetics. The `Vector` in `Simulation` is inferred from the force we pass. See [Examples](https://github.com/swiftgraphs/Grape/tree/main/Examples) for example Xcode projects. ================================================ FILE: Sources/ForceSimulation/ForceSimulation.docc/Documentation.md ================================================ # ``ForceSimulation`` Run force simulation within any number of dimensions. ## Overview The `ForceSimulation` library enables you to create any dimensional simulation that uses velocity Verlet integration. If you’re looking for an out-of-the-box SwiftUI View to render force-directed graphs, please refer to [Grape | Documentation](https://swiftgraphs.github.io/Grape/Grape/documentation/grape/). @Image(source: "ForceDirectedGraph.png", alt: "An example of 2D force directied graph.") For more information on force simulations, read: [Force simulations - D3](https://d3js.org/d3-force/simulation). ## Topics ### Creating a simulation * * ``Simulation`` * ``Kinetics`` ### Built-in forces * ``Kinetics/LinkForce`` * ``Kinetics/ManyBodyForce`` * ``Kinetics/CenterForce`` * ``Kinetics/CollideForce`` * ``Kinetics/PositionForce`` * ``Kinetics/RadialForce`` * ``Kinetics/EmptyForce`` ### Utility forces for compositing a force field * ``ForceField`` * ``CompositedForce`` * ``SealedForce2D`` * ``SealedForce3D`` ### Spatial partitioning data structures - ``KDTree`` - ``KDBox`` - ``BufferedKDTree`` - ``KDTreeDelegate`` ### Deterministic randomness - ``SimulatableFloatingPoint`` - ``DeterministicRandomGenerator`` - ``HasDeterministicRandomGenerator`` - ``DoubleLinearCongruentialGenerator`` - ``FloatLinearCongruentialGenerator`` ### Supporting protocols - ``ForceProtocol`` - ``SimulatableVector`` ### Utilities - ``UnsafeArray`` - ``EdgeID`` - ``AttributeDescriptor`` ================================================ FILE: Sources/ForceSimulation/ForceSimulation.docc/theme-settings.json ================================================ { "theme": { "typography": { "html-font": "system-ui, -apple-system, \"InterVar\"", "html-font-mono": "ui-monospace, \"JetBrains Mono\", \"IBM Plex Mono\", monospace" } } } ================================================ FILE: Sources/ForceSimulation/Forces/CenterForce.swift ================================================ extension Kinetics { /// A force that drives nodes towards the center. /// /// Center force is relatively fast, the complexity is `O(n)`, /// where `n` is the number of nodes. /// See [Collide Force - D3](https://d3js.org/d3-force/collide). public struct CenterForce: ForceProtocol { @inlinable public func apply(to kinetics: inout Kinetics) { var meanPosition = Vector.zero let positionBufferPointer = kinetics.position.mutablePointer for i in 0..: KDTreeDelegate where Vector: SimulatableVector { @usableFromInline var radiusBufferPointer: UnsafeMutablePointer public var maxNodeRadius: Vector.Scalar = .zero @inlinable public mutating func didAddNode(_ node: Int, at position: Vector) { maxNodeRadius = max(maxNodeRadius, radiusBufferPointer[node]) } @inlinable public mutating func didRemoveNode(_ node: Int, at position: Vector) { if radiusBufferPointer[node] >= maxNodeRadius { // 🤯 for Collide force, set to 0 is fine // Otherwise you need to traverse the delegate again maxNodeRadius = 0 } } @inlinable public func spawn() -> MaxRadiusNDTreeDelegate { return Self(radiusBufferPointer: radiusBufferPointer) } @inlinable init(maxNodeRadius: Vector.Scalar = 0, radiusBufferPointer: UnsafeMutablePointer) { self.maxNodeRadius = maxNodeRadius self.radiusBufferPointer = radiusBufferPointer } } extension Kinetics { public typealias CollideRadius = AttributeDescriptor /// A force that prevents nodes from overlapping. /// /// This is a very expensive force, the complexity is `O(n log(n))`, /// where `n` is the number of nodes. /// See [Collide Force - D3](https://d3js.org/d3-force/collide). public struct CollideForce: ForceProtocol { // @usableFromInline // var kinetics: Kinetics! = nil public var radius: CollideRadius public let iterationsPerTick: UInt public var strength: Vector.Scalar @inlinable public init( radius: CollideRadius, strength: Vector.Scalar = 1.0, iterationsPerTick: UInt = 1 ) { self.radius = radius self.iterationsPerTick = iterationsPerTick self.strength = strength } @inlinable public mutating func bindKinetics(_ kinetics: Kinetics) { // self.kinetics = kinetics self.calculatedRadius = self.radius.calculateUnsafe(for: kinetics.validCount) self.tree = .allocate(capacity: 1) self.tree.initialize( to: BufferedKDTree( rootBox: .init( p0: .init(repeating: 0), p1: .init(repeating: 1) ), nodeCapacity: kinetics.validCount, rootDelegate: .init( radiusBufferPointer: self.calculatedRadius.mutablePointer) ) ) } @usableFromInline var calculatedRadius: UnsafeArray! = nil @usableFromInline internal var tree: UnsafeMutablePointer>>! = nil @inlinable public func apply(to kinetics: inout Kinetics) { let strength = self.strength let calculatedRadius = self.calculatedRadius!.mutablePointer let positionBufferPointer = kinetics.position.mutablePointer let velocityBufferPointer = kinetics.velocity.mutablePointer let tree = self.tree! for _ in 0...cover(of: kinetics.position) tree.pointee.reset( rootBox: coveringBox, rootDelegate: .init(radiusBufferPointer: calculatedRadius) ) assert(tree.pointee.validCount == 1) for p in kinetics.range { tree.pointee.add(nodeIndex: p, at: positionBufferPointer[p]) } for i in kinetics.range { let iOriginalPosition = positionBufferPointer[i] let iOriginalVelocity = velocityBufferPointer[i] let iR = calculatedRadius[i] let iR2 = iR * iR let iPosition = iOriginalPosition + iOriginalVelocity tree.pointee.visit { t in let maxRadiusOfQuad = t.delegate.maxNodeRadius let deltaR = maxRadiusOfQuad + iR if var jNode = t.nodeIndices { while true { let j = jNode.index // print("\(i)<=>\(j)") // is leaf, make sure every collision happens once. if j > i { let jR = calculatedRadius[j] let jOriginalPosition = positionBufferPointer[j] let jOriginalVelocity = velocityBufferPointer[j] var deltaPosition = iPosition - (jOriginalPosition + jOriginalVelocity) let l = (deltaPosition).lengthSquared() let deltaR = iR + jR if l < deltaR * deltaR { var l = /*simd_length*/ (deltaPosition.jiggled(by: &kinetics.randomGenerator)) .length() l = (deltaR - l) / l * strength let jR2 = jR * jR let k = jR2 / (iR2 + jR2) deltaPosition *= l velocityBufferPointer[i] += deltaPosition * k velocityBufferPointer[j] -= deltaPosition * (1 - k) } } if jNode.next == nil { break } else { jNode = jNode.next!.pointee } } return false } // TODO: SIMD mask // for laneIndex in t.box.p0.indices { // let _v = t.box.p0[laneIndex] // if _v > iPosition[laneIndex] + deltaR /* True if no overlap */ { // return false // } // } // for laneIndex in t.box.p1.indices { // let _v = t.box.p1[laneIndex] // if _v < iPosition[laneIndex] - deltaR /* True if no overlap */ { // return false // } // } let p0Flag = t.box.p0 .> (iPosition + deltaR) let p1Flag = t.box.p1 .< (iPosition - deltaR) let flag = p0Flag .| p1Flag for laneIndex in t.box.p0.indices { if flag[laneIndex] { return false } // let _v = t.box.p1[laneIndex] // if (t.box.p0[laneIndex] > iPosition[laneIndex] + deltaR) // || (t.box.p1[laneIndex] < iPosition[laneIndex] // - deltaR) /* True if no overlap */ // { // return false // } } return true } } } } /// Deinitialize the tree and deallocate the memory. /// Called when `Simulation` is deinitialized. @inlinable public func dispose() { self.tree.dispose() } } } ================================================ FILE: Sources/ForceSimulation/Forces/CompositedForce.swift ================================================ /// Workaround for yet unsupported packed generic pack types & same type requirements public struct CompositedForce: ForceProtocol where F1: ForceProtocol, F2: ForceProtocol, Vector: SimulatableVector & L2NormCalculatable, F1.Vector == Vector, F2.Vector == Vector, F1.Vector == Vector { @usableFromInline var force1: F1? @usableFromInline var force2: F2 @inlinable public init(force1: F1? = nil, force2: F2) { self.force1 = force1 self.force2 = force2 } @inlinable public func apply(to kinetics: inout Kinetics) { self.force1?.apply(to: &kinetics) self.force2.apply(to: &kinetics) } @inlinable public func dispose() { self.force1?.dispose() self.force2.dispose() } @inlinable public mutating func bindKinetics(_ kinetics: Kinetics) { self.force1?.bindKinetics(kinetics) self.force2.bindKinetics(kinetics) } @inlinable public init(@ForceBuilder _ builder: () -> CompositedForce) { self = builder() } } // public typealias CompositedForce2D = CompositedForce, F1, F2> // where F1: ForceProtocol>, F2: ForceProtocol> // public typealias CompositedForce3D = CompositedForce, F1, F2> // where F1: ForceProtocol>, F2: ForceProtocol> @resultBuilder public struct ForceBuilder where Vector: SimulatableVector & L2NormCalculatable { public static func buildPartialBlock(first: F) -> F// CompositedForce.EmptyForce, F> where F: ForceProtocol, Vector: SimulatableVector & L2NormCalculatable { return first //.init(force2: first) } public static func buildPartialBlock( accumulated: F1, next: F2 ) -> CompositedForce where F1: ForceProtocol, F2: ForceProtocol, Vector: SimulatableVector & L2NormCalculatable, F1.Vector == Vector, F2.Vector == Vector { return CompositedForce(force1: accumulated, force2: next) } public static func buildBlock( _ force1: F1? = nil, _ force2: F2 ) -> CompositedForce where F1: ForceProtocol, F2: ForceProtocol, Vector: SimulatableVector & L2NormCalculatable, F1.Vector == Vector, F2.Vector == Vector { return CompositedForce(force1: force1, force2: force2) } public static func buildExpression( _ expression: Descriptor ) -> Descriptor.ConcreteForce { expression.createForce() } public static func buildExpression( _ expression: F ) -> F { expression } } ================================================ FILE: Sources/ForceSimulation/Forces/EmptyForce.swift ================================================ extension Kinetics { public struct EmptyForce: ForceProtocol { @inlinable public func apply(to kinetics: inout Kinetics) {} @inlinable public func bindKinetics(_ kinetics: Kinetics) {} @inlinable public func dispose() {} } } ================================================ FILE: Sources/ForceSimulation/Forces/KDTreeForce.swift ================================================ // public protocol KDTreeForce: ForceProtocol // where // Vector: SimulatableVector & L2NormCalculatable // { // associatedtype Delegate: KDTreeDelegate where Delegate.Vector == Vector, Delegate.NodeID == Int // var kinetics: Kinetics! { get set } // func epilogue() // func buildDelegate() -> Delegate // func visitForeignTree( // tree: inout KDTree, getDelegate: (D) -> Delegate) // } // public struct CompositedKDTreeDelegate: KDTreeDelegate // where // V: SimulatableVector & L2NormCalculatable, // D1: KDTreeDelegate, D2: KDTreeDelegate // { // var d1: D1 // var d2: D2 // mutating public func didAddNode(_ node: Int, at position: V) { // d1.didAddNode(node, at: position) // d2.didAddNode(node, at: position) // } // mutating public func didRemoveNode(_ node: Int, at position: V) { // d1.didRemoveNode(node, at: position) // d2.didRemoveNode(node, at: position) // } // public func spawn() -> CompositedKDTreeDelegate { // return .init(d1: d1.spawn(), d2: d2.spawn()) // } // } // extension Kinetics.ManyBodyForce: KDTreeForce { // public typealias Delegate = MassCentroidKDTreeDelegate // public func epilogue() { // } // public func buildDelegate() -> MassCentroidKDTreeDelegate { // return .init(massProvider: { self.precalculatedMass[$0] }) // } // public func visitForeignTree( // tree: inout KDTree, getDelegate: (D) -> MassCentroidKDTreeDelegate // ) { // } // } // extension Kinetics.CollideForce: KDTreeForce { // public typealias Delegate = MaxRadiusNDTreeDelegate // public func epilogue() { // } // public func buildDelegate() -> MaxRadiusNDTreeDelegate { // return .init(radiusProvider: { self.calculatedRadius[$0] }) // } // public func visitForeignTree( // tree: inout KDTree, getDelegate: (D) -> MaxRadiusNDTreeDelegate // ) where D: KDTreeDelegate, Vector == D.Vector, D.NodeID == Int { // } // } // public struct CompositedKDTreeForce: ForceProtocol // where // KF1: KDTreeForce, KF2: KDTreeForce, // Vector: SimulatableVector & L2NormCalculatable, // KF1.Vector == Vector, KF2.Vector == Vector, KF1.Vector == Vector // { // var force1: KF1 // var force2: KF2 // public func apply() { // force1.epilogue() // force2.epilogue() // var tree = KDTree>( // covering: force1.kinetics!.position, // rootDelegate: CompositedKDTreeDelegate( // d1: force1.buildDelegate(), // d2: force2.buildDelegate() // ) // ) // force1.visitForeignTree(tree: &tree, getDelegate: \.d1) // force2.visitForeignTree(tree: &tree, getDelegate: \.d2) // } // public mutating func bindKinetics(_ kinetics: Kinetics) { // } // } ================================================ FILE: Sources/ForceSimulation/Forces/LinkForce.swift ================================================ import Darwin extension Kinetics { public enum LinkStiffness { case constant(Vector.Scalar) case varied((EdgeID, LinkLookup) -> Vector.Scalar) case weightedByDegree(k: (EdgeID, LinkLookup) -> Vector.Scalar) @inlinable func calculate(for link: [EdgeID], in lookup: LinkLookup) -> [Vector.Scalar] { switch self { case .constant(let m): return Array(repeating: m, count: link.count) case .varied(let f): return link.map { l in f(l, lookup) } case .weightedByDegree(let k): return link.map { l in k(l, lookup) / (Vector.Scalar( min( lookup.count[l.source, default: 0], lookup.count[l.target, default: 0] ) )) } } } } public enum LinkLength { case constant(Vector.Scalar) case varied((EdgeID, LinkLookup) -> Vector.Scalar) @inlinable func calculate(for link: [EdgeID], in lookup: LinkLookup) -> [Vector.Scalar] { switch self { case .constant(let m): return Array(repeating: m, count: link.count) case .varied(let f): return link.map { l in f(l, lookup) } } } } /// A force that represents links between nodes. /// /// The complexity is `O(e)`, where `e` is the number of links. /// See [Link Force - D3](https://d3js.org/d3-force/link). public struct LinkForce: ForceProtocol { // @usableFromInline // internal var kinetics: Kinetics! = nil @usableFromInline var linkStiffness: LinkStiffness @usableFromInline var calculatedStiffness: [Vector.Scalar] = [] @usableFromInline var linkLength: LinkLength @usableFromInline var calculatedLength: [Vector.Scalar] = [] /// Bias @usableFromInline var calculatedBias: [Vector.Scalar] = [] @inlinable public func apply(to kinetics: inout Kinetics) { let positionBufferPointer = kinetics.position.mutablePointer let velocityBufferPointer = kinetics.velocity.mutablePointer for _ in 0..]! = nil @usableFromInline internal var linkLookup: LinkLookup = .init(links: []) @inlinable public mutating func bindKinetics(_ kinetics: Kinetics) { // self.kinetics = kinetics self.links = kinetics.links self.links = self.links.filter { $0.source < kinetics.validCount && $0.target < kinetics.validCount } self.linkLookup = .init(links: self.links) self.calculatedBias = self.links.map { l in Vector.Scalar(self.linkLookup.count[l.source, default: 0]) / Vector.Scalar( linkLookup.count[l.target, default: 0] + linkLookup.count[l.source, default: 0]) } self.calculatedStiffness = self.linkStiffness.calculate( for: self.links, in: self.linkLookup) self.calculatedLength = self.linkLength.calculate(for: self.links, in: self.linkLookup) } @usableFromInline var iterationsPerTick: UInt @inlinable public init( // _ links: [EdgeID], stiffness: LinkStiffness, originalLength: LinkLength = .constant(30), iterationsPerTick: UInt = 1 ) { // self.links = links self.iterationsPerTick = iterationsPerTick self.linkStiffness = stiffness self.linkLength = originalLength } @inlinable public func dispose() {} } } public struct LinkLookup { public let sourceToTarget: [NodeID: [NodeID]] public let targetToSource: [NodeID: [NodeID]] public let count: [NodeID: Int] @inlinable public init(links: [EdgeID]) { var sourceToTarget: [NodeID: [NodeID]] = [:] var targetToSource: [NodeID: [NodeID]] = [:] var count: [NodeID: Int] = [:] for link in links { sourceToTarget[link.source, default: []].append(link.target) targetToSource[link.target, default: []].append(link.source) count[link.source, default: 0] += 1 count[link.target, default: 0] += 1 } self.sourceToTarget = sourceToTarget self.targetToSource = targetToSource self.count = count } } extension Kinetics.LinkStiffness: Equatable { @inlinable public static func == (lhs: Kinetics.LinkStiffness, rhs: Kinetics.LinkStiffness) -> Bool { switch (lhs, rhs) { case (.constant(let l), .constant(let r)): return l == r default: return false } } } extension Kinetics.LinkLength: Equatable { @inlinable public static func == (lhs: Kinetics.LinkLength, rhs: Kinetics.LinkLength) -> Bool { switch (lhs, rhs) { case (.constant(let l), .constant(let r)): return l == r default: return false } } } ================================================ FILE: Sources/ForceSimulation/Forces/ManyBodyForce.swift ================================================ import simd public struct MassCentroidKDTreeDelegate: KDTreeDelegate where Vector: SimulatableVector { public var accumulatedMass: Vector.Scalar = .zero public var accumulatedCount: Int = 0 public var accumulatedMassWeightedPositions: Vector = .zero @usableFromInline let massArray: UnsafeMutablePointer //(NodeID) -> Vector.Scalar @inlinable init(massProvider: UnsafeMutablePointer) { self.massArray = massProvider } @inlinable init( initialAccumulatedProperty: Vector.Scalar, initialAccumulatedCount: Int, initialWeightedAccumulatedNodePositions: Vector, massProvider: UnsafeMutablePointer //@escaping (Int) -> Vector.Scalar ) { self.accumulatedMass = initialAccumulatedProperty self.accumulatedCount = initialAccumulatedCount self.accumulatedMassWeightedPositions = initialWeightedAccumulatedNodePositions self.massArray = massProvider } @inlinable public mutating func didAddNode(_ node: Int, at position: Vector) { let p = massArray[node] #if DEBUG assert(p > 0) #endif accumulatedCount += 1 accumulatedMass += p accumulatedMassWeightedPositions += position * p } @inlinable public mutating func didRemoveNode(_ node: Int, at position: Vector) { let p = massArray[node] accumulatedCount -= 1 accumulatedMass -= p accumulatedMassWeightedPositions -= position * p // TODO: parent removal? } // @inlinable public func copy() -> Self { // return Self( // initialAccumulatedProperty: self.accumulatedMass, // initialAccumulatedCount: self.accumulatedCount, // initialWeightedAccumulatedNodePositions: self.accumulatedMassWeightedPositions, // massProvider: self.massProvider // ) // } @inlinable public func spawn() -> Self { return Self(massProvider: self.massArray) } } extension Kinetics { public typealias NodeMass = AttributeDescriptor /// A force that simulate the many-body force. /// /// This is a very expensive force, the complexity is `O(n log(n))`, /// where `n` is the number of nodes. The complexity might degrade to `O(n^2)` if the nodes are too close to each other. /// See [Manybody Force - D3](https://d3js.org/d3-force/many-body). public struct ManyBodyForce: ForceProtocol { @usableFromInline let strength: Vector.Scalar @usableFromInline var theta2: Vector.Scalar @usableFromInline var theta: Vector.Scalar { didSet { theta2 = theta * theta } } @usableFromInline let distanceMin: Vector.Scalar = 1 @usableFromInline let distanceMin2: Vector.Scalar = 1 @usableFromInline let distanceMax2: Vector.Scalar = .infinity @usableFromInline let distanceMax: Vector.Scalar = .infinity public let mass: NodeMass @usableFromInline var precalculatedMass: UnsafeArray! = nil @inlinable public init( strength: Vector.Scalar, nodeMass: NodeMass = .constant(1.0), theta: Vector.Scalar = 0.9 ) { self.strength = strength self.mass = nodeMass self.theta = theta self.theta2 = theta * theta } @inlinable public func apply(to kinetics: inout Kinetics) { // Avoid capturing self let alpha = kinetics.alpha let theta2 = self.theta2 let distanceMin2 = self.distanceMin2 let distanceMax2 = self.distanceMax2 let strength = self.strength let precalculatedMass = self.precalculatedMass.mutablePointer let positionBufferPointer = kinetics.position.mutablePointer // let random = kinetics.randomGenerator let tree = self.tree! let coveringBox = KDBox.cover(of: kinetics.position) tree.pointee.reset(rootBox: coveringBox, rootDelegate: .init(massProvider: precalculatedMass)) for p in kinetics.range { tree.pointee.add(nodeIndex: p, at: positionBufferPointer[p]) } for i in kinetics.range { let pos = positionBufferPointer[i] var f = Vector.zero tree.pointee.visit { t in guard t.delegate.accumulatedCount > 0 else { return false } let centroid = t.delegate.accumulatedMassWeightedPositions / t.delegate.accumulatedMass let vec = centroid - pos let boxWidth = (t.box.p1 - t.box.p0)[0] var distanceSquared = (vec .jiggled(by: &kinetics.randomGenerator)).lengthSquared() let farEnough: Bool = (distanceSquared * theta2) > (boxWidth * boxWidth) if distanceSquared < distanceMin2 { distanceSquared = (distanceMin2 * distanceSquared).squareRoot() } if farEnough { guard distanceSquared < distanceMax2 else { return true } /// Workaround for "The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions" let k: Vector.Scalar = strength * alpha * t.delegate.accumulatedMass / distanceSquared // distanceSquared.squareRoot() f += vec * k return false } else if t.childrenBufferPointer != nil { return true } if t.isFilledLeaf { if t.nodeIndices!.contains(i) { return false } let massAcc = t.delegate.accumulatedMass let k: Vector.Scalar = strength * alpha * massAcc / distanceSquared // distanceSquared.squareRoot() f += vec * k return false } else { return true } } positionBufferPointer[i] += f / precalculatedMass[i] } } // public var kinetics: Kinetics! = nil @inlinable public mutating func bindKinetics(_ kinetics: Kinetics) { // self.kinetics = kinetics self.precalculatedMass = self.mass.calculateUnsafe(for: (kinetics.validCount)) self.tree = .allocate(capacity: 1) self.tree.initialize( to: BufferedKDTree( rootBox: .init( p0: .init(repeating: 0), p1: .init(repeating: 1) ), nodeCapacity: kinetics.validCount, rootDelegate: MassCentroidKDTreeDelegate( massProvider: precalculatedMass.mutablePointer) ) ) } /// The buffered KDTree used across all ticks. @usableFromInline internal var tree: UnsafeMutablePointer>>! = nil /// Deinitialize the tree and deallocate the memory. /// Called when `Simulation` is deinitialized. @inlinable public func dispose() { self.tree.deinitialize(count: 1) self.tree.deallocate() } } } ================================================ FILE: Sources/ForceSimulation/Forces/PackedForce.swift ================================================ // struct PackedForce: ForceProtocol where Vector: SIMD, Vector.Scalar: FloatingPoint, repeat each Force: ForceProtocol { // let forces: (repeat each Force) // // var kinetics: Kinetics? // init(forces: repeat each Force) { // self.forces = (repeat each forces) // } // func apply() { // repeat (each forces).apply() // } // func bindKinetics(_ kinetics: Kinetics) { // repeat (each forces).bindKinetics(kinetics) // } // } ================================================ FILE: Sources/ForceSimulation/Forces/PositionForce.swift ================================================ extension Kinetics { public typealias TargetOnDirection = AttributeDescriptor public enum DirectionOfPositionForce: Equatable { case x case y case z case entryOfVector(Int) } public typealias PositionStrength = AttributeDescriptor /// A force that moves nodes to a target position. /// /// Center force is relatively fast, the complexity is `O(n)`, /// where `n` is the number of nodes. /// See [Position Force - D3](https://d3js.org/d3-force/position). public struct PositionForce: ForceProtocol { // @usableFromInline var kinetics: Kinetics! = nil public var strength: PositionStrength public var direction: Int public var calculatedStrength: UnsafeArray! = nil public var targetOnDirection: TargetOnDirection public var calculatedTargetOnDirection: UnsafeArray! = nil @inlinable public func apply(to kinetics: inout Kinetics) { let alpha = kinetics.alpha let lane = self.direction for i in kinetics.range { kinetics.velocity[i][lane] += (self.calculatedTargetOnDirection[i] - kinetics.position[i][lane]) * self.calculatedStrength[i] * alpha } } @inlinable public mutating func bindKinetics(_ kinetics: Kinetics) { // self.kinetics = kinetics self.calculatedTargetOnDirection = self.targetOnDirection.calculateUnsafe( for: kinetics.validCount) self.calculatedStrength = self.strength.calculateUnsafe(for: kinetics.validCount) } @inlinable public init( direction: DirectionOfPositionForce, targetOnDirection: TargetOnDirection, strength: PositionStrength = .constant(1.0) ) { self.strength = strength self.direction = direction.lane self.targetOnDirection = targetOnDirection } @inlinable public func dispose() {} } } extension Kinetics.DirectionOfPositionForce { @inlinable var lane: Int { switch self { case .x: return 0 case .y: return 1 case .z: return 2 case .entryOfVector(let i): return i } } } ================================================ FILE: Sources/ForceSimulation/Forces/RadialForce.swift ================================================ extension Kinetics { public typealias RadialBound = AttributeDescriptor public typealias RadialStrength = AttributeDescriptor /// A force that applies a radial force to all nodes. /// /// Center force is relatively fast, the complexity is `O(n)`, /// where `n` is the number of nodes. /// See [Position Force - D3](https://d3js.org/d3-force/position). public struct RadialForce: ForceProtocol { // @usableFromInline var kinetics: Kinetics! = nil public var radius: RadialBound public var strength: RadialStrength public var center: Vector @usableFromInline var calculatedRadius: UnsafeArray! = nil @usableFromInline var calculatedStrength: UnsafeArray! = nil @inlinable public func apply(to kinetics: inout Kinetics) { let alpha = kinetics.alpha for i in kinetics.range { let deltaPosition = (kinetics.position[i] - self.center).jiggled( by: &kinetics.randomGenerator) //.jiggled() let r = deltaPosition.length() let k = (self.calculatedRadius[i] * self.calculatedStrength[i] * alpha) / r kinetics.velocity[i] += deltaPosition * k } } @inlinable public mutating func bindKinetics(_ kinetics: Kinetics) { // self.kinetics = kinetics self.calculatedRadius = self.radius.calculateUnsafe(for: kinetics.validCount) self.calculatedStrength = self.strength.calculateUnsafe(for: kinetics.validCount) } @inlinable public init(center: Vector, radius: RadialBound, strength: RadialStrength) { self.center = center self.radius = radius self.strength = strength } @inlinable public func dispose() {} } } ================================================ FILE: Sources/ForceSimulation/Forces/SealedForce2D.swift ================================================ /// A force that can be composed of one or multiple forces. The forces you can add /// here include: /// - `Kinetics2D.CenterForce` /// - `Kinetics2D.RadialForce` /// - `Kinetics2D.ManyBodyForce` /// - `Kinetics2D.LinkForce` /// - `Kinetics2D.CollideForce` /// - `Kinetics2D.PositionForce` /// - `Kinetics2D.EmptyForce` /// /// If you want to add a custom force, checkout `CompositedForce`. public struct SealedForce2D: Force2D { public var entries: [ForceEntry] = [] @inlinable public func apply(to kinetics: inout Kinetics>) { for force in self.entries { force.apply(to: &kinetics) } } @inlinable public func dispose() { for force in self.entries { force.dispose() } } @inlinable public init( _ entries: [ForceEntry] ) { self.entries = entries } @inlinable public mutating func bindKinetics(_ kinetics: Kinetics>) { self.entries = self.entries.map { entry in switch entry { case .center(var force): force.bindKinetics(kinetics) return .center(force) case .radial(var force): force.bindKinetics(kinetics) return .radial(force) case .manyBody(var force): force.bindKinetics(kinetics) return .manyBody(force) case .link(var force): force.bindKinetics(kinetics) return .link(force) case .collide(var force): force.bindKinetics(kinetics) return .collide(force) case .position(var force): force.bindKinetics(kinetics) return .position(force) default: return entry } } } public enum ForceEntry { case center(Kinetics2D.CenterForce) case radial(Kinetics2D.RadialForce) case manyBody(Kinetics2D.ManyBodyForce) case link(Kinetics2D.LinkForce) case collide(Kinetics2D.CollideForce) case position(Kinetics2D.PositionForce) case empty @inlinable public func dispose() { switch self { case .center(let force): force.dispose() case .radial(let force): force.dispose() case .manyBody(let force): force.dispose() case .link(let force): force.dispose() case .collide(let force): force.dispose() case .position(let force): force.dispose() default: break } } @inlinable public func apply(to kinetics: inout Kinetics>) { switch self { case .center(let force): force.apply(to: &kinetics) case .radial(let force): force.apply(to: &kinetics) case .manyBody(let force): force.apply(to: &kinetics) case .link(let force): force.apply(to: &kinetics) case .collide(let force): force.apply(to: &kinetics) case .position(let force): force.apply(to: &kinetics) default: break } } } @inlinable public init(@SealedForce2DBuilder _ builder: () -> [ForceEntry]) { self.entries = builder() } } @resultBuilder public struct SealedForce2DBuilder { public static func buildBlock(_ components: SealedForce2D.ForceEntry...) -> [SealedForce2D.ForceEntry] { components } public static func buildExpression(_ expression: FD) -> SealedForce2D.ForceEntry where FD: ForceDescriptor, FD.ConcreteForce: Force2D { let f = expression.createForce() switch f { case let f as Kinetics2D.CenterForce: return .center(f) case let f as Kinetics2D.RadialForce: return .radial(f) case let f as Kinetics2D.ManyBodyForce: return .manyBody(f) case let f as Kinetics2D.LinkForce: return .link(f) case let f as Kinetics2D.CollideForce: return .collide(f) case let f as Kinetics2D.PositionForce: return .position(f) default: return .empty } } public static func buildExpression(_ f: F) -> SealedForce2D.ForceEntry where F:Force2D { switch f { case let f as Kinetics2D.CenterForce: return .center(f) case let f as Kinetics2D.RadialForce: return .radial(f) case let f as Kinetics2D.ManyBodyForce: return .manyBody(f) case let f as Kinetics2D.LinkForce: return .link(f) case let f as Kinetics2D.CollideForce: return .collide(f) case let f as Kinetics2D.PositionForce: return .position(f) default: return .empty } } } ================================================ FILE: Sources/ForceSimulation/Forces/SealedForce3D.swift ================================================ /// A force that can be composed of one or multiple forces. The forces you can add /// here include: /// - `Kinetics3D.CenterForce` /// - `Kinetics3D.RadialForce` /// - `Kinetics3D.ManyBodyForce` /// - `Kinetics3D.LinkForce` /// - `Kinetics3D.CollideForce` /// - `Kinetics3D.PositionForce` /// - `Kinetics3D.EmptyForce` /// /// If you want to add a custom force, checkout `CompositedForce`. public struct SealedForce3D: Force3D { public var entries: [ForceEntry] = [] @inlinable public func apply(to kinetics: inout Kinetics>) { for force in self.entries { force.apply(to: &kinetics) } } @inlinable public func dispose() { for force in self.entries { force.dispose() } } @inlinable public init( _ entries: [ForceEntry] ) { self.entries = entries } @inlinable public mutating func bindKinetics(_ kinetics: Kinetics>) { self.entries = self.entries.map { entry in switch entry { case .center(var force): force.bindKinetics(kinetics) return .center(force) case .radial(var force): force.bindKinetics(kinetics) return .radial(force) case .manyBody(var force): force.bindKinetics(kinetics) return .manyBody(force) case .link(var force): force.bindKinetics(kinetics) return .link(force) case .collide(var force): force.bindKinetics(kinetics) return .collide(force) case .position(var force): force.bindKinetics(kinetics) return .position(force) default: return entry } } } public enum ForceEntry { case center(Kinetics3D.CenterForce) case radial(Kinetics3D.RadialForce) case manyBody(Kinetics3D.ManyBodyForce) case link(Kinetics3D.LinkForce) case collide(Kinetics3D.CollideForce) case position(Kinetics3D.PositionForce) case empty @inlinable public func dispose() { switch self { case .center(let force): force.dispose() case .radial(let force): force.dispose() case .manyBody(let force): force.dispose() case .link(let force): force.dispose() case .collide(let force): force.dispose() case .position(let force): force.dispose() default: break } } @inlinable public func apply(to kinetics: inout Kinetics>) { switch self { case .center(let force): force.apply(to: &kinetics) case .radial(let force): force.apply(to: &kinetics) case .manyBody(let force): force.apply(to: &kinetics) case .link(let force): force.apply(to: &kinetics) case .collide(let force): force.apply(to: &kinetics) case .position(let force): force.apply(to: &kinetics) default: break } } } @inlinable public init(@SealedForce3DBuilder _ builder: () -> [ForceEntry]) { self.entries = builder() } } @resultBuilder public struct SealedForce3DBuilder { public static func buildBlock(_ components: SealedForce3D.ForceEntry...) -> [SealedForce3D.ForceEntry] { components } public static func buildExpression(_ expression: FD) -> SealedForce3D.ForceEntry where FD: ForceDescriptor, FD.ConcreteForce: Force3D { let f = expression.createForce() switch f { case let f as Kinetics3D.CenterForce: return .center(f) case let f as Kinetics3D.RadialForce: return .radial(f) case let f as Kinetics3D.ManyBodyForce: return .manyBody(f) case let f as Kinetics3D.LinkForce: return .link(f) case let f as Kinetics3D.CollideForce: return .collide(f) case let f as Kinetics3D.PositionForce: return .position(f) default: return .empty } } public static func buildExpression(_ f: F) -> SealedForce3D.ForceEntry where F:Force3D { switch f { case let f as Kinetics3D.CenterForce: return .center(f) case let f as Kinetics3D.RadialForce: return .radial(f) case let f as Kinetics3D.ManyBodyForce: return .manyBody(f) case let f as Kinetics3D.LinkForce: return .link(f) case let f as Kinetics3D.CollideForce: return .collide(f) case let f as Kinetics3D.PositionForce: return .position(f) default: return .empty } } } ================================================ FILE: Sources/ForceSimulation/KDTree/BufferedKDTree.swift ================================================ public struct BufferedKDTree: Disposable where Vector: SimulatableVector & L2NormCalculatable, Delegate: KDTreeDelegate { public typealias Box = KDBox public typealias TreeNode = KDTreeNode @usableFromInline internal var rootPointer: UnsafeMutablePointer { treeNodeBuffer.mutablePointer } @usableFromInline internal var validCount: Int = 0 @usableFromInline internal var treeNodeBuffer: UnsafeArray @inlinable static internal var clusterDistanceSquared: Vector.Scalar { return Vector.clusterDistanceSquared } @inlinable public var root: TreeNode { rootPointer.pointee } @inlinable public init( rootBox: Box, nodeCapacity: Int, rootDelegate: @autoclosure () -> Delegate ) { // Assuming each add creates 2^Vector.scalarCount nodes // In most situations this is sufficient (for example the miserable graph) // But It's possible to exceed this limit: // 2 additions very close but not clustered in the same box // In this case there's no upperbound for addition so `resize` is needed let maxBufferCount = (nodeCapacity << Vector.scalarCount) + 1 self.rootDelegate = rootDelegate() treeNodeBuffer = .createBuffer( withHeader: maxBufferCount, count: maxBufferCount, initialValue: .zeroWithDelegate(self.rootDelegate) ) rootPointer.pointee = TreeNode( nodeIndices: nil, childrenBufferPointer: nil, delegate: self.rootDelegate, box: rootBox ) self.validCount = 1 } @usableFromInline internal var rootDelegate: Delegate @inlinable public mutating func reset( rootBox: Box, rootDelegate: @autoclosure () -> Delegate ) { self.rootDelegate = rootDelegate() treeNodeBuffer.withUnsafeMutablePointerToElements { for i in 0.. treeNodeBuffer.header) let rootCopy = root #endif let oldRootPointer = rootPointer let newTreeNodeBuffer = UnsafeArray.createBuffer( withHeader: newTreeNodeBufferSize, count: newTreeNodeBufferSize, moving: treeNodeBuffer.mutablePointer, movingCount: validCount, fillingExcessiveBufferWith: .zeroWithDelegate(self.rootDelegate) ) let newRootPointer = newTreeNodeBuffer.withUnsafeMutablePointerToElements { $0 } for i in 0.. Bool { if validCount + count > treeNodeBuffer.count { let factor = (count / self.treeNodeBuffer.count) + 2 resize(to: treeNodeBuffer.count * factor) assert(treeNodeBuffer.count >= validCount + count) return true } return false } @inlinable public mutating func add( nodeIndex: Int, at point: Vector ) { assert(validCount > 0) cover(point: point) addWithoutCover( onTreeNode: rootPointer, nodeOf: nodeIndex, at: point ) } @inlinable internal mutating func addWithoutCover( onTreeNode treeNode: UnsafeMutablePointer, nodeOf nodeIndex: Int, at point: Vector ) { if treeNode.pointee.childrenBufferPointer != nil { let directionOfNewNode = getIndexInChildren( point, relativeTo: treeNode.pointee.box.center) let treeNodeOffset = (treeNode) - rootPointer self.addWithoutCover( onTreeNode: treeNode.pointee.childrenBufferPointer! + directionOfNewNode, nodeOf: nodeIndex, at: point ) rootPointer[treeNodeOffset].delegate.didAddNode(nodeIndex, at: point) return } else if treeNode.pointee.nodeIndices == nil { // empty leaf treeNode.pointee.nodeIndices = TreeNode.NodeIndex(nodeIndex) treeNode.pointee.nodePosition = point treeNode.pointee.delegate.didAddNode(nodeIndex, at: point) return } else if treeNode.pointee.nodePosition.distanceSquared(to: point) > Self.clusterDistanceSquared { // filled leaf let treeNodeOffset = (treeNode) - rootPointer resizeIfNeededBeforeAllocation(for: Self.directionCount) let newTreeNode = self.rootPointer + treeNodeOffset let spawnedDelegate = newTreeNode.pointee.delegate.spawn() let center = newTreeNode.pointee.box.center let _box = newTreeNode.pointee.box for j in 0..> i) & 0b1 if isOnTheHigherRange != 0 { __box.p0[i] = center[i] } else { __box.p1[i] = center[i] } } let obsoletePtr = self.rootPointer + validCount + j obsoletePtr.pointee.disposeNodeIndices() obsoletePtr.pointee = TreeNode( nodeIndices: nil, childrenBufferPointer: nil, delegate: spawnedDelegate, box: __box ) } newTreeNode.pointee.childrenBufferPointer = rootPointer + validCount validCount += Self.directionCount if let childrenBufferPointer = newTreeNode.pointee.childrenBufferPointer { let direction = getIndexInChildren( newTreeNode.pointee.nodePosition, relativeTo: center ) // newly created, no need to dispose // childrenBufferPointer[direction].disposeNodeIndices() childrenBufferPointer[direction] = .init( nodeIndices: newTreeNode.pointee.nodeIndices, childrenBufferPointer: childrenBufferPointer[direction] .childrenBufferPointer, delegate: newTreeNode.pointee.delegate, box: childrenBufferPointer[direction].box, nodePosition: newTreeNode.pointee.nodePosition ) newTreeNode.pointee.nodeIndices = nil newTreeNode.pointee.nodePosition = .zero } let directionOfNewNode = getIndexInChildren(point, relativeTo: center) // This add might also resize this buffer! addWithoutCover( onTreeNode: newTreeNode.pointee.childrenBufferPointer! + directionOfNewNode, nodeOf: nodeIndex, at: point ) rootPointer[treeNodeOffset].delegate.didAddNode(nodeIndex, at: point) return } else { // filled leaf and within cluster distance treeNode.pointee.nodeIndices!.append(nodeIndex: nodeIndex) treeNode.pointee.delegate.didAddNode(nodeIndex, at: point) return } } @inlinable internal mutating func cover(point: Vector) { if self.root.box.contains(point) { return } repeat { let direction = self.getIndexInChildren(point, relativeTo: self.root.box.p0) self.expand(towards: direction) } while !self.root.box.contains(point) } @inlinable internal mutating func expand(towards direction: Int) { let nailedDirection = (Self.directionCount - 1) - direction let nailedCorner = self.root.box.getCorner(of: nailedDirection) let _corner = self.root.box.getCorner(of: direction) let expandedCorner = (_corner + _corner) - nailedCorner let newRootBox = Box(nailedCorner, expandedCorner) let _rootValue = self.root // spawn the delegate with the same internal values // for the children, use implicit copy of spawned let spawned = _rootValue.delegate.spawn() let newChildrenPointer = self.rootPointer + validCount resizeIfNeededBeforeAllocation(for: Self.directionCount) for j in 0..> i) & 0b1 // TODO: use simd mask if isOnTheHigherRange != 0 { __box.p0[i] = _corner[i] } else { __box.p1[i] = _corner[i] } } // newly allocated, no need to dispose if j != nailedDirection { self.treeNodeBuffer[validCount + j] = TreeNode( nodeIndices: nil, childrenBufferPointer: nil, delegate: spawned, box: __box, nodePosition: .zero ) } else { self.treeNodeBuffer[validCount + j] = TreeNode( nodeIndices: _rootValue.nodeIndices, childrenBufferPointer: _rootValue.childrenBufferPointer, delegate: _rootValue.delegate, box: __box, nodePosition: _rootValue.nodePosition ) } } self.validCount += Self.directionCount // don't dispose, they are used in treeNodeBuffer[validCount + j] self.rootPointer.pointee = .init( nodeIndices: nil, childrenBufferPointer: newChildrenPointer, delegate: _rootValue.delegate, box: newRootBox ) } @inlinable static internal var directionCount: Int { 1 << Vector.scalarCount } @inlinable public func dispose() { treeNodeBuffer.withUnsafeMutablePointerToElements { for i in 0.. Int { var index = 0 let mask = point .>= originalPoint for i in 0..) -> Bool ) { rootPointer.pointee.visit(shouldVisitChildren: shouldVisitChildren) } } ================================================ FILE: Sources/ForceSimulation/KDTree/KDBox.swift ================================================ // // KDBox.swift // // // Created by li3zhen1 on 10/14/23. // import simd /// A box in N-dimensional space. /// /// - Note: `p0` is the minimum point of the box, `p1` is the maximum point of the box. public struct KDBox: Equatable where V: SIMD, V.Scalar: FloatingPoint & ExpressibleByFloatLiteral { /// the minimum anchor of the box public var p0: V /// the maximum anchor of the box public var p1: V /// Create a box with 2 anchors. /// /// - Parameters: /// - p0: anchor /// - p1: another anchor in the diagonal position of `p0` /// - Note: `p0` you pass does not have to be minimum point of the box. /// `p1` does not have to be maximum point of the box. The initializer will /// automatically adjust the order of `p0` and `p1` to make sure `p0` is the /// minimum point of the box and `p1` is the maximum point of the box. @inlinable public init(p0: V, p1: V) { // #if DEBUG assert(p0 != p1, "NdBox was initialized with 2 same anchor") // #endif // var p0 = p0 // var p1 = p1 // for i in p0.indices { // if p1[i] < p0[i] { // swap(&p0[i], &p1[i]) // } // } // let mask = p0 .< p1 // self.p0 = p1.replacing(with: p0, where: mask) // self.p1 = p0.replacing(with: p1, where: mask) self.p0 = pointwiseMin(p0, p1) self.p1 = pointwiseMax(p0, p1) // TODO: use Mask } /// Create a box with 2 anchors. /// /// - Parameters: /// - pMin: minimum anchor of the box /// - pMax: maximum anchor of the box /// - Note: Please make sure `pMin` is the minimum point of the box and `pMax` is the /// maximum point of the box. @inlinable @available(*, deprecated, renamed: "init(uncheckedP0:uncheckedP1:)") internal init(pMin: V, pMax: V) { assert(pMin != pMax, "NdBox was initialized with 2 same anchor") self.p0 = pMin self.p1 = pMax } @inlinable internal init(uncheckedP0: V, uncheckedP1: V) { self.p0 = uncheckedP0 self.p1 = uncheckedP1 } /// Create a box with 2 zero anchors. @inlinable @available(*, deprecated, renamed: "zero") public init() { p0 = .zero p1 = .zero } /// Create a box with 2 anchors. /// - Parameters: /// - p0: anchor /// - p1: another anchor in the diagonal position of `p0` /// - Note: `p0` you pass does not have to be minimum point of the box. /// `p1` does not have to be maximum point of the box. The initializer will /// automatically adjust the order of `p0` and `p1` to make sure `p0` is the /// minimum point of the box and `p1` is the maximum point of the box. @inlinable public init(_ p0: V, _ p1: V) { self.init(p0: p0, p1: p1) } } extension KDBox { @inlinable @inline(__always) var diagnalVector: V { return p1 - p0 } @inlinable static var zero: Self { return Self(uncheckedP0: .zero, uncheckedP1: .zero) } @inlinable @inline(__always) var center: V { (p1 + p0) / 2.0 } /// Test if the box contains a point. /// - Parameter point: N dimensional point /// - Returns: `true` if the box contains the point, `false` otherwise. /// The boundary test is similar to ..< operator. @inlinable @inline(__always) func contains(_ point: V) -> Bool { // let mask = return !any((p0 .> point) .| (point .>= p1)) //mask == .init(repeating: false) // equivalent to: // for i in point.indices { // if p0[i] > point[i] || point[i] >= p1[i] { // return false // } // } // return true } } extension KDBox { @inlinable func getCorner(of direction: Int) -> V { var mask = SIMDMask() for i in 0..> i) & 0b1) == 1 } return p0.replacing(with: p1, where: mask) // equivalent to: // var corner = V.zero // for i in 0..> i) & 0b1) == 1 ? p1[i] : p0[i] // } // return p0.replacing(with: p1, where: mask) //corner } // @inlinable public var debugDescription: String { // return "[\(p0), \(p1)]" // } } extension KDBox { /// Get the small box that contains a list points and guarantees the box's size is at least 1x..x1. /// /// - Parameter points: The points to be covered. /// - Returns: The box that contains all the points. @inlinable public static func cover(of points: [V]) -> Self { var _p0 = points[0] var _p1 = points[0] for p in points { // let mask1 = p .< _p0 // let mask2 = p .>= _p1 // _p0 = _p0.replacing(with: p, where: mask1) // _p1 = _p1.replacing(with: p + 1, where: mask2) _p0 = pointwiseMin(p, _p0) _p1 = pointwiseMax(p, _p1) // for i in p.indices { // if p[i] < _p0[i] { // _p0[i] = p[i] // } // if p[i] >= _p1[i] { // _p1[i] = p[i] + 1 // } // } } #if DEBUG let _box = Self(_p0, _p1) assert( points.allSatisfy { p in _box.contains(p) }) #endif return Self(_p0, _p1) } @inlinable public static func cover(of points: UnsafeArray) -> Self { var _p0 = points[0] var _p1 = points[0] for pi in 0.. _p1[i] { // _p1[i] = p[i] + 1 // } // } } #if DEBUG let testBox = Self(_p0, _p1 + 1) for i in points.range { assert(testBox.contains(points[i])) } #endif return Self(_p0, _p1 + 1) } } ================================================ FILE: Sources/ForceSimulation/KDTree/KDTree.swift ================================================ /// A node in NDTree /// - Note: `NDTree` is a generic type that can be used in any dimension. /// `NDTree` is a value type. public struct KDTree where Vector: SimulatableVector & L2NormCalculatable, Delegate: KDTreeDelegate { public typealias NodeIndex = Delegate.NodeID public typealias Direction = Int public typealias Box = KDBox public var box: Box public var children: [KDTree]? public var nodePosition: Vector? public var nodeIndices: [NodeIndex] @inlinable var clusterDistanceSquared: Vector.Scalar { return Vector.clusterDistanceSquared } public var delegate: Delegate @inlinable public init( box: Box, // clusterDistanceSquared: Vector.Scalar, spawnedDelegateBeingConsumed: Delegate ) { self.box = box self.nodeIndices = [] self.delegate = spawnedDelegateBeingConsumed } @inlinable init( box: Box, spawnedDelegateBeingConsumed: Delegate, childrenBeingConsumed: [KDTree] ) { self.box = box self.nodeIndices = [] self.delegate = spawnedDelegateBeingConsumed self.children = childrenBeingConsumed } @inlinable static var directionCount: Int { 1 << Vector.scalarCount } @inlinable mutating func cover(_ point: Vector) { if box.contains(point) { return } repeat { let direction = getIndexInChildren(point, relativeTo: box.p0) expand(towards: direction) } while !box.contains(point) } /// Get the index of the child that contains the point. /// /// **Complexity**: `O(n*(2^n))`, where `n` is the dimension of the vector. @inlinable func getIndexInChildren(_ point: Vector, relativeTo originalPoint: Vector) -> Int { var index = 0 let mask = point .>= originalPoint for i in 0..]() result.reserveCapacity(Self.directionCount) // let center = newRootBox.center for j in 0..> i) & 0b1 // TODO: use simd mask if isOnTheHigherRange != 0 { __box.p0[i] = _corner[i] } else { __box.p1[i] = _corner[i] } } result.append( Self( box: __box, // clusterDistanceSquared: clusterDistanceSquared, spawnedDelegateBeingConsumed: j != nailedDirection ? self.delegate : spawned ) ) } // result[nailedDirection] = tempSelf self = Self( box: newRootBox, spawnedDelegateBeingConsumed: self.delegate, childrenBeingConsumed: result ) } @inlinable public mutating func add(_ nodeIndex: NodeIndex, at point: Vector) { cover(point) addWithoutCover(nodeIndex, at: point) } @inlinable public mutating func addWithoutCover(_ nodeIndex: NodeIndex, at point: Vector) { defer { delegate.didAddNode(nodeIndex, at: point) } guard children != nil else { if nodePosition == nil { nodeIndices.append(nodeIndex) nodePosition = point return } else if nodePosition!.distanceSquared(to: point) < clusterDistanceSquared { // the condition (nodePosition == point) is mostly only true when the tree is initialized // hence omitted nodeIndices.append(nodeIndex) return } else { var spawnedChildren = [KDTree]() spawnedChildren.reserveCapacity(Self.directionCount) let spawendDelegate = self.delegate.spawn() let center = box.center for j in 0..> i) & 0b1 // TODO: use simd mask if isOnTheHigherRange != 0 { __box.p0[i] = center[i] } else { __box.p1[i] = center[i] } } spawnedChildren.append( Self( box: __box, // clusterDistanceSquared: clusterDistanceSquared, spawnedDelegateBeingConsumed: spawendDelegate ) ) } if let nodePosition { let direction = getIndexInChildren(nodePosition, relativeTo: box.center) spawnedChildren[direction].nodeIndices = self.nodeIndices spawnedChildren[direction].nodePosition = self.nodePosition spawnedChildren[direction].delegate = self.delegate self.nodeIndices = [] self.nodePosition = nil // TODO: Consume } let directionOfNewNode = getIndexInChildren(point, relativeTo: box.center) spawnedChildren[directionOfNewNode].addWithoutCover(nodeIndex, at: point) self.children = spawnedChildren return } } let directionOfNewNode = getIndexInChildren(point, relativeTo: box.center) self.children![directionOfNewNode].addWithoutCover(nodeIndex, at: point) return } } extension KDTree where Delegate.NodeID == Int { /// Initialize a KDTree with a list of points and a key path to the vector. /// /// - Parameters: /// - points: A list of points. The points are only used to calculate the covering box. You should still call `add` to add the points to the tree. /// - clusterDistance: If 2 points are close enough, they will be clustered into the same leaf node. /// - buildRootDelegate: A closure that tells the tree how to initialize the data you want to store in the rootPointer. /// The closure is called only once. The `NDTreeDelegate` will then be created in children tree nods by calling `spawn` on the rootPointer delegate. @inlinable public init( covering points: [Vector], buildRootDelegate: () -> Delegate ) { let coveringBox = Box.cover(of: points) self.init( box: coveringBox, spawnedDelegateBeingConsumed: buildRootDelegate() ) for i in points.indices { add(i, at: points[i]) } } @inlinable public init( covering points: UnsafeArray, buildRootDelegate: () -> Delegate ) { let coveringBox = Box.cover(of: points) self.init( box: coveringBox, spawnedDelegateBeingConsumed: buildRootDelegate() ) for i in 0.., rootDelegate: @autoclosure () -> Delegate ) { let coveringBox = Box.cover(of: points) self.init( box: coveringBox, spawnedDelegateBeingConsumed: rootDelegate() ) for i in 0..( // covering points: [T], // keyPath: KeyPath, // buildRootDelegate: () -> Delegate // ) { // let coveringBox = Box.cover(of: points, keyPath: keyPath) // self.init( // box: coveringBox, clusterDistance: clusterDistance, buildRootDelegate: buildRootDelegate // ) // for i in points.indices { // add(i, at: points[i][keyPath: keyPath]) // } // } } extension KDTree { /// The bounding box of the current node @inlinable public var extent: Box { box } /// Returns true is the current tree node is leaf. /// /// Does not guarantee that the tree node has point in it. @inlinable public var isLeaf: Bool { children == nil } /// Returns true is the current tree node is internal. /// /// Internal tree node are always empty and do not contain any points. @inlinable public var isInternalNode: Bool { children != nil } /// Returns true is the current tree node is leaf and has point in it. @inlinable public var isFilledLeaf: Bool { nodePosition != nil } /// Returns true is the current tree node is leaf and does not have point in it. @inlinable public var isEmptyLeaf: Bool { nodePosition == nil } /// Visit the tree in pre-order. /// /// - Parameter shouldVisitChildren: a closure that returns a boolean value indicating whether should continue to visit children. @inlinable public mutating func visit( shouldVisitChildren: (inout KDTree) -> Bool ) { if shouldVisitChildren(&self) && children != nil { // this is an internal node for i in children!.indices { children![i].visit(shouldVisitChildren: shouldVisitChildren) } } } } ================================================ FILE: Sources/ForceSimulation/KDTree/KDTreeDelegate.swift ================================================ // // NDTree.swift // // // Created by li3zhen1 on 10/14/23. // /// The data structure carried by a node of NDTree. /// /// It receives notifications when a node is added or removed on a node, /// regardless of whether the node is internal or leaf. /// It is designed to calculate properties like a box's center of mass. /// /// When implementing your delegates, ensure they /// are value types to enable memberwise copy. public protocol KDTreeDelegate { associatedtype NodeID: Hashable associatedtype Vector: SIMD where Vector.Scalar: FloatingPoint & ExpressibleByFloatLiteral /// Called when a node is added on a node, regardless of whether the node is internal or leaf. /// /// If you add `n` points to the root, this method will be called `n` times in the root delegate, /// although it is probably not containing points now. /// - Parameters: /// - node: The nodeID of the node that is added. /// - position: The position of the node that is added. @inlinable mutating func didAddNode(_ node: NodeID, at position: Vector) /// Called when a node is removed on a node, regardless of whether the node is internal or leaf. @inlinable mutating func didRemoveNode(_ node: NodeID, at position: Vector) /// Copy object. /// /// This method is called when the root box is not large enough to cover the new nodes. // @inlinable func copy() -> Self /// Create new object with properties set to initial value as if the box is empty. /// /// However, you can still carry something like a closure to get information from outside. /// This method is called when a leaf box is splited due to the insertion of a new node in this box. @inlinable func spawn() -> Self } ================================================ FILE: Sources/ForceSimulation/KDTree/KDTreeNode.swift ================================================ public struct KDTreeNode where Vector: SimulatableVector & L2NormCalculatable, Delegate: KDTreeDelegate { public var box: KDBox public var nodePosition: Vector public var childrenBufferPointer: UnsafeMutablePointer? @usableFromInline internal var nodeIndices: NodeIndex? public var delegate: Delegate @inlinable init( nodeIndices: NodeIndex?, childrenBufferPointer: UnsafeMutablePointer?, delegate: Delegate, box: KDBox, nodePosition: Vector = .zero ) { self.childrenBufferPointer = childrenBufferPointer self.nodeIndices = nodeIndices self.delegate = delegate self.box = box self.nodePosition = nodePosition } @inlinable mutating public func disposeNodeIndices() { nodeIndices?.dispose() nodeIndices = nil } } extension KDTreeNode { @usableFromInline struct NodeIndex: Disposable { @usableFromInline var index: Int @usableFromInline var next: UnsafeMutablePointer? } } extension KDTreeNode.NodeIndex { @inlinable internal init( nodeIndex: Int ) { self.index = nodeIndex self.next = nil } @inlinable internal init( _ nodeIndex: Int ) { self.index = nodeIndex self.next = nil } @inlinable internal mutating func append(nodeIndex: Int) { if let next { next.pointee.append(nodeIndex: nodeIndex) } else { next = .allocate(capacity: 1) next!.initialize(to: .init(nodeIndex: nodeIndex)) // next!.pointee = .init(nodeIndex: nodeIndex) } } @inlinable internal func dispose() { if let next { next.pointee.dispose() next.deallocate() } } @inlinable internal func contains(_ nodeIndex: Int) -> Bool { if index == nodeIndex { return true } if let next { return next.pointee.contains(nodeIndex) } else { return false } } @inlinable internal func forEach(_ body: (Int) -> Void) { body(index) if let next { next.pointee.forEach(body) } } } extension KDTreeNode { /// Returns true is the current tree node is leaf. /// /// Does not guarantee that the tree node has point in it. @inlinable public var isLeaf: Bool { childrenBufferPointer == nil } /// Returns true is the current tree node is internal. /// /// Internal tree node are always empty and do not contain any points. @inlinable public var isInternalNode: Bool { childrenBufferPointer != nil } /// Returns true is the current tree node is leaf and has point in it. @inlinable public var isFilledLeaf: Bool { nodeIndices != nil } /// Returns true is the current tree node is leaf and does not have point in it. @inlinable public var isEmptyLeaf: Bool { nodeIndices == nil } /// Visit the tree in pre-order. /// /// - Parameter shouldVisitChildren: a closure that returns a boolean value indicating whether should continue to visit children. @inlinable public mutating func visit( shouldVisitChildren: (inout KDTreeNode) -> Bool ) { if shouldVisitChildren(&self) && childrenBufferPointer != nil { // this is an internal node for i in 0...directionCount { childrenBufferPointer![i].visit(shouldVisitChildren: shouldVisitChildren) } } } /// Returns an array of point indices in the tree node. @inlinable public var containedIndices: [Int] { guard isFilledLeaf else { return [] } var result: [Int] = [] nodeIndices!.forEach { result.append($0) } return result } @inlinable static func zeroWithDelegate(_ delegate: Delegate) -> Self { return Self( nodeIndices: nil, childrenBufferPointer: nil, delegate: delegate, box: .zero, nodePosition: .zero ) } } ================================================ FILE: Sources/ForceSimulation/Kinetics.swift ================================================ /// A class that holds the state of the simulation, which /// includes the positions, velocities of the nodes. public struct Kinetics where Vector: SimulatableVector & L2NormCalculatable { /// The position of points stored in simulation. /// /// Ordered as the nodeIds you passed in when initializing simulation. /// They are always updated. /// Exposed publicly so examples & clients can read out the latest positions. public var position: UnsafeArray // public var positionBufferPointer: UnsafeMutablePointer /// The velocities of points stored in simulation. /// /// Ordered as the nodeIds you passed in when initializing simulation. /// They are always updated. @usableFromInline package var velocity: UnsafeArray // public var velocityBufferPointer: UnsafeMutablePointer /// The fixed positions of points stored in simulation. /// /// Ordered as the nodeIds you passed in when initializing simulation. /// They are always updated. @usableFromInline package var fixation: UnsafeArray public var validCount: Int public var alpha: Vector.Scalar public let alphaMin: Vector.Scalar public let alphaDecay: Vector.Scalar public let alphaTarget: Vector.Scalar public let velocityDecay: Vector.Scalar @usableFromInline var randomGenerator: Vector.Scalar.Generator @usableFromInline package let links: [EdgeID] // public var validRanges: [Range] // public var validRanges: Range @inlinable package var range: Range { return 0..], initialAlpha: Vector.Scalar, alphaMin: Vector.Scalar, alphaDecay: Vector.Scalar, alphaTarget: Vector.Scalar, velocityDecay: Vector.Scalar, position: [Vector], velocity: [Vector], fixation: [Vector?] ) { self.links = links // self.initializedAlpha = initialAlpha self.alpha = initialAlpha self.alphaMin = alphaMin self.alphaDecay = alphaDecay self.alphaTarget = alphaTarget self.velocityDecay = velocityDecay let count = position.count self.validCount = count self.position = .createBuffer(moving: position, fillingWithIfFailed: .zero) self.velocity = .createBuffer(moving: velocity, fillingWithIfFailed: .zero) self.fixation = .createBuffer(moving: fixation, fillingWithIfFailed: nil) self.randomGenerator = .init() } @inlinable package static var empty: Kinetics { Kinetics( links: [], initialAlpha: 0, alphaMin: 0, alphaDecay: 0, alphaTarget: 0, velocityDecay: 0, position: [], velocity: [], fixation: [] ) } } extension Kinetics { @inlinable @inline(__always) func updatePositions() { for i in range { if let fix = fixation[i] { position[i] = fix } else { velocity[i] *= velocityDecay position[i] += velocity[i] } } } @inlinable @inline(__always) mutating func updateAlpha() { alpha += alphaTarget - alpha * alphaDecay } } public typealias Kinetics2D = Kinetics> public typealias Kinetics3D = Kinetics> ================================================ FILE: Sources/ForceSimulation/Simulation.swift ================================================ @usableFromInline package enum Ticks: Sendable { case untilReachingAlpha(Scalar?) case iteration(Int) @inlinable public static var zero: Self { .iteration(0) } @inlinable public static var untilStable: Self { .untilReachingAlpha(nil) } } /// An any-dimensional force simulation. /// The points are placed in a space where you use a SIMD data structure /// to describe their coordinates. public final class Simulation: @unchecked Sendable where Vector: SimulatableVector & L2NormCalculatable, ForceField: ForceProtocol { @usableFromInline var forceField: ForceField public var kinetics: Kinetics /// Create a new simulation. /// /// - Parameters: /// - nodeCount: Count of the nodes. Force simulation calculate them by order once created. /// - links: The links between nodes. /// - forceField: The force field that drives the simulation. The simulation takes ownership of the force field. /// - alpha: Initial alpha value, determines how "active" the simulation is. /// - alphaMin: The minimum alpha value. The simulation stops when alpha is less than this value. /// - alphaDecay: The larger the value, the faster the simulation converges to the final result. /// - alphaTarget: The alpha value the simulation converges to. /// - velocityDecay: A multiplier for the velocity of the nodes in Velocity Verlet integration. The position of the nodes is updated by the formula `x += v * velocityDecay`. // @inlinable // public init( // nodeCount: Int, // links: [EdgeID], // forceField: ForceField, // initialAlpha: Vector.Scalar = 1, // alphaMin: Vector.Scalar = 1e-2, // alphaDecay: Vector.Scalar = 2e-3, // alphaTarget: Vector.Scalar = 0.0, // velocityDecay: Vector.Scalar = 0.6 // ) { // self.kinetics = .createZeros( // links: links, // initialAlpha: initialAlpha, // alphaMin: alphaMin, // alphaDecay: alphaDecay, // alphaTarget: alphaTarget, // velocityDecay: velocityDecay, // count: nodeCount // ) // // self.kinetics.jigglePosition() // forceField.bindKinetics(self.kinetics) // self.forceField = forceField // } /// Create a new simulation. /// /// - Parameters: /// - nodeCount: Count of the nodes. Force simulation calculate them by order once created. /// - links: The links between nodes. /// - forceField: The force field that drives the simulation. The simulation takes ownership of the force field. /// - alpha: Initial alpha value, determines how "active" the simulation is. /// - alphaMin: The minimum alpha value. The simulation stops when alpha is less than this value. /// - alphaDecay: The larger the value, the faster the simulation converges to the final result. /// - alphaTarget: The alpha value the simulation converges to. /// - velocityDecay: A multiplier for the velocity of the nodes in Velocity Verlet integration. The position of the nodes is updated by the formula `x += v * velocityDecay`. @inlinable public init( nodeCount: Int, links: [EdgeID], forceField: consuming ForceField, initialAlpha: Vector.Scalar = 1, alphaMin: Vector.Scalar = 1e-3, alphaDecay: Vector.Scalar = 1e-2, alphaTarget: Vector.Scalar = 0.0, velocityDecay: Vector.Scalar = 0.6, position: [Vector]? = nil, velocity: [Vector]? = nil, fixation: [Vector?]? = nil ) { self.kinetics = Kinetics( links: links, initialAlpha: initialAlpha, alphaMin: alphaMin, alphaDecay: alphaDecay, alphaTarget: alphaTarget, velocityDecay: velocityDecay, position: position ?? Array(repeating: .zero, count: nodeCount), velocity: velocity ?? Array(repeating: .zero, count: nodeCount), fixation: fixation ?? Array(repeating: nil, count: nodeCount) ) // self.kinetics.jigglePosition() forceField.bindKinetics(self.kinetics) self.forceField = forceField } /// Run a number of iterations of ticks. @inlinable @inline(__always) public func tick(iterations: UInt = 1) { // print(self.kinetics.alpha, self.kinetics.alphaMin) guard self.kinetics.alpha >= self.kinetics.alphaMin else { return } for _ in 0..) { switch ticks { case .iteration(let count): self.tick(iterations: UInt(count)) case .untilReachingAlpha(let alpha): let alpha = alpha ?? self.kinetics.alphaMin while self.kinetics.alpha >= alpha { self.kinetics.updateAlpha() self.forceField.apply(to: &self.kinetics) self.kinetics.updatePositions() } } } @inlinable deinit { self.forceField.dispose() } } public typealias Simulation2D = Simulation, ForceField> where ForceField: ForceProtocol> public typealias Simulation3D = Simulation, ForceField> where ForceField: ForceProtocol> ================================================ FILE: Sources/ForceSimulation/Utils/AttributeDescriptor.swift ================================================ public enum AttributeDescriptor { case varied((Int) -> T) case constant(T) } extension AttributeDescriptor: Equatable where T: Equatable { @inlinable public static func == (lhs: AttributeDescriptor, rhs: AttributeDescriptor) -> Bool { switch (lhs, rhs) { case (.constant(let l), .constant(let r)): return l == r default: return false } } } extension AttributeDescriptor { @inlinable func calculate(for count: Int) -> [T] { switch self { case .constant(let m): return [T](repeating: m, count: count) case .varied(let radiusProvider): return (0.. UnsafeArray where T: Numeric { switch self { case .constant(let m): return UnsafeArray.createBuffer( withHeader: count, count: count, initialValue: m ) case .varied(let valueProvider): let array = UnsafeArray.createBuffer(withHeader: count, count: count) { valueProvider($0) } return array } } } ================================================ FILE: Sources/ForceSimulation/Utils/Disposable.swift ================================================ public protocol Disposable { @inlinable mutating func dispose() } extension UnsafeMutablePointer where Pointee: Disposable { /// Disposes the underlying memory block and /// deallocates the memory block previously allocated at this pointer. /// /// This pointer must be a pointer to the start of a previously allocated memory /// block. The memory must not be initialized or `Pointee` must be a trivial type. @inlinable public func dispose() { self.pointee.dispose() self.deallocate() } } ================================================ FILE: Sources/ForceSimulation/Utils/EdgeID.swift ================================================ /// A Hashable identifier for an edge. /// /// It’s a utility type for preserving `Hashable` conformance. public struct EdgeID: Hashable { public var source: NodeID public var target: NodeID public init(source: NodeID, target: NodeID) { self.source = source self.target = target } } extension EdgeID { public init(_ source: NodeID, _ target: NodeID) { self.source = source self.target = target } } ================================================ FILE: Sources/ForceSimulation/Utils/LinearCongruentialGenerator.swift ================================================ // TODO: https://forums.swift.org/t/deterministic-randomness-in-swift/20835/5 /// A random number generator that generates deterministic random numbers. public protocol DeterministicRandomGenerator { associatedtype Scalar where Scalar: FloatingPoint & ExpressibleByFloatLiteral associatedtype OverflowingInteger: FixedWidthInteger & UnsignedInteger @inlinable mutating func next() -> Scalar @inlinable init(seed: OverflowingInteger) @inlinable init() } /// A random number generator that generates deterministic random numbers for `Double`. public struct DoubleLinearCongruentialGenerator: DeterministicRandomGenerator { public typealias OverflowingInteger = UInt32 @usableFromInline internal static let a: UInt32 = 1_664_525 @usableFromInline internal static let c: UInt32 = 1_013_904_223 @usableFromInline internal var s: UInt32 = 1 @usableFromInline internal static let m: Double = 4_294_967_296 @inlinable public mutating func next() -> Double { // Perform the linear congruential generation with integer arithmetic. // The overflow addition and multiplication automatically wrap around, // thus imitating the modulo operation. s = (Self.a &* s) &+ Self.c // Convert the result to Double and divide by m to normalize it. return Double(s) / Self.m } @inlinable public init(seed: OverflowingInteger) { self.s = 1 //seed } @inlinable public init() { self.init(seed: 1) } } /// A random number generator that generates deterministic random numbers for `Float`. public struct FloatLinearCongruentialGenerator: DeterministicRandomGenerator { public typealias OverflowingInteger = UInt16 @usableFromInline internal static let a: UInt16 = 75 @usableFromInline internal static let c: UInt16 = 74 @usableFromInline internal var s: UInt16 = 1 @usableFromInline internal static let m: Float = 65537.0 @inlinable public mutating func next() -> Float { // Perform the linear congruential generation with integer arithmetic. // The overflow addition and multiplication automatically wrap around. s = (Self.a &* s) &+ Self.c // Convert the result to Float and divide by m to normalize it. return Float(s) / Self.m } @inlinable public init(seed: OverflowingInteger) { self.s = seed } @inlinable public init() { self.init(seed: 1) } } /// A floating point type that can be generated with a deterministic random number generator ``DeterministicRandomGenerator``. public protocol HasDeterministicRandomGenerator: FloatingPoint & ExpressibleByFloatLiteral { associatedtype Generator: DeterministicRandomGenerator where Generator.Scalar == Self } extension Double: HasDeterministicRandomGenerator { public typealias Generator = DoubleLinearCongruentialGenerator } extension Float: HasDeterministicRandomGenerator { public typealias Generator = FloatLinearCongruentialGenerator } extension HasDeterministicRandomGenerator { @inlinable static var jigglingScale: Self { return 1e-5 } @inlinable public func jiggled(by: UnsafeMutablePointer) -> Self { if self == .zero || self.isNaN { return (by.pointee.next() - 0.5) * Self.jigglingScale } return self } } ================================================ FILE: Sources/ForceSimulation/Utils/SimulatableVector.swift ================================================ /// A protocol for vectors that can be jiggled, and has a certain precision for /// simulation — so zero vectors could be altered /// into a small random non-zero vector, and then the force simulation could be /// could be numerically stable. public protocol SimulatableVector: SIMD where Scalar: FloatingPoint & HasDeterministicRandomGenerator { @inlinable static var clusterDistance: Scalar { get } @inlinable static var clusterDistanceSquared: Scalar { get } @inlinable func jiggled(by: UnsafeMutablePointer) -> Self } // extension SimulatableVector { // /// If the vector is zero, returns a vector with the same magnitude as `self` but pointing in a random direction, // /// otherwise returns `self`. // @inlinable // public func jiggled() -> Self { // var result = Self.zero // for i in indices { // result[i] = self[i].jiggled() // } // return result // } // } /// A protocol for vectors that can be calculated with L2 norms, i.e. Euclidean distance. public protocol L2NormCalculatable: SIMD where Scalar: FloatingPoint { @inlinable func distanceSquared(to point: Self) -> Scalar @inlinable func distance(to point: Self) -> Scalar @inlinable func lengthSquared() -> Scalar @inlinable func length() -> Scalar } extension SIMD2: SimulatableVector where Scalar: FloatingPoint & HasDeterministicRandomGenerator { @inlinable public static var clusterDistance: Scalar { return 1e-5 } @inlinable public static var clusterDistanceSquared: Scalar { return clusterDistance * clusterDistance } @inlinable public func jiggled(by: UnsafeMutablePointer) -> Self { return .init(x: self.x.jiggled(by: by), y: self.y.jiggled(by: by)) } } extension SIMD3: SimulatableVector where Scalar: FloatingPoint & HasDeterministicRandomGenerator { @inlinable public static var clusterDistance: Scalar { return 1e-5 } @inlinable public static var clusterDistanceSquared: Scalar { return clusterDistance * clusterDistance } @inlinable public func jiggled(by: UnsafeMutablePointer) -> Self { return .init( x: self.x.jiggled(by: by), y: self.y.jiggled(by: by), z: self.z.jiggled(by: by) ) } } #if canImport(simd) import simd extension SIMD2: L2NormCalculatable where Scalar == Double { @inlinable public func distanceSquared(to point: SIMD2) -> Scalar { return simd_distance_squared(self, point) } @inlinable public func distance(to point: SIMD2) -> Scalar { return simd_distance(self, point) } @inlinable public func lengthSquared() -> Scalar { return simd_length_squared(self) } @inlinable public func length() -> Scalar { return simd_fast_length(self) } } extension SIMD3: L2NormCalculatable where Scalar == Float { @inlinable public func distanceSquared(to point: SIMD3) -> Scalar { return simd_distance_squared(self, point) } @inlinable public func distance(to point: SIMD3) -> Scalar { return simd_distance(self, point) } @inlinable public func lengthSquared() -> Scalar { return simd_length_squared(self) } @inlinable public func length() -> Scalar { return simd_fast_length(self) } } #endif ================================================ FILE: Sources/ForceSimulation/Utils/UnsafeArray.swift ================================================ /// A wrapper of managed buffer that stores an array of elements. @_eagerMove public final class UnsafeArray: ManagedBuffer { @inlinable class func createBuffer(withHeader header: Int, count: Int, initialValue: Element) -> UnsafeArray { let buffer = self.create(minimumCapacity: count) { _ in header } buffer.withUnsafeMutablePointerToElements { $0.initialize(repeating: initialValue, count: count) } return unsafeDowncast(buffer, to: UnsafeArray.self) } @inlinable class func createBuffer(withHeader header: Int, count: Int, initializer: (Int) -> Element) -> UnsafeArray { let buffer = self.create(minimumCapacity: count) { _ in header } buffer.withUnsafeMutablePointerToElements { for i in 0.., movingCount: Int, fillingExcessiveBufferWith initialValue: Element ) -> UnsafeArray { let buffer = self.create(minimumCapacity: count) { _ in header } buffer.withUnsafeMutablePointerToElements { $0.moveInitialize(from: moving, count: movingCount) $0.advanced(by: movingCount).initialize( repeating: initialValue, count: count - movingCount ) } return unsafeDowncast(buffer, to: UnsafeArray.self) } @inlinable class func createBuffer( moving array: [Element], fillingWithIfFailed element: Element ) -> UnsafeArray { let buffer = self.create(minimumCapacity: array.count) { _ in array.count } array.withUnsafeBufferPointer { bufferPtr in if let baseAddr = bufferPtr.baseAddress { buffer.withUnsafeMutablePointerToElements { $0.moveInitialize(from: .init(mutating: baseAddr), count: array.count) } } else { buffer.withUnsafeMutablePointerToElements { for i in 0.. UnsafeArray { let buffer = self.create(minimumCapacity: count) { _ in count } // buffer.withUnsafeMutablePointerToElements { // $0.initialize(repeating: Element(), count: count) // } return unsafeDowncast(buffer, to: UnsafeArray.self) } @inlinable public var count: Int { return header } @inlinable public var range: Range { return 0..
Element { return withUnsafeMutablePointerToElements { $0[index] } } @inlinable func setElement(_ element: Element, at index: Int) { withUnsafeMutablePointerToElements { $0[index] = element } } @inlinable deinit { withUnsafeMutablePointers { headerPtr, elementPtr in elementPtr.deinitialize(count: self.header) headerPtr.deinitialize(count: 1) } } @inlinable public subscript(index: Int) -> Element { get { return withUnsafeMutablePointerToElements { $0[index] } } set { withUnsafeMutablePointerToElements { $0[index] = newValue } } } @inlinable public func asArray() -> [Element] { return withUnsafeMutablePointerToElements { Array(UnsafeBufferPointer(start: $0, count: self.header)) } } @inlinable public func firstIndex(where predicate: (Element) throws -> Bool) rethrows -> Int? { var result: Int? = nil try withUnsafeMutablePointerToElements { for i in 0.. { return withUnsafeMutablePointerToElements { $0 } } } ================================================ FILE: Sources/Grape/Contents/AnyGraphContent.swift ================================================ public struct AnyGraphContent: GraphContent { @usableFromInline let storage: any GraphContent @inlinable public init(_ storage: any GraphContent) { self.storage = storage } @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { storage._attachToGraphRenderingContext(&context) } } ================================================ FILE: Sources/Grape/Contents/GraphContent.swift ================================================ import SwiftUI public protocol GraphContent { associatedtype NodeID: Hashable associatedtype Body: GraphContent @inlinable func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) @inlinable @GraphContentBuilder var body: Body { get } } extension GraphContent { @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { body._attachToGraphRenderingContext(&context) } } ================================================ FILE: Sources/Grape/Contents/GraphContentBuilder.swift ================================================ @resultBuilder public struct GraphContentBuilder { public typealias Content = GraphContent @inlinable public static func buildPartialBlock(first content: T) -> T where T.NodeID == NodeID { return content } @inlinable public static func buildPartialBlock(accumulated: T1, next: T2) -> some Content where T1: Content, T2: Content, T1.NodeID == NodeID, T2.NodeID == NodeID, T1.NodeID == T2.NodeID { return _PairedGraphContent(accumulated, next) } @inlinable public static func buildBlock() -> some Content { return _EmptyGraphContent() } @inlinable public static func buildEither(first component: T1) -> _ConditionalGraphContent where T1: Content, T1.NodeID == NodeID, T2: Content, T2.NodeID == NodeID { return _ConditionalGraphContent(.trueContent(component)) } @inlinable public static func buildEither(second component: T2) -> _ConditionalGraphContent where T1: Content, T1.NodeID == NodeID, T2: Content, T2.NodeID == NodeID { return _ConditionalGraphContent(.falseContent(component)) } @inlinable public static func buildIf(_ component: T?) -> some Content where T: Content, T.NodeID == NodeID { return _OptionalGraphContent(component) } @inlinable public static func buildExpression(_ expression: T) -> T where T: Content, T.NodeID == NodeID { return expression } } ================================================ FILE: Sources/Grape/Contents/LinkMark.swift ================================================ import ForceSimulation import SwiftUI public struct LinkMark: GraphContent & Identifiable { @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } // public enum LabelDisplayStrategy { // case auto // case specified(Bool) // case byPageRank((Double, Double) -> Bool) // } // public enum LabelPositioning { // case auto // } // public enum ArrowStyle { // case none // case triangle // } public var id: EdgeID // public var label: String? // public var labelColor: Color // public var labelDisplayStrategy: LabelDisplayStrategy // public var labelPositioning: LabelPositioning // public var strokeColor: Color // public var strokeWidth: Double // public var strokeDashArray: [Double]? // public var arrowStyle: ArrowStyle @inlinable public init( from: NodeID, to: NodeID // label: String? = nil, // labelColor: Color = .gray, // labelDisplayStrategy: LabelDisplayStrategy = .auto, // labelPositioning: LabelPositioning = .auto, // strokeColor: Color = .gray.opacity(0.2), // strokeWidth: Double = 1.0, // strokeDashArray: [Double]? = nil, // arrowStyle: ArrowStyle = .none ) { self.id = .init(source: from, target: to) // self.label = label // self.labelColor = labelColor // self.labelDisplayStrategy = labelDisplayStrategy // self.labelPositioning = labelPositioning // self.strokeColor = strokeColor // self.strokeWidth = strokeWidth // self.strokeDashArray = strokeDashArray // self.arrowStyle = arrowStyle } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { let currentLinkShape = context.states.currentLinkShape context.linkOperations.append( .init( self, context.states.currentStroke, { currentLinkShape.path(from: $0.cgPoint, to: $1.cgPoint) } ) ) context.states.currentID = .link(id.source, id.target) } } extension LinkMark: CustomDebugStringConvertible { @inlinable public var debugDescription: String { return "LinkMark(\(id.source) -> \(id.target))" } } extension LinkMark: Equatable { @inlinable public static func == (lhs: Self, rhs: Self) -> Bool { return lhs.id == rhs.id } } ================================================ FILE: Sources/Grape/Contents/ModifiedGraphContent.swift ================================================ import SwiftUI public struct ModifiedGraphContent where C: GraphContent, M: GraphContentModifier { @usableFromInline let content: C @usableFromInline let modifier: M @inlinable public init( _ content: C, _ modifier: M ) { self.content = content self.modifier = modifier } } // public struct ModifiedGraphContent_Environment where C: GraphContent { // @usableFromInline // let content: C // @usableFromInline // let keyPath: WritableKeyPath<_Grape.Environment, T> // @usableFromInline // let value: T // @inlinable // init( // _ content: C, // _ keyPath: WritableKeyPath<_Grape.Environment, T>, // _ value: T // ) { // self.content = content // self.keyPath = keyPath // self.value = value // } // } extension ModifiedGraphContent: GraphContent { public typealias NodeID = C.NodeID @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { modifier._into(&context) content._attachToGraphRenderingContext(&context) modifier._exit(&context) } @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } } ================================================ FILE: Sources/Grape/Contents/NodeMark.swift ================================================ import SwiftUI import simd import Charts public struct NodeMark: GraphContent, Identifiable, Equatable { public var id: NodeID @inlinable public init( id: NodeID ) { self.id = id } @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { context.nodeOperations.append( .init( self, context.states.currentShading, context.states.currentStroke, context.states.currentSymbolShapeOrSize ) ) context.states.currentID = .node(id) context.nodeHitSizeAreaLookup[id] = simd_length_squared( context.states.currentSymbolSizeOrDefault.simd) } } public struct AnnotationNodeMark: GraphContent, Identifiable { public var id: NodeID @usableFromInline var radius: CGFloat @usableFromInline var annotation: AnyView @inlinable public init(id: NodeID, radius: CGFloat, @ViewBuilder annnotation: () -> some View) { self.id = id self.radius = radius self.annotation = AnyView(annnotation()) } @inlinable public var body: some GraphContent { NodeMark(id: id) .symbolSize(radius: radius) .foregroundStyle(.clear) .annotation("\(id)", alignment: .center, offset: .zero) { annotation } } } ================================================ FILE: Sources/Grape/Contents/Series.swift ================================================ public struct Series where Data: RandomAccessCollection, Content: GraphContent, NodeID: Hashable { @usableFromInline let data: Data @usableFromInline let content: (Data.Element) -> Content @inlinable public init( _ data: Data, @GraphContentBuilder graphContent: @escaping (Data.Element) -> Content ) { self.data = data self.content = graphContent } @inlinable public var body: _IdentifiableNever { fatalError() } } extension Series: GraphContent { @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { self.data.forEach { element in self.content(element)._attachToGraphRenderingContext(&context) } } } ================================================ FILE: Sources/Grape/Contents/_ArrayGraphContent.swift ================================================ @usableFromInline struct _ArrayGraphContent: GraphContent where C: GraphContent { public typealias NodeID = C.NodeID @usableFromInline let storage: [C] @inlinable public init( _ storage: [C] ) { self.storage = storage } @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { for content in storage { content._attachToGraphRenderingContext(&context) } } } ================================================ FILE: Sources/Grape/Contents/_ConditionalGraphContent.swift ================================================ public struct _ConditionalGraphContent: GraphContent where C1: GraphContent, C2: GraphContent, C1.NodeID == C2.NodeID { public typealias NodeID = C1.NodeID public enum Storage { case trueContent(C1) case falseContent(C2) } @usableFromInline let storage: Storage @inlinable public init( _ storage: Storage ) { self.storage = storage } @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { switch storage { case .trueContent(let content): content._attachToGraphRenderingContext(&context) case .falseContent(let content): content._attachToGraphRenderingContext(&context) } } } ================================================ FILE: Sources/Grape/Contents/_EmptyGraphContent.swift ================================================ @usableFromInline struct _EmptyGraphContent: GraphContent { @inlinable public init() { } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { } @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } } ================================================ FILE: Sources/Grape/Contents/_IdentifiableNever.swift ================================================ public enum _IdentifiableNever { @usableFromInline internal init() { fatalError() } } @inlinable public func _fatalError(of identityType: ID) -> _IdentifiableNever where ID: Hashable { _IdentifiableNever() } extension _IdentifiableNever: GraphContent { public typealias NodeID = ID @inlinable public var body: Self { _IdentifiableNever() } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { fatalError() } } extension _IdentifiableNever: Identifiable { @inlinable public var id: ID { fatalError() } } ================================================ FILE: Sources/Grape/Contents/_OptionalGraphContent.swift ================================================ @usableFromInline struct _OptionalGraphContent: GraphContent where C: GraphContent { public typealias NodeID = C.NodeID @usableFromInline let storage: C? @inlinable public init( _ storage: C? ) { self.storage = storage } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { switch storage { case .none: break case .some(let content): content._attachToGraphRenderingContext(&context) } } @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } } ================================================ FILE: Sources/Grape/Contents/_PairedGraphContent.swift ================================================ /// TODO: switch to Generic packs when same type requirements are supported @usableFromInline struct _PairedGraphContent: GraphContent where C1: GraphContent, C2: GraphContent, NodeID: Hashable, C1.NodeID == NodeID, C2.NodeID == NodeID { @usableFromInline let first: C1 @usableFromInline let second: C2 @inlinable public init(_ first: C1, _ second: C2) { self.first = first self.second = second } @inlinable public func _attachToGraphRenderingContext(_ context: inout _GraphRenderingContext) { first._attachToGraphRenderingContext(&context) second._attachToGraphRenderingContext(&context) } @inlinable public var body: _IdentifiableNever { _IdentifiableNever<_>() } } ================================================ FILE: Sources/Grape/Descriptors/ForceDescriptor.swift ================================================ import ForceSimulation import simd public enum NodeAttribute { case varied((NodeID) -> Attribute) case constant(Attribute) } extension NodeAttribute: ExpressibleByFloatLiteral where Attribute == Double { @inlinable public init(floatLiteral value: Double) { self = .constant(value) } } extension NodeAttribute { @inlinable func makeCompactRepresentation(nodeIDs: [NodeID]) -> ForceSimulation.AttributeDescriptor { switch self { case .constant(let value): return .constant(value) case .varied(let f): return .varied { node in f(nodeIDs[node]) } } } } public protocol _ForceDescriptor { associatedtype NodeID: Hashable func _makeForceField(forceField: inout SealedForce2D, nodeIDs: [NodeID]) func _makeDescriptor(descriptor: inout SealedForceDescriptor) } public struct SealedForceDescriptor: _ForceDescriptor { public func _makeDescriptor(descriptor: inout SealedForceDescriptor) { for entry in storage { descriptor.storage.append(entry) } } @usableFromInline enum Entry { case center(CenterForce) case link(LinkForce) case manyBody(ManyBodyForce) case position(PositionForce) case collide(CollideForce) case radial(RadialForce) } @usableFromInline var storage: [Entry] @inlinable public func _makeForceField(forceField: inout ForceSimulation.SealedForce2D, nodeIDs: [NodeID]) { storage.forEach { switch $0 { case .center(let descriptor): descriptor._makeForceField(forceField: &forceField, nodeIDs: nodeIDs) case .link(let descriptor): descriptor._makeForceField(forceField: &forceField, nodeIDs: nodeIDs) case .manyBody(let descriptor): descriptor._makeForceField(forceField: &forceField, nodeIDs: nodeIDs) case .position(let descriptor): descriptor._makeForceField(forceField: &forceField, nodeIDs: nodeIDs) case .collide(let descriptor): descriptor._makeForceField(forceField: &forceField, nodeIDs: nodeIDs) case .radial(let descriptor): descriptor._makeForceField(forceField: &forceField, nodeIDs: nodeIDs) } } } @inlinable @discardableResult public static func center(x: Double = 0.0, y: Double = 0.0, strength: Double = 0.5) -> Self { SealedForceDescriptor([.center(CenterForce(x: x, y: y, strength: strength))]) } @inlinable @discardableResult public static func manyBody(strength: Double = -30.0, mass: ManyBodyForce.NodeMass = .constant(1.0), theta: Double = 0.9) -> Self { SealedForceDescriptor([.manyBody(ManyBodyForce(strength: strength, mass: mass, theta: theta))]) } @inlinable @discardableResult public static func link( originalLength: LinkForce.LinkLength = .constant(30.0), stiffness: LinkForce.Stiffness = .weightedByDegree { _, _ in 1.0 }, iterationsPerTick: UInt = 1 ) -> Self { SealedForceDescriptor([.link(LinkForce(originalLength: originalLength, stiffness: stiffness, iterationsPerTick: iterationsPerTick))]) } @inlinable @discardableResult public static func collide( strength: Double = 0.5, radius: CollideForce.CollideRadius = .constant(3.0), iterationsPerTick: UInt = 1 ) -> Self { SealedForceDescriptor([.collide(CollideForce(strength: strength, radius: radius, iterationsPerTick: iterationsPerTick))]) } @inlinable @discardableResult public static func position( direction: Kinetics2D.DirectionOfPositionForce, targetOnDirection: PositionForce.TargetOnDirection, strength: PositionForce.PositionStrength = .constant(1.0) ) -> Self { SealedForceDescriptor([.position(PositionForce(direction: direction, targetOnDirection: targetOnDirection, strength: strength))]) } @inlinable @discardableResult public static func radial( center: SIMD2 = .zero, strength: RadialForce.RadialStrength = .constant(1.0), radius: RadialForce.Radius = .constant(3.0) ) -> Self { SealedForceDescriptor([.radial(RadialForce(center: center, strength: strength, radius: radius))]) } @inlinable @discardableResult public consuming func center(x: Double = 0.0, y: Double = 0.0, strength: Double = 0.5) -> Self { storage.append(.center(CenterForce(x: x, y: y, strength: strength))) return self } @inlinable @discardableResult public consuming func manyBody(strength: Double = -30.0, mass: ManyBodyForce.NodeMass = .constant(1.0), theta: Double = 0.9) -> Self { storage.append(.manyBody(ManyBodyForce(strength: strength, mass: mass, theta: theta))) return self } @inlinable @discardableResult public consuming func link( originalLength: LinkForce.LinkLength = .constant(30.0), stiffness: LinkForce.Stiffness = .weightedByDegree { _, _ in 1.0 }, iterationsPerTick: UInt = 1 ) -> Self{ storage.append(.link(LinkForce(originalLength: originalLength, stiffness: stiffness, iterationsPerTick: iterationsPerTick))) return self } @inlinable @discardableResult public consuming func collide( strength: Double = 0.5, radius: CollideForce.CollideRadius = .constant(3.0), iterationsPerTick: UInt = 1 ) -> Self { storage.append(.collide(CollideForce(strength: strength, radius: radius, iterationsPerTick: iterationsPerTick))) return self } @inlinable @discardableResult public consuming func position( direction: Kinetics2D.DirectionOfPositionForce, targetOnDirection: PositionForce.TargetOnDirection, strength: PositionForce.PositionStrength = .constant(1.0) ) -> Self{ storage.append(.position(PositionForce(direction: direction, targetOnDirection: targetOnDirection, strength: strength))) return self } @inlinable @discardableResult public consuming func radial( center: SIMD2 = .zero, strength: RadialForce.RadialStrength = .constant(1.0), radius: RadialForce.Radius = .constant(3.0) ) -> Self{ storage.append(.radial(RadialForce(center: center, strength: strength, radius: radius))) return self } @inlinable init(_ storage: [Entry] = []) { self.storage = storage } } @resultBuilder public struct SealedForceDescriptorBuilder { public static func buildPartialBlock>(first: FD) -> SealedForceDescriptor { var descriptor = SealedForceDescriptor() first._makeDescriptor(descriptor: &descriptor) return descriptor } public static func buildPartialBlock( accumulated: some _ForceDescriptor, next: some _ForceDescriptor ) -> SealedForceDescriptor { var descriptor = SealedForceDescriptor() accumulated._makeDescriptor(descriptor: &descriptor) next._makeDescriptor(descriptor: &descriptor) return descriptor } } public struct CenterForce: _ForceDescriptor { public var x: Double public var y: Double public var strength: Double @inlinable public init( x: Double = 0.0, y: Double = 0.0, strength: Double = 0.5 ) { self.x = x self.y = y self.strength = strength } @inlinable public func _makeForceField(forceField: inout ForceSimulation.SealedForce2D, nodeIDs: [NodeID]) { let force = Kinetics2D.CenterForce(center: [x, y], strength: strength) forceField.entries.append(.center(force)) } @inlinable public func _makeDescriptor(descriptor: inout SealedForceDescriptor) { descriptor.storage.append(.center(self)) } } public struct ManyBodyForce: _ForceDescriptor { public typealias NodeMass = NodeAttribute public var strength: Double public var mass: NodeMass public var theta: Double @inlinable public init( strength: Double = -30.0, mass: NodeMass = .constant(1.0), theta: Double = 0.9 ) { self.strength = strength self.mass = mass self.theta = theta } @inlinable public func _makeForceField(forceField: inout ForceSimulation.SealedForce2D, nodeIDs: [NodeID]) { let compactMass: Kinetics2D.NodeMass = mass.makeCompactRepresentation(nodeIDs: nodeIDs) let force = Kinetics2D.ManyBodyForce( strength: strength, nodeMass: compactMass, theta: theta ) forceField.entries.append(.manyBody(force)) } @inlinable public func _makeDescriptor(descriptor: inout SealedForceDescriptor) { descriptor.storage.append(.manyBody(self)) } } public struct LinkForce: _ForceDescriptor { public enum Stiffness: ExpressibleByFloatLiteral { case constant(Double) case weightedByDegree((EdgeID, LinkLookup) -> Double) @inlinable public init(floatLiteral value: Double) { self = .weightedByDegree({ _, _ in value }) } } public enum LinkLength: ExpressibleByFloatLiteral { case constant(Double) case varied((EdgeID, LinkLookup) -> Double) @inlinable public init(floatLiteral value: Double) { self = .constant(value) } } public var stiffness: Stiffness public var originalLength: LinkLength public var iterationsPerTick: UInt @usableFromInline var links: [EdgeID] @inlinable public init( originalLength: LinkLength = .constant(30.0), stiffness: Stiffness = .weightedByDegree { _, _ in 1.0 }, iterationsPerTick: UInt = 1 ) { self.stiffness = stiffness self.originalLength = originalLength self.iterationsPerTick = iterationsPerTick self.links = [] } @inlinable public func _makeForceField(forceField: inout ForceSimulation.SealedForce2D, nodeIDs: [NodeID]) { let mappedLookup = LinkLookup(links: links) let compactStiffness: Kinetics2D.LinkStiffness = switch stiffness { case .constant(let value): .constant(value) case .weightedByDegree(let f): .weightedByDegree { edge, _ in let mappedEdge = EdgeID( source: nodeIDs[edge.source], target: nodeIDs[edge.target] ) return f(mappedEdge, mappedLookup) } } let compactLength: Kinetics2D.LinkLength = switch originalLength { case .constant(let value): .constant(value) case .varied(let f): .varied { edge, _ in let mappedEdge = EdgeID( source: nodeIDs[edge.source], target: nodeIDs[edge.target] ) return f(mappedEdge, mappedLookup) } } let force = Kinetics2D.LinkForce( stiffness: compactStiffness, originalLength: compactLength, iterationsPerTick: iterationsPerTick ) forceField.entries.append(.link(force)) } @inlinable public func _makeDescriptor(descriptor: inout SealedForceDescriptor) { descriptor.storage.append(.link(self)) } } public struct CollideForce: _ForceDescriptor { public typealias CollideRadius = NodeAttribute public var strength: Double public var radius: CollideRadius = .constant(3.0) public var iterationsPerTick: UInt = 1 @inlinable public init( strength: Double = 0.5, radius: CollideRadius = .constant(3.0), iterationsPerTick: UInt = 1 ) { self.strength = strength self.radius = radius self.iterationsPerTick = iterationsPerTick } @inlinable public func _makeForceField(forceField: inout ForceSimulation.SealedForce2D, nodeIDs: [NodeID]) { let compactRadius: Kinetics2D.CollideRadius = radius.makeCompactRepresentation(nodeIDs: nodeIDs) let force = Kinetics2D.CollideForce( radius: compactRadius, strength: strength, iterationsPerTick: iterationsPerTick ) forceField.entries.append(.collide(force)) } @inlinable public func _makeDescriptor(descriptor: inout SealedForceDescriptor) { descriptor.storage.append(.collide(self)) } } public struct PositionForce: _ForceDescriptor { public typealias PositionStrength = NodeAttribute public typealias TargetOnDirection = NodeAttribute public typealias DirectionOfPositionForce = Kinetics2D.DirectionOfPositionForce public var strength: PositionStrength public var targetOnDirection: TargetOnDirection public var direction: Kinetics2D.DirectionOfPositionForce @inlinable public init( direction: Kinetics2D.DirectionOfPositionForce, targetOnDirection: TargetOnDirection, strength: PositionStrength = .constant(1.0) ) { self.strength = strength self.direction = direction self.targetOnDirection = targetOnDirection } @inlinable public func _makeForceField(forceField: inout ForceSimulation.SealedForce2D, nodeIDs: [NodeID]) { let compactStrength: Kinetics2D.PositionStrength = strength.makeCompactRepresentation(nodeIDs: nodeIDs) let compactTargetOnDirection: Kinetics2D.TargetOnDirection = targetOnDirection.makeCompactRepresentation(nodeIDs: nodeIDs) let force = Kinetics2D.PositionForce( direction: direction, targetOnDirection: compactTargetOnDirection, strength: compactStrength ) forceField.entries.append(.position(force)) } @inlinable public func _makeDescriptor(descriptor: inout SealedForceDescriptor) { descriptor.storage.append(.position(self)) } } public struct RadialForce: _ForceDescriptor { public typealias Radius = NodeAttribute public typealias RadialStrength = NodeAttribute public var strength: RadialStrength public var radius: Radius = .constant(3.0) public var center: SIMD2 = .zero public var iterationsPerTick: UInt = 1 @inlinable public init( center: SIMD2 = .zero, strength: RadialStrength = .constant(1.0), radius: Radius = .constant(3.0) ) { self.center = center self.strength = strength self.radius = radius } @inlinable public func _makeForceField(forceField: inout ForceSimulation.SealedForce2D, nodeIDs: [NodeID]) { let compactRadius: Kinetics2D.CollideRadius = radius.makeCompactRepresentation(nodeIDs: nodeIDs) let compactStrength: Kinetics2D.PositionStrength = strength.makeCompactRepresentation(nodeIDs: nodeIDs) let force = Kinetics2D.RadialForce( center: center, radius: compactRadius, strength: compactStrength ) forceField.entries.append(.radial(force)) } @inlinable public func _makeDescriptor(descriptor: inout SealedForceDescriptor) { descriptor.storage.append(.radial(self)) } } ================================================ FILE: Sources/Grape/Gestures/GraphDragGesture.swift ================================================ import ForceSimulation import SwiftUI public enum GraphDragState { case node(NodeID) case background(SIMD2) } #if !os(tvOS) @usableFromInline struct GraphDragModifier: ViewModifier { @inlinable public var dragGesture: some Gesture { DragGesture( minimumDistance: Self.minimumDragDistance, coordinateSpace: .local ) .onChanged(onChanged) .onEnded(onEnded) } @inlinable public func body(content: Content) -> some View { content.gesture(dragGesture) } @inlinable @State public var dragState: GraphDragState? @usableFromInline let graphProxy: GraphProxy @usableFromInline let action: ((GraphDragState?) -> Void)? @inlinable init( graphProxy: GraphProxy, action: ((GraphDragState?) -> Void)? = nil ) { self.graphProxy = graphProxy self.action = action } @inlinable static var minimumDragDistance: CGFloat { 3.0 } @inlinable static var minimumAlphaAfterDrag: CGFloat { 0.5 } @inlinable public func onEnded( value: DragGesture.Value ) { if dragState != nil { switch dragState { case .node(let nodeID): graphProxy.setNodeFixation(nodeID: nodeID, fixation: nil) case .background(let start): let delta = value.location.simd - start graphProxy.modelTransform.translate += delta dragState = .background(value.location.simd) case .none: break } dragState = .none } if let action { action(dragState) } } @inlinable public func onChanged( value: DragGesture.Value ) { if dragState == nil { if let nodeID = graphProxy.node(of: NodeID.self, at: value.startLocation) { dragState = .node(nodeID) graphProxy.setNodeFixation(nodeID: nodeID, fixation: value.startLocation) } else { dragState = .background(value.location.simd) } } else { switch dragState { case .node(let nodeID): graphProxy.setNodeFixation(nodeID: nodeID, fixation: value.location) case .background(let start): let delta = value.location.simd - start graphProxy.modelTransform.translate += delta dragState = .background(value.location.simd) case .none: break } } if let action { action(dragState) } } } extension View { /// Attach a drag gesture to an overlay or a background view created with ``SwiftUICore/View/graphOverlay(alignment:content:)``. /// - Parameters: /// - proxy: The graph proxy that provides the graph context. /// - type: The type of the node ID. The drag gesture will look for the node ID of this type. /// - action: The action to perform when the drag gesture changes. @inlinable public func withGraphDragGesture( _ proxy: GraphProxy, of type: NodeID.Type, action: ((GraphDragState?) -> Void)? = nil ) -> some View { self.modifier(GraphDragModifier(graphProxy: proxy, action: action)) } } #endif ================================================ FILE: Sources/Grape/Gestures/GraphMagnifyGesture.swift ================================================ import SwiftUI #if os(iOS) || os(macOS) public struct GraphMagnifyModifier: ViewModifier { @usableFromInline let proxy: GraphProxy @usableFromInline let action: (() -> Void)? @inlinable init(_ proxy: GraphProxy, action: (() -> Void)? = nil) { self.proxy = proxy self.action = action } @inlinable public func body(content: Content) -> some View { content.gesture(gesture) } @inlinable public var gesture: some Gesture { MagnifyGesture(minimumScaleDelta: Self.minimumScaleDelta) .onChanged(onMagnifyChange) .onEnded(onMagnifyEnd) } @inlinable static var minimumScaleDelta: CGFloat { 0.001 } @inlinable static var minimumScale: CGFloat { 1e-2 } @inlinable static var maximumScale: CGFloat { .infinity } @inlinable static var magnificationDecay: CGFloat { 0.1 } @inlinable internal func clamp( _ value: CGFloat, min: CGFloat, max: CGFloat ) -> CGFloat { Swift.min(Swift.max(value, min), max) } @inlinable internal func onMagnifyChange( _ value: MagnifyGesture.Value ) { var startTransform: ViewportTransform if let t = self.proxy.lastTransformRecord { startTransform = t } else { self.proxy.lastTransformRecord = self.proxy.modelTransform startTransform = self.proxy.modelTransform } let alpha = (startTransform.translate(by: self.proxy.obsoleteState.cgSize.simd / 2)) .invert(value.startLocation.simd) let newScale = clamp( value.magnification * startTransform.scale, min: Self.minimumScale, max: Self.maximumScale) let newTranslate = (startTransform.scale - newScale) * alpha + startTransform.translate let newModelTransform = ViewportTransform( translate: newTranslate, scale: newScale ) self.proxy.modelTransform = newModelTransform if let action { action() } } @inlinable internal func onMagnifyEnd( _ value: MagnifyGesture.Value ) { var startTransform: ViewportTransform if let t = self.proxy.lastTransformRecord { startTransform = t } else { self.proxy.lastTransformRecord = self.proxy.modelTransform startTransform = self.proxy.modelTransform } let alpha = (startTransform.translate(by: self.proxy.obsoleteState.cgSize.simd / 2)) .invert(value.startLocation.simd) let newScale = clamp( value.magnification * startTransform.scale, min: Self.minimumScale, max: Self.maximumScale) let newTranslate = (startTransform.scale - newScale) * alpha + startTransform.translate let newModelTransform = ViewportTransform( translate: newTranslate, scale: newScale ) self.proxy.lastTransformRecord = nil self.proxy.modelTransform = newModelTransform if let action { action() } } } extension View { @inlinable public func withGraphMagnifyGesture( _ proxy: GraphProxy, action: (() -> Void)? = nil ) -> some View { self.modifier(GraphMagnifyModifier(proxy, action: action)) } } #endif ================================================ FILE: Sources/Grape/Gestures/GraphTapGesture.swift ================================================ import SwiftUI extension View { @inlinable @available(tvOS, unavailable) public func withGraphTapGesture( _ proxy: GraphProxy, of type: NodeID.Type, action: @escaping (NodeID) -> Void ) -> some View { self.onTapGesture { value in if let nodeID = proxy.node(of: type, at: value) { action(nodeID) } } } } ================================================ FILE: Sources/Grape/Grape.docc/CreatingAForceDirectedGraph.md ================================================ # Creating a Force Directed Graph ## Describe a graph A graph is a collection of nodes and links. Each node is connected to other nodes by links. In Grape, you describe a node with a `NodeMark` and a link with a `LinkMark`. `NodeMark` and `LinkMark` are associated with an `id` or `id`s that identifies them. An `id` can be any type that conforms to `Hashable`. Grape provides a `ForceDirectedGraph` view to visualize a graph. You can easily initialize it like you would do in SwiftUI. ```swift struct MyGraph: View { var body: some View { ForceDirectedGraph { NodeMark(id: "A") NodeMark(id: "B") LinkMark(from: "A", to: "B") } } } ``` For the array data, `Series` comes handy for describing a collection of nodes and links. Consider it a simplified version of `ForEach` in SwiftUI. @Row { @Column { ```swift struct MyGraph: View { let myNodes = ["A", "B", "C"] let myLinks = [("A", "B"), ("B", "C")] var body: some View { ForceDirectedGraph { Series(myNodes) { id in NodeMark(id: id) } Series(myLinks) { from, to in LinkMark(from: from, to: to) } } } } ``` } @Column { @Image(source: "BasicExample.png", alt: "Rendered example of a small graph") { Rendered example of a small graph. } } } > Grape currently does not protect you from linking to non-existing nodes. If you link to a node that does not exist, view crashes. ## Customize forces You can customize the forces that interfere with the nodes and links. By default, Grape uses a `LinkForce` and a `ManyBodyForce`. For example, the `CenterForce` can keep the mass center of the graph at the center of the view, so it does not drift away. To add a `CenterForce`, you can do the following. ```swift struct MyGraph: View { let myNodes = ["A", "B", "C"] let myLinks = [("A", "B"), ("B", "C")] var body: some View { ForceDirectedGraph { Series(myNodes) { id in NodeMark(id: id) } Series(myLinks) { from, to in LinkMark(from: from, to: to) } } force: { .manyBody() .link() .center() } } } ``` Note that when you override the default forces, you may need to add the `LinkForce` and `ManyBodyForce` back. Otherwise, the nodes may stay static since no forces are moving them to other places. ## Decorate marks Add modifiers like you would do in SwiftUI to style your nodes and links. ```swift struct MyGraph: View { let myNodes = ["A", "B", "C"] let myLinks = [("A", "B"), ("B", "C")] var body: some View { ForceDirectedGraph { Series(myNodes) { id in NodeMark(id: id) .foregroundStyle(.blue) } Series(myLinks) { from, to in LinkMark(from: from, to: to) } } } } ``` ## Respond to interactions and events Grape provides a set of interactions and events to help you respond to user interactions, including dragging, zooming, and tapping. They are mostly supported by default, and you can install your callbacks to respond to them. For detailed usages, please refer to [MermaidVisualization.swift](https://github.com/swiftgraphs/Grape/blob/main/Examples/ForceDirectedGraphExample/ForceDirectedGraphExample/MermaidVisualization.swift). @Video(source: "https://github.com/swiftgraphs/Grape/assets/45376537/80d933c1-8b5b-4b1a-9062-9628577bd2e0", alt: "A screen record of mermaid") // TODO: Add examples ================================================ FILE: Sources/Grape/Grape.docc/Documentation.md ================================================ # ``Grape`` Construct and visualize graphs on Apple platforms. ## Overview The Grape framework enables you to create a graph visualization in SwiftUI. With Grape, you can build effective and customizable force-directed graphs with minimal code. This framework provides nodes, links and forces as building blocks for constructing graphs. @Image(source: "GrapeOverview.png", alt: "A force-directed graph visualization of a small graph.") Grape supports localization features. You can localize the labels in the graph visualization by providing a localized string key. > If you’re looking for a more detailed control of force-directed layouts, please refer to [ForceSimulation | Documentation](https://swiftgraphs.github.io/Grape/ForceSimulation/documentation/forcesimulation/). ## Topics ### Creating a graph visualization * * * ``ForceDirectedGraph`` ### Describing a graph * ``GraphContent`` * ``GraphContentBuilder`` * ``NodeMark`` * ``LinkMark`` * ``Series`` * ``GraphComponent`` ### Adding interactivity * ``GraphProxy`` * ``SwiftUICore/View/graphOverlay(alignment:content:)`` * ``SwiftUICore/View/graphBackground(alignment:content:)`` * ``SwiftUICore/View/withGraphTapGesture(_:action:)`` * ``SwiftUICore/View/withGraphDragGesture(_:action:)`` * ``SwiftUICore/View/withGraphMagnifyGesture(_:action:)`` ### Managing the view state * ``ForceDirectedGraphModel`` * ``KeyFrame`` * ``KineticState`` * ``TransformProtocol`` * ``ViewportTransform`` ### Describing forces * ``CenterForce`` * ``CollideForce`` * ``LinkForce`` * ``ManyBodyForce`` * ``PositionForce`` * ``RadialForce`` * ``SealedForceDescriptor`` * ``SealedForceDescriptorBuilder`` ### Decorating marks * ``GraphContentModifier`` * ``ModifiedGraphContent`` * ``AnyGraphContentModifier`` * ``StrokeColor`` * ``LinkShape`` * ``StraightLineLinkShape`` * ``PlainLineLink`` * ``ArrowLineLink`` ================================================ FILE: Sources/Grape/Grape.docc/StateManagementAndEliminatingRedundantRerenders.md ================================================ # State Management and Eliminating Redundant Rerenders ## Control the state You can control the view state like this: ```swift import Grape struct MyStatefulGraph: View { // States including running status, transformation, etc. // Gives you a handle to control the states. @State var graphStates = ForceDirectedGraphState() var body: some View { ForceDirectedGraph(states: graphStates) { // ... } force: { // ... } } } ``` `ForceDirectedGraphState` utilizes the `Observation` framework so all you need to change the state is to mutate its properties: ```swift graphStates.isRunning.toggle() graphStates.transform = .identity // reset transform to identity ``` ## Eliminate redundant rerenders One trick to eliminate redundant rerenders is to not referencing any observed properties in the `body` of the `View`. Instead, try to reference the entire `Observable` object. This way, the `body` will not re-evaluate when the observed properties change. ```swift import Grape struct MyStatefulGraph: View { // States including running status, transformation, etc. // Gives you a handle to control the states. @State var graphStates = ForceDirectedGraphState() var body: some View { HStack { ForceDirectedGraph(states: graphStates) { // ... } force: { // ... } GraphStateToggle(graphStates: graphStates) // seperate views so we can reference the entire graphStates } } } struct GraphStateToggle: View { @Bindable var graphStates: ForceDirectedGraphState var body: some View { Button { graphStates.isRunning.toggle() } label: { // ... } } } ``` Although this introduces boilerplates, `Grape` do benefit from this pattern since its re-evaluation is expensive (especially with large graphs or heavy rich text labels). > This might not always work for other `Observation` based state management. ================================================ FILE: Sources/Grape/Grape.docc/theme-settings.json ================================================ { "$schema": "https://raw.githubusercontent.com/apple/swift-docc/main/Sources/SwiftDocC/SwiftDocC.docc/Resources/ThemeSettings.spec.json", "theme": { "typography": { "html-font": "system-ui, -apple-system, \"InterVar\"", "html-font-mono": "ui-monospace, \"JetBrains Mono\", \"IBM Plex Mono\", monospace" } } } ================================================ FILE: Sources/Grape/Modifiers/AnyGraphContentModifier.swift ================================================ public struct AnyGraphContentModifier: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { storage._into(&context) } @inlinable public func _exit( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { storage._exit(&context) } @usableFromInline let storage: any GraphContentModifier @inlinable public init(erasing: T) { self.storage = erasing } @inlinable public static func == (lhs: AnyGraphContentModifier, rhs: AnyGraphContentModifier) -> Bool { return false } } ================================================ FILE: Sources/Grape/Modifiers/Effects/GrapeEffect.ForegroundStyle.swift ================================================ import SwiftUI extension GraphContentEffect { @usableFromInline internal struct ForegroundStyle where S: ShapeStyle { @usableFromInline let style: S @inlinable public init(_ style: S) { self.style = style } } @usableFromInline internal struct Shading { @usableFromInline let shading: GraphicsContext.Shading @inlinable public init(_ shading: GraphicsContext.Shading) { self.shading = shading } } @usableFromInline internal struct ShadingBy { @usableFromInline let value: AnyHashable @inlinable public init(by value: T) { self.value = value } } } extension GraphContentEffect.ForegroundStyle: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { let shading: GraphicsContext.Shading = .style(style) context.states.shading.append(shading) // context.operations.append(.updateShading(shading)) } @inlinable public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { context.states.shading.removeLast() // context.operations.append( // .updateShading(context.states.currentShading) // ) } } extension GraphContentEffect.Shading: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { context.states.shading.append(shading) } @inlinable public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { context.states.shading.removeLast() } } ================================================ FILE: Sources/Grape/Modifiers/Effects/GrapeEffect.Label.swift ================================================ import SwiftUI extension GraphContentEffect { @usableFromInline internal struct TextAnnotation { @usableFromInline let text: Text? @usableFromInline let alignment: Alignment @usableFromInline let offset: CGVector @inlinable public init( _ text: Text?, alignment: Alignment = .bottom, offset: CGVector = .zero ) { self.text = text self.alignment = alignment self.offset = offset } } @usableFromInline internal struct ViewAnnotation { @usableFromInline let view: AnyView @usableFromInline let tag: String @usableFromInline let alignment: Alignment @usableFromInline let offset: CGVector @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.TextAnnotation: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { } @inlinable public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { guard let text = text else { return } if let currentID = context.states.currentID { let resolvedText = text.resolved() context.resolvedTexts[currentID] = resolvedText context.symbols[resolvedText] = .pending(text) 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) } } } } extension GraphContentEffect.ViewAnnotation: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { } @inlinable 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) } } } } extension Alignment { @inlinable internal func anchorOffset(for size: CGSize) -> SIMD2 { // vertical text ? switch vertical { case .top: return SIMD2(0, -Double(size.height) / 2) case .center: switch horizontal { case .leading: return SIMD2(Double(size.width) / 2, 0) case .trailing: return SIMD2(-Double(size.width) / 2, 0) default: return .zero } case .bottom: return SIMD2(0, Double(size.height) / 2) default: return .zero } } @inlinable internal func textImageOffsetInCGContext(width: Double, height: Double) -> SIMD2 { let dx: Double = switch horizontal { case .center: -width / 2 case .trailing: -width case .leading: 0 default: 0 } let dy: Double = switch vertical { case .center: height / 2 case .bottom: height case .top: 0 default: 0 } return SIMD2(dx, dy) } } ================================================ FILE: Sources/Grape/Modifiers/Effects/GrapeEffect.Opacity.swift ================================================ extension GraphContentEffect { @usableFromInline internal struct Opacity { @usableFromInline let value: Double @inlinable public init(_ value: Double) { self.value = value } } } extension GraphContentEffect.Opacity: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { context.states.opacity.append(value) } @inlinable public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { context.states.opacity.removeLast() } } ================================================ FILE: Sources/Grape/Modifiers/Effects/GrapeEffect.Stroke.swift ================================================ import SwiftUI public enum StrokeColor: Equatable, Hashable { case clip case color(Color) } extension GraphContentEffect { @usableFromInline internal struct Stroke: Equatable, Hashable { // @usableFromInline // let shading: GraphicsContext.Shading @usableFromInline let color: StrokeColor @usableFromInline let style: StrokeStyle? @inlinable public init( // _ shading: GraphicsContext.Shading, _ color: StrokeColor = .clip, _ style: StrokeStyle? = nil ) { self.color = color self.style = style } @inlinable public func hash(into hasher: inout Hasher) { hasher.combine(color) if let style { hasher.combine(style.lineWidth) hasher.combine(style.lineCap) hasher.combine(style.lineJoin) hasher.combine(style.miterLimit) hasher.combine(style.dash) hasher.combine(style.dashPhase) } } } } extension GraphContentEffect.Stroke: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { context.states.stroke.append(self) } @inlinable public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { context.states.stroke.removeLast() } } ================================================ FILE: Sources/Grape/Modifiers/Effects/GrapeEffect.Symbol.swift ================================================ import SwiftUI extension GraphContentEffect { @usableFromInline internal struct Symbol { @usableFromInline let shape: AnyShape @inlinable public init(_ shape: S) where S: SwiftUI.Shape { self.shape = .init(shape) } } } extension GraphContentEffect.Symbol: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { let currentSize = context.states.currentSymbolSizeOrDefault context.states.symbolShape.append( shape.path( in: CGRect( origin: CGPoint(x: -currentSize.width / 2, y: -currentSize.height / 2), size: currentSize ) ) ) } @inlinable public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { context.states.symbolShape.removeLast() } } ================================================ FILE: Sources/Grape/Modifiers/Effects/GrapeEffect.SymbolSize.swift ================================================ import SwiftUI extension GraphContentEffect { @usableFromInline internal struct SymbolSize { @usableFromInline let size: CGSize @inlinable public init(_ size: CGSize) { self.size = size } } } extension GraphContentEffect.SymbolSize: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { context.states.symbolSize.append(size) } @inlinable public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { context.states.symbolSize.removeLast() } } ================================================ FILE: Sources/Grape/Modifiers/Effects/GrapeEffect._LinkShape.swift ================================================ import SwiftUI extension GraphContentEffect { @usableFromInline internal struct _LinkShape { @usableFromInline let storage: any LinkShape @inlinable public init(_ path: some LinkShape) { self.storage = path } } } extension GraphContentEffect._LinkShape: GraphContentModifier { @inlinable public func _into( _ context: inout _GraphRenderingContext ) where NodeID: Hashable { context.states.linkShape.append(storage) } @inlinable public func _exit(_ context: inout _GraphRenderingContext) where NodeID: Hashable { context.states.linkShape.removeLast() } } ================================================ FILE: Sources/Grape/Modifiers/Effects/GrapeEffect.swift ================================================ @usableFromInline enum GraphContentEffect {} ================================================ FILE: Sources/Grape/Modifiers/GraphContent+GraphContentModifiers.swift ================================================ import SwiftUI #if canImport(Charts) import Charts #endif extension GraphContent { @inlinable @_disfavoredOverload public func foregroundStyle(_ style: S) -> some GraphContent where S: SwiftUI.ShapeStyle { return ModifiedGraphContent(self, GraphContentEffect.Shading(.style(style))) } @inlinable public func foregroundStyle(_ color: Color) -> some GraphContent { return ModifiedGraphContent(self, GraphContentEffect.Shading(.color(color))) } @inlinable @_disfavoredOverload public func symbol(_ shape: S) -> some GraphContent where S: SwiftUI.Shape { return ModifiedGraphContent(self, GraphContentEffect.Symbol(shape)) } #if canImport(Charts) @inlinable public func symbol(_ shape: BasicChartSymbolShape) -> some GraphContent { return ModifiedGraphContent(self, GraphContentEffect.Symbol(shape)) } #endif @inlinable public func symbolSize(_ size: CGSize) -> some GraphContent { return ModifiedGraphContent(self, GraphContentEffect.SymbolSize(size)) } @inlinable public func symbolSize(_ size: SIMD2) -> some GraphContent { return ModifiedGraphContent(self, GraphContentEffect.SymbolSize(size.cgSize)) } @inlinable public func symbolSize(radius: CGFloat) -> some GraphContent { return ModifiedGraphContent( self, GraphContentEffect.SymbolSize( CGSize(width: radius * 2, height: radius * 2) )) } @inlinable public func annotation( _ text: Text?, alignment: Alignment = .bottom, offset: CGVector = .zero ) -> some GraphContent { return ModifiedGraphContent( self, GraphContentEffect.TextAnnotation(text, alignment: alignment, offset: offset)) } @inlinable public func annotation( _ text: Text?, alignment: Alignment = .bottom, offset: SIMD2 = .zero ) -> some GraphContent { return annotation(text, alignment: alignment, offset: offset.cgVector) } @inlinable public func annotation( _ string: String?, alignment: Alignment = .bottom, offset: CGVector = .zero ) -> some GraphContent { return ModifiedGraphContent( self, GraphContentEffect.TextAnnotation(nil, alignment: alignment, offset: offset)) } @inlinable public func annotation( alignment: Alignment = .bottom, offset: CGVector = .zero, @ViewBuilder _ content: () -> Text? ) -> some GraphContent { return ModifiedGraphContent( self, GraphContentEffect.TextAnnotation(content(), alignment: alignment, offset: offset)) } @inlinable @_disfavoredOverload public func annotation( _ tag: String, alignment: Alignment = .bottom, offset: CGVector = .zero, @ViewBuilder _ content: () -> some View ) -> some GraphContent { return ModifiedGraphContent( self, GraphContentEffect.ViewAnnotation(tag, content(), alignment: alignment, offset: offset) ) } /// Sets the stroke style for this graph content. /// /// - When a `.clip` color is applied to node marks, the stroke color of the symbol /// will be **the same as the background (cliped to transparent).** /// - When a `.clip` color is applied to link marks, the stroke will not be drawn. /// - When a `nil` stroke style is applied to node marks, the stroke style will be the same as the default stroke style. @inlinable @_disfavoredOverload public func stroke( _ color: StrokeColor = .clip, _ strokeStyle: StrokeStyle? = nil ) -> some GraphContent { return ModifiedGraphContent( self, GraphContentEffect.Stroke(color, strokeStyle)) } @inlinable public func stroke( _ color: Color, _ strokeStyle: StrokeStyle? = nil ) -> some GraphContent { return ModifiedGraphContent( self, GraphContentEffect.Stroke(.color(color), strokeStyle)) } @inlinable public func linkShape( _ shape: some LinkShape ) -> some GraphContent { return ModifiedGraphContent(self, GraphContentEffect._LinkShape(shape)) } } ================================================ FILE: Sources/Grape/Modifiers/GraphContentModifier.swift ================================================ @_typeEraser(AnyGraphContentModifier) public protocol GraphContentModifier { @inlinable func _into( _ context: inout _GraphRenderingContext ) @inlinable func _exit( _ context: inout _GraphRenderingContext ) } ================================================ FILE: Sources/Grape/Modifiers/GraphForegroundScale.swift ================================================ import SwiftUI extension EnvironmentValues { @usableFromInline var graphForegroundScaleEnvironment: [AnyHashable: GraphicsContext.Shading] { get { self[__Key_graphForegroundScaleEnvironment.self] } set { self[__Key_graphForegroundScaleEnvironment.self] = newValue } } private struct __Key_graphForegroundScaleEnvironment: EnvironmentKey { typealias Value = [AnyHashable: GraphicsContext.Shading] static var defaultValue: Value { [:] } } } @usableFromInline struct GraphEnvironmentViewModifier: ViewModifier { @usableFromInline let colorScale: [AnyHashable: GraphicsContext.Shading] @inlinable init(_ mapping: KeyValuePairs) where S: ShapeStyle, DataValue: Hashable { var colorScale: [AnyHashable: GraphicsContext.Shading] = [:] mapping.forEach { colorScale[.init($0.0)] = .style($0.1) } self.colorScale = colorScale } @inlinable func body(content: Content) -> some View { content .environment(\.graphForegroundScaleEnvironment, colorScale) } } extension View { @inlinable func graphForegroundStyleScale(_ mapping: KeyValuePairs) -> some View where S: ShapeStyle, DataValue: Hashable { return modifier(GraphEnvironmentViewModifier(mapping)) } } ================================================ FILE: Sources/Grape/Modifiers/GraphProxy.swift ================================================ import ForceSimulation import SwiftUI public struct GraphProxy { @usableFromInline var storage: (any _AnyGraphProxyProtocol)? @inlinable init(_ storage: some _AnyGraphProxyProtocol) { self.storage = storage } @inlinable init() { self.storage = nil } } extension GraphProxy: _AnyGraphProxyProtocol { /// Find the node ID at the given location in the viewport coordinate, with specific type. /// Returns `nil` if no node is found or the node is not of the specified type. @inlinable public func node(of type: ID.Type, at locationInViewportCoordinate: CGPoint) -> ID? where ID : Hashable { storage?.node(of: type, at: locationInViewportCoordinate) } /// Find the type erased node ID at the given location in the viewport coordinate. /// Returns `nil` if no node is found. @inlinable public func node(at locationInViewportCoordinate: CGPoint) -> AnyHashable? { storage?.node(at: locationInViewportCoordinate) } @inlinable public func setNodeFixation(nodeID: ID, fixation: CGPoint?, minimumAlpha: Double = 0.5) { storage?.setNodeFixation(nodeID: nodeID, fixation: fixation, minimumAlpha: minimumAlpha) } @inlinable public var kineticAlpha: Double { get { storage?.kineticAlpha ?? 0 } nonmutating set { storage?.kineticAlpha = newValue } } @inlinable public var finalTransform: ViewportTransform { storage?.finalTransform ?? .identity } @inlinable public var modelTransform: ViewportTransform { _read { if let storage = storage { yield storage.modelTransform } else { fatalError() } } nonmutating _modify { if let storage { yield &storage.modelTransform } else { fatalError() } } } @inlinable public var obsoleteState: ObsoleteState { get { storage?.obsoleteState ?? .init(cgSize: .init(width: 0, height: 0)) } nonmutating set { storage?.obsoleteState = newValue } } public var lastTransformRecord: ViewportTransform? { get { storage?.lastTransformRecord } nonmutating set { storage?.lastTransformRecord = newValue } } } @usableFromInline struct GraphProxyKey: PreferenceKey { @inlinable static func reduce(value: inout GraphProxy, nextValue: () -> GraphProxy) { value = nextValue() } @inlinable static var defaultValue: GraphProxy { .init() } } extension View { @inlinable public func graphOverlay( alignment: Alignment = .center, @ViewBuilder content: @escaping (GraphProxy) -> V ) -> some View where V: View { self.overlayPreferenceValue(GraphProxyKey.self, content) } @inlinable public func graphBackground( alignment: Alignment = .center, @ViewBuilder content: @escaping (GraphProxy) -> V ) -> some View where V: View { self.backgroundPreferenceValue(GraphProxyKey.self, content) } } ================================================ FILE: Sources/Grape/Utils/CoreGraphics+SIMD.swift ================================================ // // CoreGraphics+SIMD.swift // // // Created by li3zhen1 on 12/13/23. // import SwiftUI //#if canImport(SwiftUI) && canImport(simd) import simd extension CGPoint { @inlinable internal var simd: SIMD2 { return SIMD2(x: x, y: y) } } extension CGSize { @inlinable internal var simd: SIMD2 { return SIMD2(x: width, y: height) } } extension CGVector { @inlinable internal var simd: SIMD2 { return SIMD2(x: dx, y: dy) } } extension SIMD2 where Scalar == Double { @inlinable internal var cgPoint: CGPoint { return CGPoint(x: x, y: y) } @inlinable internal var cgSize: CGSize { return CGSize(width: x, height: y) } @inlinable internal var cgVector: CGVector { return CGVector(dx: x, dy: y) } } extension CGRect { @inlinable internal func contains(_ point: SIMD2) -> Bool { return point.x >= origin.x && point.x <= origin.x + size.width && point.y >= origin.y && point.y <= origin.y + size.height } } //#endif ================================================ FILE: Sources/Grape/Utils/GraphProtocol.swift ================================================ import ForceSimulation protocol GraphProtocol { associatedtype Node: Identifiable associatedtype Edge: Identifiable where Edge.ID == EdgeID @inlinable var nodes: [Node] { get set } @inlinable var links: [Edge] { get set } } extension GraphProtocol { @inlinable mutating func pruneLinks() { let nodeDictionary = Dictionary(uniqueKeysWithValues: nodes.map { ($0.id, $0) }) let nodeSet = Set(nodes.map { $0.id }) // let linkSet = Set(links.map { $0.id }) // let nodesOccuredInLinkSet = Set(links.map { $0.id.source } + links.map { $0.id.target }) let validLinks = links.filter { nodeSet.contains($0.id.source) && nodeSet.contains($0.id.target) } self.nodes = nodeSet.map { nodeDictionary[$0]! } self.links = validLinks } @inlinable func isPruned() -> Bool { guard nodes.count == Set(nodes.map { $0.id }).count else { return false } guard links.count == Set(links.map { $0.id }).count else { return false } guard (links.allSatisfy { l in nodes.contains(where: { $0.id == l.id.source }) && nodes.contains(where: { $0.id == l.id.target }) }) else { return false } return true } @inlinable func difference(from other: Self) -> (nodeDiff: CollectionDifference, edgeDiff: CollectionDifference) { #if DEBUG assert(isPruned()) #endif let nodeDiff = nodes.difference(from: other.nodes) { $0.id == $1.id } let edgeDiff = links.difference(from: other.links) { $0.id == $1.id } return (nodeDiff, edgeDiff) } } ================================================ FILE: Sources/Grape/Utils/KeyFrame.swift ================================================ public struct KeyFrame { public var elapsed: UInt = 0 @inlinable @inline(__always) public init(rawValue: UInt) { self.elapsed = rawValue } @inlinable @inline(__always) public mutating func advance(by delta: UInt = 1) { elapsed &+= delta } @inlinable @inline(__always) public mutating func reset() { elapsed = 0 } } extension KeyFrame: RawRepresentable, Equatable, Hashable, ExpressibleByIntegerLiteral { @inlinable @inline(__always) public var rawValue: UInt { return elapsed } @inlinable @inline(__always) public init(integerLiteral value: UInt) { self.init(rawValue: value) } } extension KeyFrame: CustomStringConvertible { @inlinable public var description: String { return elapsed.description } } ================================================ FILE: Sources/Grape/Utils/LinkShape.swift ================================================ import SwiftUI public protocol LinkShape { @inlinable func path(from: CGPoint, to: CGPoint) -> Path @inlinable func decoration(from: CGPoint, to: CGPoint) -> Path? } extension LinkShape { @inlinable public func decoration(from: CGPoint, to: CGPoint) -> Path? { nil } } public protocol StraightLineLinkShape: LinkShape {} extension LinkShape where Self: StraightLineLinkShape { @inlinable public func path(from: CGPoint, to: CGPoint) -> Path { Path { path in path.move(to: from) path.addLine(to: to) } } } public struct PlainLineLink: LinkShape, StraightLineLinkShape { @inlinable public init() {} } extension LinkShape where Self == ArrowLineLink { @inlinable public static func arrow( size: CGFloat = 10, angle: Angle = .degrees(32), cornerRadius: CGFloat = 0 ) -> Self { .init(arrowSize: size, arrowAngle: angle, arrowCornerRadius: cornerRadius) } @inlinable public static var arrow: Self { arrow() } } public struct ArrowLineLink: LinkShape { @usableFromInline let arrowSize: CGFloat @usableFromInline let arrowAngle: Angle @usableFromInline let arrowCornerRadius: CGFloat @inlinable public init(arrowSize: CGFloat, arrowAngle: Angle, arrowCornerRadius: CGFloat) { self.arrowSize = arrowSize self.arrowAngle = arrowAngle self.arrowCornerRadius = arrowCornerRadius } @inlinable public func path(from: CGPoint, to: CGPoint) -> Path { let arrowAngle = self.arrowAngle.radians return Path { path in let angle = atan2(to.y - from.y, to.x - from.x) let angleLeft = angle + arrowAngle let angleRight = angle - arrowAngle path.move(to: from) path.addLine(to: to) path.move( to: CGPoint( x: to.x - arrowSize * cos(angleLeft), y: to.y - arrowSize * sin(angleLeft) )) path.addLine(to: to) path.addLine( to: CGPoint( x: to.x - arrowSize * cos(angleRight), y: to.y - arrowSize * sin(angleRight) ) ) } } } ================================================ FILE: Sources/Grape/Utils/RasterizedViewStore.swift ================================================ 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 } } } ================================================ FILE: Sources/Grape/Utils/Transform.swift ================================================ public protocol TransformProtocol { associatedtype Scalar: FloatingPoint & ExpressibleByFloatLiteral associatedtype Vector: SIMD where Vector.Scalar == Scalar @inlinable var translate: Vector { get set } @inlinable var scale: Scalar { get set } @inlinable init(translate: Vector, scale: Scalar) } extension TransformProtocol { @inlinable public static var identity: Self { return Self(translate: .zero, scale: 1) } @inlinable public func apply(to point: Vector) -> Vector { return point * scale + translate } @inlinable public func invert(_ point: Vector) -> Vector { return (point - translate) / scale } @inlinable public func apply(to points: [Vector]) -> [Vector] { return points.map(apply) } @inlinable public func translate(by delta: Vector) -> Self { return Self(translate: translate + delta, scale: scale) } @inlinable public func translate(to point: Vector) -> Self { return Self(translate: point, scale: scale) } @inlinable public mutating func translating(by delta: Vector) { self.translate = translate + delta } @inlinable public mutating func translating(to point: Vector) { // self = Self(translate: point, scale: scale) self.translate = point } @inlinable public func scale(by factor: Scalar) -> Self { return Self(translate: translate, scale: scale * factor) } @inlinable public func scale(to newScale: Scalar) -> Self { return Self(translate: translate, scale: newScale) } @inlinable public mutating func scaling(by delta: Scalar) { // self = Self(translate: translate, scale: scale * delta) self.scale = scale * delta } @inlinable public mutating func scaling(to factor: Scalar) { // self = Self(translate: translate, scale: factor) self.scale = factor } } public struct ViewportTransform: TransformProtocol { public typealias Scalar = Double public var translate: SIMD2 public var scale: Scalar @inlinable public init(translate: SIMD2, scale: Scalar) { self.translate = translate self.scale = scale } } public struct VolumeTransform: TransformProtocol { public typealias Scalar = Double // TODO: translate wastes 1 lane, // combine translate and scale into a single SIMD4? public var translate: SIMD3 public var scale: Scalar @inlinable public init(translate: SIMD3, scale: Scalar) { self.translate = translate self.scale = scale } } #if canImport(SwiftUI) import SwiftUI extension ViewportTransform { @inlinable public func toCGAffineTransform() -> CGAffineTransform { return CGAffineTransform( a: CGFloat(scale), b: 0, c: 0, d: CGFloat(scale), tx: CGFloat(translate.x), ty: CGFloat(translate.y) ) } @inlinable public func fromCGAffineTransform(_ transform: CGAffineTransform) -> Self { return Self( translate: .init(x: Scalar(transform.tx), y: Scalar(transform.ty)), scale: Scalar(transform.a) ) } } #endif ================================================ FILE: Sources/Grape/Utils/View+CGImage.swift ================================================ import CoreGraphics import SwiftUI //#if canImport(AppKit) // import AppKit // @inlinable // internal func getDisplayScale() -> CGFloat { // return NSScreen.main?.backingScaleFactor ?? 2.0 // } //#elseif os(xrOS) // @inlinable // internal func getDisplayScale() -> CGFloat { // return 2.0 // } //#elseif canImport(UIKit) // import UIKit // @inlinable // internal func getDisplayScale() -> CGFloat { // return UIScreen.main.scale // } //#else // @inlinable // internal func getDisplayScale() -> CGFloat { // return 2.0 // } //#endif // #if os(macOS) // import AppKit // @inlinable // func getCGContext() -> CGContext? { // return NSGraphicsContext.current?.cgContext // } // #elseif os(iOS) // import UIKit // @inlinable // func getCGContext() -> CGContext? { // return UIGraphicsGetCurrentContext() // } // #endif // class CLD: NSObject, CALayerDelegate { // func draw(_ layer: CALayer, in ctx: CGContext) { // let text = "Hello World!" // let font = NSFont.systemFont(ofSize: 72) // let attributes = [NSAttributedString.Key.font: font] // let attributedString = NSAttributedString(string: text, attributes: attributes) // let line = CTLineCreateWithAttributedString(attributedString) // let bounds = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions.useOpticalBounds) // ctx.textMatrix = .identity // ctx.translateBy(x: 0, y: bounds.height) // ctx.scaleBy(x: 1.0, y: -1.0) // CTLineDraw(line, ctx) // } // } extension View { @inlinable @MainActor internal func toCGImage(scaledBy factor: CGFloat) -> CGImage? { let renderer = ImageRenderer( content: self ) renderer.scale = factor // guard let image = renderer.nsImage else { return nil } // var imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) // let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) return renderer.cgImage } @inlinable @MainActor internal func toCGImage(with environment: EnvironmentValues, antialias: Double = 1.5) -> CGImage? { let renderer = ImageRenderer( content: self.environment(\.self, environment) ) renderer.scale = environment.displayScale * antialias // guard let image = renderer.nsImage else { return nil } // var imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) // let imageRef = image.cgImage(forProposedRect: &imageRect, context: nil, hints: nil) return renderer.cgImage } // @inlinable // @MainActor // internal func toCGImage() -> CGImage? { // let uicont // return renderer.cgImage // } // @inlinable // @MainActor // public func toCALayer() -> CALayer? { // let renderer = ImageRenderer(content: self) // if let context = getCGContext() { // renderer.render(rasterizationScale: 2.0) { size, render in // let caLayer = CALayer // } // } // return renderer.cgImage // } } extension Text { @inlinable internal func resolved() -> String { // This is an undocumented API return self._resolveText(in: Self.resolvingEnvironment) } @inlinable static internal var resolvingEnvironment: EnvironmentValues { return EnvironmentValues() } } ================================================ FILE: Sources/Grape/Views/ForceDirectedGraph+View.swift ================================================ import ForceSimulation import SwiftUI @MainActor extension ForceDirectedGraph: View { @inlinable public var body: some View { // HStack { // #if DEBUG // debugView // #endif // canvas // } canvas .preference(key: GraphProxyKey.self, value: .init(model)) .onChange( of: self._graphRenderingContextShadow, initial: false // Don't trigger on initial value, keep `changeMessage` as "N/A" ) { _, newValue in self.model.revive( for: newValue, forceDescriptor: self._forceDescriptors, alpha: self.model.simulationContext.storage.kinetics.alpha ) } .onAppear { self.model.trackStateMixin() } } // #if DEBUG // @ViewBuilder // @inlinable // var debugView: some View { // VStack(alignment: .leading, spacing: 8.0) { // Text("Elapsed Time: \(model.currentFrame)") // Divider() // Text(self.model.changeMessage) // Divider() // Button { // // self.clickCount += 1 // } label: { // Text("Click") // } // ScrollView { // ForEach(self.model.graphRenderingContext.nodes, id: \.id) { node in // Text("\(node.debugDescription)") // } // }.frame(maxWidth: .infinity) // } // .frame(width: 200.0) // } // #endif @inlinable @MainActor @ViewBuilder var canvas: some View { // #if DEBUG // let _ = Self._printChanges() // #endif Canvas { context, size in let _ = model.currentFrame self.model.render(&context, size) } // #if !os(tvOS) // .gesture( // DragGesture( // minimumDistance: Self.minimumDragDistance, // coordinateSpace: .local // ) // .onChanged(onDragChange) // .onEnded(onDragEnd) // ) // // .onTapGesture(count: 1, perform: onTapGesture) // #endif // #if os(iOS) || os(macOS) // .gesture( // MagnifyGesture(minimumScaleDelta: Self.minimumScaleDelta) // .onChanged(onMagnifyChange) // .onEnded(onMagnifyEnd) // ) // #endif } } extension ForceDirectedGraph: Equatable { @inlinable public static func == (lhs: Self, rhs: Self) -> Bool { return lhs._graphRenderingContextShadow == rhs._graphRenderingContextShadow // && lhs._forceDescriptors == rhs._forceDescriptors } } ================================================ FILE: Sources/Grape/Views/ForceDirectedGraph.swift ================================================ import ForceSimulation import SwiftUI public struct ForceDirectedGraph where NodeID == Content.NodeID { // public typealias NodeID = Content.NodeID @inlinable @Environment(\.self) internal var environment: EnvironmentValues @inlinable @Environment(\.graphForegroundScaleEnvironment) internal var graphForegroundScale @inlinable @Environment(\.colorScheme) internal var colorScheme @inlinable @Environment(\.colorSchemeContrast) internal var colorSchemeContrast // the copy of the graph context to be used for comparison in `onChange` // should be not used for rendering @usableFromInline internal let _graphRenderingContextShadow: _GraphRenderingContext @usableFromInline internal let _forceDescriptors: SealedForceDescriptor // // TBD: Some state to be retained when the graph is updated // @State // @inlinable // internal var clickCount = 0 // @State @inlinable internal var model: ForceDirectedGraphModel { @storageRestrictions(initializes: _model) init(initialValue) { _model = .init(initialValue: initialValue) } get { _model.wrappedValue } set { _model.wrappedValue = newValue } } @usableFromInline internal var _model: State> /// The default force to be applied to the graph /// /// - Returns: The default forces @inlinable static public func defaultForce() -> SealedForceDescriptor { .link().center() } /// Creates a force-directed graph view. /// /// This function creates a force-directed graph view with the given parameters. /// /// - Parameters: /// - states: The initial state of the force-directed graph. /// - ticksPerSecond: The number of ticks per second. Notice that this only determines the frequency of /// the simulation updates, and the actual frame rate may be different. /// - graph: The graph content. The `ForceDirectedGraph` will observe the changes of the graph content /// and try to update the elements with minimal changes across the parameter updates. /// - buildForce: The forces to be applied to the graph. /// - emittingNewNodesWithStates: Tells the simulation where to place the new nodes and provide their /// initial kinetic states. This is only applied on the new nodes that is not seen before when the /// graph is created (or updated). @inlinable @MainActor public init( states: ForceDirectedGraphState = ForceDirectedGraphState(), ticksPerSecond: Double = 60.0, @GraphContentBuilder graph: () -> Content, force buildForce: () -> SealedForceDescriptor = defaultForce, emittingNewNodesWithStates: @escaping (NodeID) -> KineticState = defaultKineticStateProvider ) { var gctx = _GraphRenderingContext() graph()._attachToGraphRenderingContext(&gctx) self._graphRenderingContextShadow = gctx self._forceDescriptors = buildForce() self.model = .init( gctx, forceDescriptor: self._forceDescriptors, stateMixin: states, emittingNewNodesWith: emittingNewNodesWithStates, ticksPerSecond: ticksPerSecond ) } @inlinable public static func defaultKineticStateProvider(nodeID: NodeID) -> KineticState { .init(position: .zero) } } ================================================ FILE: Sources/Grape/Views/ForceDirectedGraphModel+Observation.swift ================================================ import Observation extension ForceDirectedGraphModel: Observation.Observable { @inlinable nonisolated func access( keyPath: KeyPath ) { _$observationRegistrar.access(self, keyPath: keyPath) } @inlinable nonisolated func withMutation( keyPath: KeyPath, _ mutation: () throws -> MutationResult ) rethrows -> MutationResult { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } ================================================ FILE: Sources/Grape/Views/ForceDirectedGraphModel.findNode.swift ================================================ import ForceSimulation import SwiftUI import simd extension ForceDirectedGraphModel { @inlinable internal func findNodeFromViewAnnotation( at locationInSimulationCoordinate: SIMD2 ) -> GraphRenderingStates.StateID? { for (symbolID, box) in rasterizedSymbols { if box.contains(locationInSimulationCoordinate) { return symbolID } } return nil } @inlinable internal func findNode( at locationInSimulationCoordinate: SIMD2 ) -> NodeID? { let viewportScale = self.finalTransform.scale for i in simulationContext.storage.kinetics.range.reversed() { let iNodeID = simulationContext.nodeIndices[i] guard let iRadius2 = graphRenderingContext.nodeHitSizeAreaLookup[ simulationContext.nodeIndices[i] ] else { continue } let iPos = simulationContext.storage.kinetics.position[i] /// https://github.com/swiftgraphs/Grape/pull/62#issue-2753932460 /// /// ```swift /// let actualRadius = pow((iRadius2 * 0.5), 0.5) * 0.5 /// let scaledRadius = actualRadius / max(.ulpOfOne, viewportScale) /// let scaledRadius2 = pow(scaledRadius, 2.0) /// ``` /// let scaledRadius2 = iRadius2 / max(.ulpOfOne, (8.0 * viewportScale * viewportScale)) let length2 = simd_length_squared(locationInSimulationCoordinate - iPos) if length2 <= scaledRadius2 { return iNodeID } } return nil } @inlinable internal func findNode( at locationInViewportCoordinate: CGPoint ) -> NodeID? { let simulationLocation = self.finalTransform.invert(locationInViewportCoordinate.simd) return findNode(at: simulationLocation) } } ================================================ FILE: Sources/Grape/Views/ForceDirectedGraphModel.swift ================================================ import ForceSimulation import Foundation import Observation import SwiftUI @MainActor public protocol _AnyGraphProxyProtocol { @inlinable func node( at locationInViewportCoordinate: CGPoint ) -> AnyHashable? @inlinable func node(of type: ID.Type, at locationInViewportCoordinate: CGPoint) -> ID? @inlinable func setNodeFixation(nodeID: ID, fixation: CGPoint?, minimumAlpha: Double) @inlinable var kineticAlpha: Double { get nonmutating set } @inlinable var finalTransform: ViewportTransform { get } @inlinable var modelTransform: ViewportTransform { get nonmutating set } @inlinable var lastTransformRecord: ViewportTransform? { get nonmutating set } @inlinable var obsoleteState: ObsoleteState { get nonmutating set } } extension ForceDirectedGraphModel: _AnyGraphProxyProtocol { @inlinable public func node(of type: ID.Type, at locationInViewportCoordinate: CGPoint) -> ID? where ID: Hashable { if type.self == NodeID.self { return findNode(at: locationInViewportCoordinate) as! ID? } else { return nil } } @inlinable public func node(at locationInViewportCoordinate: CGPoint) -> AnyHashable? { // Find from view annotation first if let nodeIDFromViewAnnotation = findNodeFromViewAnnotation( at: finalTransform.invert(locationInViewportCoordinate.simd) ) { if case .node(let nodeID) = nodeIDFromViewAnnotation { return AnyHashable(nodeID) } } if let nodeID = findNode(at: locationInViewportCoordinate) { return AnyHashable(nodeID) } else { return nil } } @inlinable public func setNodeFixation(nodeID: ID, fixation: CGPoint?, minimumAlpha: Double) { guard let nodeID = nodeID as? NodeID else { return } simulationContext.storage.kinetics.alpha = max( simulationContext.storage.kinetics.alpha, minimumAlpha ) let newLocationInSimulation: SIMD2? = if let fixation { finalTransform.invert(fixation.simd) } else { nil } if let nodeIndex = simulationContext.nodeIndexLookup[nodeID] { simulationContext.storage.kinetics.fixation[nodeIndex] = newLocationInSimulation } } @inlinable public var kineticAlpha: Double { get { simulationContext.storage.kinetics.alpha } _modify { yield &simulationContext.storage.kinetics.alpha } } } public struct ObsoleteState { @usableFromInline var cgSize: CGSize @inlinable public init(cgSize: CGSize) { self.cgSize = cgSize } } @MainActor public final class ForceDirectedGraphModel { @usableFromInline var graphRenderingContext: _GraphRenderingContext @usableFromInline var simulationContext: SimulationContext @inlinable public var modelTransform: ViewportTransform { // @storageRestrictions(initializes: _modelTransform) // init(initialValue) { // _modelTransform = initialValue // } get { stateMixinRef.modelTransform } set { // _modelTransform = newValue stateMixinRef.modelTransform = newValue } } /// Moves the zero-centered simulation to final view // @usableFromInline public var finalTransform: ViewportTransform = .identity @usableFromInline var viewportPositions: UnsafeArray> @usableFromInline var draggingNodeID: NodeID? = nil @usableFromInline var backgroundDragStart: SIMD2? = nil @inlinable var isDragStartStateRecorded: Bool { return draggingNodeID != nil || backgroundDragStart != nil } // records the transform right before a magnification gesture starts public var lastTransformRecord: ViewportTransform? = nil @usableFromInline var rasterizedSymbols: [(GraphRenderingStates.StateID, CGRect)] = [] @usableFromInline let velocityDecay: Double // cache this so text size don't change on monitor switch @usableFromInline var lastRasterizedScaleFactor: Double = 2.0 @usableFromInline var _$changeMessage = "N/A" @usableFromInline var _$currentFrame: UInt = 0 @inlinable var changeMessage: String { @storageRestrictions(initializes: _$changeMessage) init(initialValue) { _$changeMessage = initialValue } get { access(keyPath: \.changeMessage) return _$changeMessage } set { withMutation(keyPath: \.changeMessage) { _$changeMessage = newValue } } } @inlinable var currentFrame: UInt { @storageRestrictions(initializes: _$currentFrame) init(initialValue) { _$currentFrame = initialValue } get { access(keyPath: \.currentFrame) return _$currentFrame } set { withMutation(keyPath: \.currentFrame) { _$currentFrame = newValue } } } /** Observation ignored params */ @usableFromInline let ticksPerSecond: Double @usableFromInline @MainActor var scheduledTimer: Timer? = nil @usableFromInline var _onTicked: ((UInt) -> Void)? = nil @usableFromInline var _onViewportTransformChanged: ((ViewportTransform, Bool) -> Void)? = nil @usableFromInline var _onSimulationStabilized: (() -> Void)? = nil @usableFromInline var _emittingNewNodesWith: (NodeID) -> KineticState // records the transform right before a magnification gesture starts public var obsoleteState = ObsoleteState(cgSize: .zero) @usableFromInline internal var stateMixinRef: ForceDirectedGraphState @inlinable init( _ graphRenderingContext: _GraphRenderingContext, forceDescriptor: SealedForceDescriptor, stateMixin: ForceDirectedGraphState, emittingNewNodesWith: @escaping (NodeID) -> KineticState = { _ in .init(position: .zero) }, ticksPerSecond: Double, velocityDecay: Double ) { self.graphRenderingContext = graphRenderingContext self.ticksPerSecond = ticksPerSecond self._emittingNewNodesWith = emittingNewNodesWith self.velocityDecay = velocityDecay let _simulationContext = SimulationContext.create( for: graphRenderingContext, makeForceField: forceDescriptor._makeForceField, velocityDecay: velocityDecay ) _simulationContext.updateAllKineticStates(emittingNewNodesWith) self.simulationContext = _simulationContext self.viewportPositions = .createUninitializedBuffer( count: self.simulationContext.storage.kinetics.position.count ) self.currentFrame = 0 self.stateMixinRef = stateMixin } @inlinable convenience init( _ graphRenderingContext: _GraphRenderingContext, forceDescriptor: SealedForceDescriptor, stateMixin: ForceDirectedGraphState, emittingNewNodesWith: @escaping (NodeID) -> KineticState = { _ in .init(position: .zero) }, ticksPerSecond: Double ) { self.init( graphRenderingContext, forceDescriptor: forceDescriptor, stateMixin: stateMixin, emittingNewNodesWith: emittingNewNodesWith, ticksPerSecond: ticksPerSecond, velocityDecay: 30 / ticksPerSecond ) } @inlinable func trackStateMixin() { Task { @MainActor [self] in switch stateMixinRef.ticksOnAppear { case .iteration(let count): simulationContext.storage.tick(ticks: .iteration(count)) case .untilReachingAlpha(let alpha): simulationContext.storage.tick(ticks: .untilReachingAlpha(alpha)) } withMutation(keyPath: \.currentFrame) { currentFrame += 1 } } if stateMixinRef.isRunning { start() } else { stop() } continuouslyTrackingRunning() continuouslyTrackingTransform() } @inlinable func continuouslyTrackingRunning() { withObservationTracking { [weak self] in guard let self else { return } self.updateModelRunningState(isRunning: self.stateMixinRef.isRunning) } onChange: { @Sendable [weak self] in guard let self else { return } Task { @MainActor [weak self] in self?.continuouslyTrackingRunning() } } } @inlinable func continuouslyTrackingTransform() { withObservationTracking { [weak self] in guard let self else { return } // FIXME: mutation cycle? _ = self.stateMixinRef.modelTransform // stateMixinRef.access(keyPath: \.modelTransform) } onChange: { [weak self] in guard let self else { return } Task { @MainActor [weak self] in self?.continuouslyTrackingTransform() } } } @inlinable func updateModelRunningState(isRunning: Bool) { if stateMixinRef.isRunning { DispatchQueue.main.async { [weak self] in self?.start() } } else { DispatchQueue.main.async { [weak self] in self?.stop() } } } @inlinable deinit { print("deinit") let _ = MainActor.assumeIsolated { scheduledTimer?.invalidate() } } @usableFromInline let _$observationRegistrar = Observation.ObservationRegistrar() } extension GraphicsContext.Shading { @inlinable static var defaultLinkShading: Self { return .color(.displayP3, red: 0.5, green: 0.5, blue: 0.5, opacity: 0.3) } @inlinable static var defaultNodeShading: Self { return .color(.primary) } } extension StrokeStyle { @inlinable static var defaultLinkStyle: Self { return StrokeStyle(lineWidth: 1.0) } } // Render related @MainActor extension ForceDirectedGraphModel { @inlinable func start(minAlpha: Double = 0.6) { guard self.scheduledTimer == nil else { return } print("Simulation started") if simulationContext.storage.kinetics.alpha < minAlpha { simulationContext.storage.kinetics.alpha = minAlpha } self.scheduledTimer = Timer.scheduledTimer( withTimeInterval: 1.0 / ticksPerSecond, repeats: true ) { [weak self] _ in if let capturedSelf = self { Task { @MainActor [weak capturedSelf] in capturedSelf?.tick() } } } } @inlinable func tick() { withMutation(keyPath: \.currentFrame) { simulationContext.storage.tick() currentFrame += 1 } _onTicked?(currentFrame) } @inlinable func stop() { print("Simulation stopped") self.scheduledTimer?.invalidate() self.scheduledTimer = nil } @inlinable func render( _ graphicsContext: inout GraphicsContext, _ size: CGSize ) { // should not invoke `access`, but actually does now ? // print("Rendering frame \(_$currentFrame.rawValue)") obsoleteState.cgSize = size let transform = modelTransform.translate(by: size.simd / 2) // debugPrint(transform.scale) // var viewportPositions = [SIMD2]() // viewportPositions.reserveCapacity(simulationContext.storage.kinetics.position.count) for i in simulationContext.storage.kinetics.position.range { viewportPositions[i] = transform.apply( to: simulationContext.storage.kinetics.position[i]) } self.finalTransform = transform for op in graphRenderingContext.linkOperations { guard let source = simulationContext.nodeIndexLookup[op.mark.id.source], let target = simulationContext.nodeIndexLookup[op.mark.id.target] else { continue } let sourcePos = viewportPositions[source] let targetPos = viewportPositions[target] let p = if let pathBuilder = op.path { { let sourceNodeRadius = sqrt(graphRenderingContext.nodeHitSizeAreaLookup[op.mark.id.source] ?? 0) / 2 let targetNodeRadius = sqrt(graphRenderingContext.nodeHitSizeAreaLookup[op.mark.id.target] ?? 0) / 2 let angle = atan2(targetPos.y - sourcePos.y, targetPos.x - sourcePos.x) let sourceOffset = SIMD2( cos(angle) * sourceNodeRadius, sin(angle) * sourceNodeRadius ) let targetOffset = SIMD2( cos(angle) * targetNodeRadius, sin(angle) * targetNodeRadius ) let sourcePosWithOffset = sourcePos + sourceOffset let targetPosWithOffset = targetPos - targetOffset // return pathBuilder(sourcePosWithOffset, targetPosWithOffset) return pathBuilder(sourcePosWithOffset, targetPosWithOffset) }() } else { Path { path in path.move(to: sourcePos.cgPoint) path.addLine(to: targetPos.cgPoint) } } if let strokeEffect = op.stroke { switch strokeEffect.color { case .color(let color): graphicsContext.stroke( p, with: .color(color), style: strokeEffect.style ?? .defaultLinkStyle ) case .clip: break } } else { graphicsContext.stroke( p, with: .defaultLinkShading, style: .defaultLinkStyle ) } } for op in graphRenderingContext.nodeOperations { guard let id = simulationContext.nodeIndexLookup[op.mark.id] else { continue } let pos = viewportPositions[id] graphicsContext.transform = .init(translationX: pos.x, y: pos.y) let finalizedPath: Path = switch op.pathOrSymbolSize { case .path(let path): path case .symbolSize(let size): Path( ellipseIn: CGRect( origin: CGPoint(x: -size.width / 2, y: -size.height / 2), size: size ) ) } graphicsContext.fill( finalizedPath, with: op.fill ?? .defaultNodeShading ) if let strokeEffect = op.stroke { switch strokeEffect.color { case .color(let color): graphicsContext.stroke( finalizedPath, with: .color(color), style: strokeEffect.style ?? .defaultLinkStyle ) case .clip: graphicsContext.blendMode = .clear graphicsContext.stroke( finalizedPath, with: .color(.black), style: strokeEffect.style ?? .defaultLinkStyle ) graphicsContext.blendMode = .normal } } } // return var newRasterizedSymbols = [(GraphRenderingStates.StateID, CGRect)]() graphicsContext.transform = .identity.concatenating(CGAffineTransform(scaleX: 1, y: -1)) graphicsContext.withCGContext { cgContext in for (symbolID, resolvedTextContent) in graphRenderingContext.resolvedTexts { guard let resolvedStatus = graphRenderingContext.symbols[resolvedTextContent] else { continue } // Look for rasterized symbol's image var rasterizedSymbol: CGImage? = nil switch resolvedStatus { case .pending(let text): let env = graphicsContext.environment let cgImage = text.toCGImage( with: env, antialias: Self.textRasterizationAntialias ) lastRasterizedScaleFactor = env.displayScale graphRenderingContext.symbols[resolvedTextContent] = .resolved( text, cgImage) rasterizedSymbol = cgImage 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) let rect = CGRect( x: pos.x + offset.x + textImageOffset.x, // - physicalWidth / 2, y: -pos.y - offset.y - textImageOffset.y, // - physicalHeight width: physicalWidth, height: physicalHeight ) cgContext.draw( rasterizedSymbol, in: rect ) newRasterizedSymbols.append((symbolID, rect)) } 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) let rect = CGRect( x: center.x + offset.x + textImageOffset.x, // - physicalWidth / 2, y: -center.y - offset.y - textImageOffset.y, // - physicalHeight width: physicalWidth, height: physicalHeight ) cgContext.draw( rasterizedSymbol, in: rect ) newRasterizedSymbols.append((symbolID, rect)) } } } 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) let rect = CGRect( x: pos.x + offset.x + textImageOffset.x, // - physicalWidth / 2, y: -pos.y - offset.y - textImageOffset.y, // - physicalHeight width: physicalWidth, height: physicalHeight ) cgContext.draw( rasterizedSymbol, in: rect ) newRasterizedSymbols.append((symbolID, rect)) } 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) let rect = CGRect( x: center.x + offset.x + textImageOffset.x, // - physicalWidth / 2, y: -center.y - offset.y - textImageOffset.y, // - physicalHeight width: physicalWidth, height: physicalHeight ) cgContext.draw( rasterizedSymbol, in: rect ) newRasterizedSymbols.append((symbolID, rect)) } } } } rasterizedSymbols = newRasterizedSymbols } @inlinable static var textRasterizationAntialias: Double { return 1.5 } @inlinable func revive( for newContext: _GraphRenderingContext, forceDescriptor: SealedForceDescriptor, alpha: Double ) { var newContext = newContext self.simulationContext.revive( for: newContext, makeForceField: forceDescriptor._makeForceField, 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 if self.simulationContext.storage.kinetics.position.count != self.viewportPositions.count { self.viewportPositions = .createUninitializedBuffer( count: self.simulationContext.storage.kinetics.position.count ) } debugPrint( "Graph state revived. Note this might cause expensive rerendering when combined with `annotation` with non-text views and unstable id." ) } } ================================================ FILE: Sources/Grape/Views/ForceDirectedGraphState.swift ================================================ import Observation // public typealias ForceDirectedGraphState = ForceDirectedGraphMixedState // extension ForceDirectedGraphMixedState where Mixin == Void { // @inlinable // convenience init( // initialIsRunning: Bool = true, // initialModelTransform: ViewportTransform = .identity // ) { // self.init( // initialMixin: (), // initialIsRunning: initialIsRunning, // initialModelTransform: initialModelTransform // ) // } // } public enum Ticks: Sendable { case untilReachingAlpha(Double?) case iteration(Int) @inlinable public static var zero: Self { .iteration(0) } @inlinable public static var untilStable: Self { .untilReachingAlpha(nil) } } public class ForceDirectedGraphState: Observation.Observable { @usableFromInline internal var ticksOnAppear: Ticks @usableFromInline internal var _$modelTransform: ViewportTransform @usableFromInline internal var _$isRunning: Bool @inlinable public var modelTransform: ViewportTransform { get { _reg.access(self, keyPath: \.modelTransform) return _$modelTransform } set { _reg.withMutation(of: self, keyPath: \.modelTransform) { _$modelTransform = newValue } } } @inlinable public var isRunning: Bool { get { _reg.access(self, keyPath: \.isRunning) return _$isRunning } set { _reg.withMutation(of: self, keyPath: \.isRunning) { _$isRunning = newValue } } } @inlinable public init( initialIsRunning: Bool = true, initialModelTransform: ViewportTransform = .identity, ticksOnAppear: Ticks = .iteration(0) ) { self._reg = Observation.ObservationRegistrar() self._$modelTransform = initialModelTransform self._$isRunning = initialIsRunning self.ticksOnAppear = ticksOnAppear } // MARK: - Observation @usableFromInline let _reg: Observation.ObservationRegistrar } ================================================ FILE: Sources/Grape/Views/GraphLayoutInputs.swift ================================================ struct GraphLayoutInputs { } ================================================ FILE: Sources/Grape/Views/GraphRenderingContext.swift ================================================ import SwiftUI public struct _GraphRenderingContext { @usableFromInline 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: ViewResolvingState] = [:] @usableFromInline internal var nodeOperations: [RenderOperation.Node] = [] /// A lookup table for the hit area of each node (width * height). @usableFromInline internal var nodeHitSizeAreaLookup: [NodeID: Double] = [:] @usableFromInline internal var linkOperations: [RenderOperation.Link] = [] @inlinable internal init() { } @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.nodeOperations == rhs.nodeOperations && lhs.linkOperations == rhs.linkOperations } } extension _GraphRenderingContext { @inlinable internal var nodes: [NodeMark] { nodeOperations.map(\.mark) } @inlinable internal var edges: [LinkMark] { linkOperations.map(\.mark) } } ================================================ FILE: Sources/Grape/Views/GraphRenderingStates.swift ================================================ import SwiftUI @usableFromInline internal struct GraphRenderingStates { @usableFromInline enum StateID: Hashable { case node(NodeID) case link(NodeID, NodeID) } @usableFromInline var currentID: StateID? = nil @usableFromInline var shading: [GraphicsContext.Shading] = [] @inlinable var currentShading: GraphicsContext.Shading? { shading.last } @usableFromInline var stroke: [GraphContentEffect.Stroke] = [] @inlinable var currentStroke: GraphContentEffect.Stroke? { stroke.last } @usableFromInline var opacity: [Double] = [] @inlinable var currentOpacity: Double? { opacity.last } @usableFromInline var symbolShape: [Path] = [] @inlinable var currentSymbolShapeOrSize: PathOrSymbolSize { if let shape = symbolShape.last { return .path(shape) } else { return .symbolSize(currentSymbolSizeOrDefault) } } @usableFromInline var symbolSize: [CGSize] = [] @inlinable var currentSymbolSize: CGSize? { symbolSize.last } @inlinable var currentSymbolSizeOrDefault: CGSize { symbolSize.last ?? defaultSymbolSize } @usableFromInline let defaultShading: GraphicsContext.Shading @usableFromInline let defaultSymbolSize = CGSize(width: 6, height: 6) @usableFromInline var linkShape: [any LinkShape] = [] @inlinable var currentLinkShape: any LinkShape { linkShape.last ?? self.defaultLinkShape } @usableFromInline let defaultLinkShape = PlainLineLink() @inlinable init( defaultShading: GraphicsContext.Shading = .color(.blue), reservingCapacity capacity: Int = 128 ) { shading.reserveCapacity(capacity) stroke.reserveCapacity(capacity) opacity.reserveCapacity(capacity) self.defaultShading = defaultShading } } ================================================ FILE: Sources/Grape/Views/RenderOperation.swift ================================================ import SwiftUI @usableFromInline enum PathOrSymbolSize: Equatable { case path(Path) case symbolSize(CGSize) } @usableFromInline internal enum RenderOperation { @usableFromInline struct Node { @usableFromInline let mark: NodeMark @usableFromInline let fill: GraphicsContext.Shading? @usableFromInline let stroke: GraphContentEffect.Stroke? @usableFromInline let pathOrSymbolSize: PathOrSymbolSize @inlinable init( _ mark: NodeMark, _ fill: GraphicsContext.Shading?, _ stroke: GraphContentEffect.Stroke?, _ pathOrSymbolSize: PathOrSymbolSize ) { self.mark = mark self.fill = fill self.stroke = stroke self.pathOrSymbolSize = pathOrSymbolSize } } @usableFromInline struct Link { @usableFromInline let mark: LinkMark @usableFromInline let stroke: GraphContentEffect.Stroke? @usableFromInline let path: ((SIMD2, SIMD2) -> Path)? @inlinable init( _ mark: LinkMark, _ stroke: GraphContentEffect.Stroke?, _ path: ((SIMD2, SIMD2) -> Path)? ) { self.mark = mark self.stroke = stroke self.path = path } } } extension RenderOperation.Node: Equatable { @inlinable internal static func == (lhs: Self, rhs: Self) -> Bool { let fillEq = lhs.fill == nil && rhs.fill == nil let pathEq = lhs.pathOrSymbolSize == rhs.pathOrSymbolSize return lhs.mark == rhs.mark && fillEq && lhs.stroke == rhs.stroke && pathEq } } extension RenderOperation.Link: Equatable { @inlinable internal static func == (lhs: Self, rhs: Self) -> Bool { let pathEq = lhs.path == nil && rhs.path == nil return lhs.mark == rhs.mark && lhs.stroke == rhs.stroke && pathEq } } // @usableFromInline // internal enum RenderingOperation { // case node( // NodeMark, // GraphicsContext.Shading?, // StrokeStyle?, // ((SIMD2) -> Path)? // ) // case link( // LinkMark, // GraphicsContext.Shading?, // StrokeStyle?, // ((SIMD2, SIMD2) -> Path)? // ) // case label(Text, id: GraphRenderingStates.StateID) // } // extension RenderingOperation: Equatable { // @inlinable // internal static func == (lhs: Self, rhs: Self) -> Bool { // return false // } // } ================================================ FILE: Sources/Grape/Views/SimulationContext.swift ================================================ import ForceSimulation public struct KineticState { public let position: SIMD2 public let velocity: SIMD2 public let fixation: SIMD2? @inlinable public init( position: SIMD2, velocity: SIMD2 = .zero, fixation: SIMD2? = nil ) { self.position = position self.velocity = velocity self.fixation = fixation } } @usableFromInline internal struct SimulationContext { public typealias Vector = ForceField.Vector public typealias ForceField = SealedForce2D @usableFromInline internal var storage: Simulation2D @usableFromInline internal var nodeIndexLookup: [NodeID: Int] @usableFromInline internal var nodeIndices: [NodeID] @inlinable package init( _ storage: Simulation2D, _ nodeIndexLookup: [NodeID: Int], _ nodeIndices: [NodeID] ) { self.storage = storage self.nodeIndexLookup = nodeIndexLookup self.nodeIndices = nodeIndices } } extension SimulationContext { @inlinable public static func create( for graphRenderingContext: _GraphRenderingContext, makeForceField: (inout SealedForce2D, [NodeID]) -> Void, velocityDecay: Vector.Scalar ) -> Self { let nodes = graphRenderingContext.nodes let nodeIndexLookup = Dictionary( uniqueKeysWithValues: nodes.enumerated().map { ($0.element.id, $0.offset) } ) let nodeIDs = nodes.map { $0.id } var forceField = SealedForce2D([]) makeForceField(&forceField, nodeIDs) let links = graphRenderingContext.edges.map { EdgeID( source: nodeIndexLookup[$0.id.source]!, target: nodeIndexLookup[$0.id.target]! ) } return .init( .init( nodeCount: nodes.count, links: links, forceField: forceField, velocityDecay: velocityDecay ), nodeIndexLookup, nodes.map(\.id) ) } // @inlinable // public static func create( // for graphRenderingContext: _GraphRenderingContext, // with forceField: ForceField, // velocityDecay: Vector.Scalar // ) -> Self { // let nodes = graphRenderingContext.nodes // let nodeIndexLookup = Dictionary( // uniqueKeysWithValues: nodes.enumerated().map { // ($0.element.id, $0.offset) // } // ) // let links = graphRenderingContext.edges.map { // EdgeID( // source: nodeIndexLookup[$0.id.source]!, // target: nodeIndexLookup[$0.id.target]! // ) // } // return .init( // .init( // nodeCount: nodes.count, // links: links, // forceField: forceField, // velocityDecay: velocityDecay // ), // nodeIndexLookup, // nodes.map(\.id) // ) // } /// reuse the same simulation context for new graph @inlinable public mutating func revive( for newContext: _GraphRenderingContext, makeForceField: (inout SealedForce2D, [NodeID]) -> Void, velocityDecay: Vector.Scalar, emittingNewNodesWith states: (NodeID) -> KineticState = { _ in .init(position: .zero) } ) { let newNodes = newContext.nodes let newNodeIndexLookup = Dictionary( uniqueKeysWithValues: newNodes.enumerated().map { ($0.element.id, $0.offset) } ) let nodeIDs = newNodes.map { $0.id } var newForceField = SealedForce2D([]) makeForceField(&newForceField, nodeIDs) let newLinks = newContext.edges.map { EdgeID( source: newNodeIndexLookup[$0.id.source]!, target: newNodeIndexLookup[$0.id.target]! ) } let newlyAddedNodes = newNodes.filter { newNode in !nodeIndexLookup.keys.contains(newNode.id) } let newlyAddedNodeStates = Dictionary( uniqueKeysWithValues: newlyAddedNodes.map { ($0.id, states($0.id)) } ) let newPosition = newNodes.map { if let index = self.nodeIndexLookup[$0.id] { return storage.kinetics.position[index] } else { if let newState = newlyAddedNodeStates[$0.id] { return newState.position } return .zero } } let newVelocity = newNodes.map { if let index = self.nodeIndexLookup[$0.id] { return storage.kinetics.velocity[index] } else { if let newState = newlyAddedNodeStates[$0.id] { return newState.velocity } return .zero } } let newFixation = newNodes.map { if let index = self.nodeIndexLookup[$0.id] { return storage.kinetics.fixation[index] } else { if let newState = newlyAddedNodeStates[$0.id] { return newState.fixation } return nil } } let newStorage = Simulation2D( nodeCount: newNodes.count, links: newLinks, forceField: newForceField, velocityDecay: velocityDecay, position: newPosition, velocity: newVelocity, fixation: newFixation ) self = .init( newStorage, newNodeIndexLookup, newNodes.map(\.id) ) } @inlinable public func getKineticState(nodeID: NodeID) -> KineticState? { if let index = nodeIndexLookup[nodeID] { return .init( position: storage.kinetics.position[index], velocity: storage.kinetics.velocity[index], fixation: storage.kinetics.fixation[index] ) } else { return nil } } @inlinable public func updateKineticState(nodeID: NodeID, _ state: KineticState) { if let index = nodeIndexLookup[nodeID] { storage.kinetics.position[index] = state.position storage.kinetics.velocity[index] = state.velocity storage.kinetics.fixation[index] = state.fixation } } @inlinable public func updateAllKineticStates(_ states: (NodeID) -> KineticState) { for (nodeID, index) in nodeIndexLookup { let state = states(nodeID) storage.kinetics.position[index] = state.position storage.kinetics.velocity[index] = state.velocity storage.kinetics.fixation[index] = state.fixation } } } ================================================ FILE: Tests/ForceSimulationTests/ForceTests.swift ================================================ // // File.swift // // // Created by li3zhen1 on 10/4/23. // import XCTest import simd @testable import ForceSimulation final class ForceTests: XCTestCase { private func _testForceMutatePositions(_ myForce: some Force2D) { let simulation = Simulation( nodeCount: 5, links: [(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)].map { EdgeID(source: $0.0, target: $0.1) }, forceField: myForce ) for i in 0...10 { simulation.tick() } let position = simulation.kinetics.position.asArray() XCTAssertNotEqual(position, Array(repeating: .zero, count: 5)) } func testLinkForceMutatesPosition() { _testForceMutatePositions( SealedForce2D { Kinetics2D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(35) ) } ) } func testManyBodyForceMutatesPosition() { _testForceMutatePositions( SealedForce2D { Kinetics2D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(35) ) Kinetics2D.ManyBodyForce(strength: -300) } ) } } ================================================ FILE: Tests/ForceSimulationTests/GKTreeCompareTest.swift ================================================ // // File.swift // // // Created by li3zhen1 on 10/4/23. // import XCTest import simd @testable import ForceSimulation // #if canImport(GameKit) // import GameKit // #endif struct DummyQuadtreeDelegate: KDTreeDelegate { @inlinable mutating func didAddNode(_ node: Int, at position: SIMD2) { count += 1 } @inlinable mutating func didRemoveNode(_ node: Int, at position: SIMD2) { count -= 1 } @inlinable func copy() -> Self { return Self(count: count) } @inlinable func spawn() -> Self { return Self(count: 0) } var count = 0 init(count: Int = 0) { self.count = count } } struct NamedNode: Identifiable { let name: String let id: Int static var count = 0 static func make(_ name: String) -> NamedNode { defer { count += 1 } return NamedNode(name: name, id: count) } } final class ManyBodyForceTests: XCTestCase { // #if canImport(GameKit) // func _testGameKit() { // // randomly generate 100000 nodes in [-100, 100] x [-100, 100] // let nodes: [simd_float2] = (0..<100000).map { _ in // let x = Float.random(in: -100...100) // let y = Float.random(in: -100...100) // return simd_float2(x, y) // } // measure { // let gkTree = GKQuadtree( // boundingQuad: .init(quadMin: [-100.0, -100.0], quadMax: [100.0, 100.0]), // minimumCellSize: 1e-5 // ) // for (i, node) in nodes.enumerated() { // gkTree.add(NSNumber(value: i), at: node) // } // // traverse the tree // var count = 0 // gkTree.elements(in: .init(quadMin: [-100.0, -100.0], quadMax: [100.0, 100.0])) // .forEach { _ in count += 1 } // XCTAssertEqual(count, nodes.count) // } // } // #endif func testGrapeKDTree() { let nodes: [simd_double2] = (0..<1000).map { _ in let x = Double.random(in: -100...100) let y = Double.random(in: -100...100) return simd_double2(x, y) } #if RELEASE measure { var kdtree = BufferedKDTree, DummyQuadtreeDelegate>( rootBox: .init([-100.0, -100.0], [100.0, 100.0]), nodeCapacity: nodes.count, rootDelegate: DummyQuadtreeDelegate() ) for (i, node) in nodes.enumerated() { kdtree.add(nodeIndex: i, at: node) } // traverse the tree var count = 0 kdtree.visit { t in if t.isLeaf { count += t.delegate.count return false } return true } XCTAssertEqual(count, nodes.count) } #else var kdtree = BufferedKDTree, DummyQuadtreeDelegate>( rootBox: .init([-100.0, -100.0], [100.0, 100.0]), nodeCapacity: nodes.count, rootDelegate: DummyQuadtreeDelegate() ) for (i, node) in nodes.enumerated() { kdtree.add(nodeIndex: i, at: node) } // traverse the tree var count = 0 kdtree.visit { t in if t.isLeaf { count += t.delegate.count return false } return true } XCTAssertEqual(count, nodes.count) #endif } } ================================================ FILE: Tests/ForceSimulationTests/MiserableData.swift ================================================ // // miserables.swift // GrapeView // // Created by li3zhen1 on 10/8/23. // import Foundation let miserables = """ { "nodes": [ {"id": "Myriel", "group": 1}, {"id": "Napoleon", "group": 1}, {"id": "Mlle.Baptistine", "group": 1}, {"id": "Mme.Magloire", "group": 1}, {"id": "CountessdeLo", "group": 1}, {"id": "Geborand", "group": 1}, {"id": "Champtercier", "group": 1}, {"id": "Cravatte", "group": 1}, {"id": "Count", "group": 1}, {"id": "OldMan", "group": 1}, {"id": "Labarre", "group": 2}, {"id": "Valjean", "group": 2}, {"id": "Marguerite", "group": 3}, {"id": "Mme.deR", "group": 2}, {"id": "Isabeau", "group": 2}, {"id": "Gervais", "group": 2}, {"id": "Tholomyes", "group": 3}, {"id": "Listolier", "group": 3}, {"id": "Fameuil", "group": 3}, {"id": "Blacheville", "group": 3}, {"id": "Favourite", "group": 3}, {"id": "Dahlia", "group": 3}, {"id": "Zephine", "group": 3}, {"id": "Fantine", "group": 3}, {"id": "Mme.Thenardier", "group": 4}, {"id": "Thenardier", "group": 4}, {"id": "Cosette", "group": 5}, {"id": "Javert", "group": 4}, {"id": "Fauchelevent", "group": 0}, {"id": "Bamatabois", "group": 2}, {"id": "Perpetue", "group": 3}, {"id": "Simplice", "group": 2}, {"id": "Scaufflaire", "group": 2}, {"id": "Woman1", "group": 2}, {"id": "Judge", "group": 2}, {"id": "Champmathieu", "group": 2}, {"id": "Brevet", "group": 2}, {"id": "Chenildieu", "group": 2}, {"id": "Cochepaille", "group": 2}, {"id": "Pontmercy", "group": 4}, {"id": "Boulatruelle", "group": 6}, {"id": "Eponine", "group": 4}, {"id": "Anzelma", "group": 4}, {"id": "Woman2", "group": 5}, {"id": "MotherInnocent", "group": 0}, {"id": "Gribier", "group": 0}, {"id": "Jondrette", "group": 7}, {"id": "Mme.Burgon", "group": 7}, {"id": "Gavroche", "group": 8}, {"id": "Gillenormand", "group": 5}, {"id": "Magnon", "group": 5}, {"id": "Mlle.Gillenormand", "group": 5}, {"id": "Mme.Pontmercy", "group": 5}, {"id": "Mlle.Vaubois", "group": 5}, {"id": "Lt.Gillenormand", "group": 5}, {"id": "Marius", "group": 8}, {"id": "BaronessT", "group": 5}, {"id": "Mabeuf", "group": 8}, {"id": "Enjolras", "group": 8}, {"id": "Combeferre", "group": 8}, {"id": "Prouvaire", "group": 8}, {"id": "Feuilly", "group": 8}, {"id": "Courfeyrac", "group": 8}, {"id": "Bahorel", "group": 8}, {"id": "Bossuet", "group": 8}, {"id": "Joly", "group": 8}, {"id": "Grantaire", "group": 8}, {"id": "MotherPlutarch", "group": 9}, {"id": "Gueulemer", "group": 4}, {"id": "Babet", "group": 4}, {"id": "Claquesous", "group": 4}, {"id": "Montparnasse", "group": 4}, {"id": "Toussaint", "group": 5}, {"id": "Child1", "group": 10}, {"id": "Child2", "group": 10}, {"id": "Brujon", "group": 4}, {"id": "Mme.Hucheloup", "group": 8} ], "links": [ {"source": "Napoleon", "target": "Myriel", "value": 1}, {"source": "Mlle.Baptistine", "target": "Myriel", "value": 8}, {"source": "Mme.Magloire", "target": "Myriel", "value": 10}, {"source": "Mme.Magloire", "target": "Mlle.Baptistine", "value": 6}, {"source": "CountessdeLo", "target": "Myriel", "value": 1}, {"source": "Geborand", "target": "Myriel", "value": 1}, {"source": "Champtercier", "target": "Myriel", "value": 1}, {"source": "Cravatte", "target": "Myriel", "value": 1}, {"source": "Count", "target": "Myriel", "value": 2}, {"source": "OldMan", "target": "Myriel", "value": 1}, {"source": "Valjean", "target": "Labarre", "value": 1}, {"source": "Valjean", "target": "Mme.Magloire", "value": 3}, {"source": "Valjean", "target": "Mlle.Baptistine", "value": 3}, {"source": "Valjean", "target": "Myriel", "value": 5}, {"source": "Marguerite", "target": "Valjean", "value": 1}, {"source": "Mme.deR", "target": "Valjean", "value": 1}, {"source": "Isabeau", "target": "Valjean", "value": 1}, {"source": "Gervais", "target": "Valjean", "value": 1}, {"source": "Listolier", "target": "Tholomyes", "value": 4}, {"source": "Fameuil", "target": "Tholomyes", "value": 4}, {"source": "Fameuil", "target": "Listolier", "value": 4}, {"source": "Blacheville", "target": "Tholomyes", "value": 4}, {"source": "Blacheville", "target": "Listolier", "value": 4}, {"source": "Blacheville", "target": "Fameuil", "value": 4}, {"source": "Favourite", "target": "Tholomyes", "value": 3}, {"source": "Favourite", "target": "Listolier", "value": 3}, {"source": "Favourite", "target": "Fameuil", "value": 3}, {"source": "Favourite", "target": "Blacheville", "value": 4}, {"source": "Dahlia", "target": "Tholomyes", "value": 3}, {"source": "Dahlia", "target": "Listolier", "value": 3}, {"source": "Dahlia", "target": "Fameuil", "value": 3}, {"source": "Dahlia", "target": "Blacheville", "value": 3}, {"source": "Dahlia", "target": "Favourite", "value": 5}, {"source": "Zephine", "target": "Tholomyes", "value": 3}, {"source": "Zephine", "target": "Listolier", "value": 3}, {"source": "Zephine", "target": "Fameuil", "value": 3}, {"source": "Zephine", "target": "Blacheville", "value": 3}, {"source": "Zephine", "target": "Favourite", "value": 4}, {"source": "Zephine", "target": "Dahlia", "value": 4}, {"source": "Fantine", "target": "Tholomyes", "value": 3}, {"source": "Fantine", "target": "Listolier", "value": 3}, {"source": "Fantine", "target": "Fameuil", "value": 3}, {"source": "Fantine", "target": "Blacheville", "value": 3}, {"source": "Fantine", "target": "Favourite", "value": 4}, {"source": "Fantine", "target": "Dahlia", "value": 4}, {"source": "Fantine", "target": "Zephine", "value": 4}, {"source": "Fantine", "target": "Marguerite", "value": 2}, {"source": "Fantine", "target": "Valjean", "value": 9}, {"source": "Mme.Thenardier", "target": "Fantine", "value": 2}, {"source": "Mme.Thenardier", "target": "Valjean", "value": 7}, {"source": "Thenardier", "target": "Mme.Thenardier", "value": 13}, {"source": "Thenardier", "target": "Fantine", "value": 1}, {"source": "Thenardier", "target": "Valjean", "value": 12}, {"source": "Cosette", "target": "Mme.Thenardier", "value": 4}, {"source": "Cosette", "target": "Valjean", "value": 31}, {"source": "Cosette", "target": "Tholomyes", "value": 1}, {"source": "Cosette", "target": "Thenardier", "value": 1}, {"source": "Javert", "target": "Valjean", "value": 17}, {"source": "Javert", "target": "Fantine", "value": 5}, {"source": "Javert", "target": "Thenardier", "value": 5}, {"source": "Javert", "target": "Mme.Thenardier", "value": 1}, {"source": "Javert", "target": "Cosette", "value": 1}, {"source": "Fauchelevent", "target": "Valjean", "value": 8}, {"source": "Fauchelevent", "target": "Javert", "value": 1}, {"source": "Bamatabois", "target": "Fantine", "value": 1}, {"source": "Bamatabois", "target": "Javert", "value": 1}, {"source": "Bamatabois", "target": "Valjean", "value": 2}, {"source": "Perpetue", "target": "Fantine", "value": 1}, {"source": "Simplice", "target": "Perpetue", "value": 2}, {"source": "Simplice", "target": "Valjean", "value": 3}, {"source": "Simplice", "target": "Fantine", "value": 2}, {"source": "Simplice", "target": "Javert", "value": 1}, {"source": "Scaufflaire", "target": "Valjean", "value": 1}, {"source": "Woman1", "target": "Valjean", "value": 2}, {"source": "Woman1", "target": "Javert", "value": 1}, {"source": "Judge", "target": "Valjean", "value": 3}, {"source": "Judge", "target": "Bamatabois", "value": 2}, {"source": "Champmathieu", "target": "Valjean", "value": 3}, {"source": "Champmathieu", "target": "Judge", "value": 3}, {"source": "Champmathieu", "target": "Bamatabois", "value": 2}, {"source": "Brevet", "target": "Judge", "value": 2}, {"source": "Brevet", "target": "Champmathieu", "value": 2}, {"source": "Brevet", "target": "Valjean", "value": 2}, {"source": "Brevet", "target": "Bamatabois", "value": 1}, {"source": "Chenildieu", "target": "Judge", "value": 2}, {"source": "Chenildieu", "target": "Champmathieu", "value": 2}, {"source": "Chenildieu", "target": "Brevet", "value": 2}, {"source": "Chenildieu", "target": "Valjean", "value": 2}, {"source": "Chenildieu", "target": "Bamatabois", "value": 1}, {"source": "Cochepaille", "target": "Judge", "value": 2}, {"source": "Cochepaille", "target": "Champmathieu", "value": 2}, {"source": "Cochepaille", "target": "Brevet", "value": 2}, {"source": "Cochepaille", "target": "Chenildieu", "value": 2}, {"source": "Cochepaille", "target": "Valjean", "value": 2}, {"source": "Cochepaille", "target": "Bamatabois", "value": 1}, {"source": "Pontmercy", "target": "Thenardier", "value": 1}, {"source": "Boulatruelle", "target": "Thenardier", "value": 1}, {"source": "Eponine", "target": "Mme.Thenardier", "value": 2}, {"source": "Eponine", "target": "Thenardier", "value": 3}, {"source": "Anzelma", "target": "Eponine", "value": 2}, {"source": "Anzelma", "target": "Thenardier", "value": 2}, {"source": "Anzelma", "target": "Mme.Thenardier", "value": 1}, {"source": "Woman2", "target": "Valjean", "value": 3}, {"source": "Woman2", "target": "Cosette", "value": 1}, {"source": "Woman2", "target": "Javert", "value": 1}, {"source": "MotherInnocent", "target": "Fauchelevent", "value": 3}, {"source": "MotherInnocent", "target": "Valjean", "value": 1}, {"source": "Gribier", "target": "Fauchelevent", "value": 2}, {"source": "Mme.Burgon", "target": "Jondrette", "value": 1}, {"source": "Gavroche", "target": "Mme.Burgon", "value": 2}, {"source": "Gavroche", "target": "Thenardier", "value": 1}, {"source": "Gavroche", "target": "Javert", "value": 1}, {"source": "Gavroche", "target": "Valjean", "value": 1}, {"source": "Gillenormand", "target": "Cosette", "value": 3}, {"source": "Gillenormand", "target": "Valjean", "value": 2}, {"source": "Magnon", "target": "Gillenormand", "value": 1}, {"source": "Magnon", "target": "Mme.Thenardier", "value": 1}, {"source": "Mlle.Gillenormand", "target": "Gillenormand", "value": 9}, {"source": "Mlle.Gillenormand", "target": "Cosette", "value": 2}, {"source": "Mlle.Gillenormand", "target": "Valjean", "value": 2}, {"source": "Mme.Pontmercy", "target": "Mlle.Gillenormand", "value": 1}, {"source": "Mme.Pontmercy", "target": "Pontmercy", "value": 1}, {"source": "Mlle.Vaubois", "target": "Mlle.Gillenormand", "value": 1}, {"source": "Lt.Gillenormand", "target": "Mlle.Gillenormand", "value": 2}, {"source": "Lt.Gillenormand", "target": "Gillenormand", "value": 1}, {"source": "Lt.Gillenormand", "target": "Cosette", "value": 1}, {"source": "Marius", "target": "Mlle.Gillenormand", "value": 6}, {"source": "Marius", "target": "Gillenormand", "value": 12}, {"source": "Marius", "target": "Pontmercy", "value": 1}, {"source": "Marius", "target": "Lt.Gillenormand", "value": 1}, {"source": "Marius", "target": "Cosette", "value": 21}, {"source": "Marius", "target": "Valjean", "value": 19}, {"source": "Marius", "target": "Tholomyes", "value": 1}, {"source": "Marius", "target": "Thenardier", "value": 2}, {"source": "Marius", "target": "Eponine", "value": 5}, {"source": "Marius", "target": "Gavroche", "value": 4}, {"source": "BaronessT", "target": "Gillenormand", "value": 1}, {"source": "BaronessT", "target": "Marius", "value": 1}, {"source": "Mabeuf", "target": "Marius", "value": 1}, {"source": "Mabeuf", "target": "Eponine", "value": 1}, {"source": "Mabeuf", "target": "Gavroche", "value": 1}, {"source": "Enjolras", "target": "Marius", "value": 7}, {"source": "Enjolras", "target": "Gavroche", "value": 7}, {"source": "Enjolras", "target": "Javert", "value": 6}, {"source": "Enjolras", "target": "Mabeuf", "value": 1}, {"source": "Enjolras", "target": "Valjean", "value": 4}, {"source": "Combeferre", "target": "Enjolras", "value": 15}, {"source": "Combeferre", "target": "Marius", "value": 5}, {"source": "Combeferre", "target": "Gavroche", "value": 6}, {"source": "Combeferre", "target": "Mabeuf", "value": 2}, {"source": "Prouvaire", "target": "Gavroche", "value": 1}, {"source": "Prouvaire", "target": "Enjolras", "value": 4}, {"source": "Prouvaire", "target": "Combeferre", "value": 2}, {"source": "Feuilly", "target": "Gavroche", "value": 2}, {"source": "Feuilly", "target": "Enjolras", "value": 6}, {"source": "Feuilly", "target": "Prouvaire", "value": 2}, {"source": "Feuilly", "target": "Combeferre", "value": 5}, {"source": "Feuilly", "target": "Mabeuf", "value": 1}, {"source": "Feuilly", "target": "Marius", "value": 1}, {"source": "Courfeyrac", "target": "Marius", "value": 9}, {"source": "Courfeyrac", "target": "Enjolras", "value": 17}, {"source": "Courfeyrac", "target": "Combeferre", "value": 13}, {"source": "Courfeyrac", "target": "Gavroche", "value": 7}, {"source": "Courfeyrac", "target": "Mabeuf", "value": 2}, {"source": "Courfeyrac", "target": "Eponine", "value": 1}, {"source": "Courfeyrac", "target": "Feuilly", "value": 6}, {"source": "Courfeyrac", "target": "Prouvaire", "value": 3}, {"source": "Bahorel", "target": "Combeferre", "value": 5}, {"source": "Bahorel", "target": "Gavroche", "value": 5}, {"source": "Bahorel", "target": "Courfeyrac", "value": 6}, {"source": "Bahorel", "target": "Mabeuf", "value": 2}, {"source": "Bahorel", "target": "Enjolras", "value": 4}, {"source": "Bahorel", "target": "Feuilly", "value": 3}, {"source": "Bahorel", "target": "Prouvaire", "value": 2}, {"source": "Bahorel", "target": "Marius", "value": 1}, {"source": "Bossuet", "target": "Marius", "value": 5}, {"source": "Bossuet", "target": "Courfeyrac", "value": 12}, {"source": "Bossuet", "target": "Gavroche", "value": 5}, {"source": "Bossuet", "target": "Bahorel", "value": 4}, {"source": "Bossuet", "target": "Enjolras", "value": 10}, {"source": "Bossuet", "target": "Feuilly", "value": 6}, {"source": "Bossuet", "target": "Prouvaire", "value": 2}, {"source": "Bossuet", "target": "Combeferre", "value": 9}, {"source": "Bossuet", "target": "Mabeuf", "value": 1}, {"source": "Bossuet", "target": "Valjean", "value": 1}, {"source": "Joly", "target": "Bahorel", "value": 5}, {"source": "Joly", "target": "Bossuet", "value": 7}, {"source": "Joly", "target": "Gavroche", "value": 3}, {"source": "Joly", "target": "Courfeyrac", "value": 5}, {"source": "Joly", "target": "Enjolras", "value": 5}, {"source": "Joly", "target": "Feuilly", "value": 5}, {"source": "Joly", "target": "Prouvaire", "value": 2}, {"source": "Joly", "target": "Combeferre", "value": 5}, {"source": "Joly", "target": "Mabeuf", "value": 1}, {"source": "Joly", "target": "Marius", "value": 2}, {"source": "Grantaire", "target": "Bossuet", "value": 3}, {"source": "Grantaire", "target": "Enjolras", "value": 3}, {"source": "Grantaire", "target": "Combeferre", "value": 1}, {"source": "Grantaire", "target": "Courfeyrac", "value": 2}, {"source": "Grantaire", "target": "Joly", "value": 2}, {"source": "Grantaire", "target": "Gavroche", "value": 1}, {"source": "Grantaire", "target": "Bahorel", "value": 1}, {"source": "Grantaire", "target": "Feuilly", "value": 1}, {"source": "Grantaire", "target": "Prouvaire", "value": 1}, {"source": "MotherPlutarch", "target": "Mabeuf", "value": 3}, {"source": "Gueulemer", "target": "Thenardier", "value": 5}, {"source": "Gueulemer", "target": "Valjean", "value": 1}, {"source": "Gueulemer", "target": "Mme.Thenardier", "value": 1}, {"source": "Gueulemer", "target": "Javert", "value": 1}, {"source": "Gueulemer", "target": "Gavroche", "value": 1}, {"source": "Gueulemer", "target": "Eponine", "value": 1}, {"source": "Babet", "target": "Thenardier", "value": 6}, {"source": "Babet", "target": "Gueulemer", "value": 6}, {"source": "Babet", "target": "Valjean", "value": 1}, {"source": "Babet", "target": "Mme.Thenardier", "value": 1}, {"source": "Babet", "target": "Javert", "value": 2}, {"source": "Babet", "target": "Gavroche", "value": 1}, {"source": "Babet", "target": "Eponine", "value": 1}, {"source": "Claquesous", "target": "Thenardier", "value": 4}, {"source": "Claquesous", "target": "Babet", "value": 4}, {"source": "Claquesous", "target": "Gueulemer", "value": 4}, {"source": "Claquesous", "target": "Valjean", "value": 1}, {"source": "Claquesous", "target": "Mme.Thenardier", "value": 1}, {"source": "Claquesous", "target": "Javert", "value": 1}, {"source": "Claquesous", "target": "Eponine", "value": 1}, {"source": "Claquesous", "target": "Enjolras", "value": 1}, {"source": "Montparnasse", "target": "Javert", "value": 1}, {"source": "Montparnasse", "target": "Babet", "value": 2}, {"source": "Montparnasse", "target": "Gueulemer", "value": 2}, {"source": "Montparnasse", "target": "Claquesous", "value": 2}, {"source": "Montparnasse", "target": "Valjean", "value": 1}, {"source": "Montparnasse", "target": "Gavroche", "value": 1}, {"source": "Montparnasse", "target": "Eponine", "value": 1}, {"source": "Montparnasse", "target": "Thenardier", "value": 1}, {"source": "Toussaint", "target": "Cosette", "value": 2}, {"source": "Toussaint", "target": "Javert", "value": 1}, {"source": "Toussaint", "target": "Valjean", "value": 1}, {"source": "Child1", "target": "Gavroche", "value": 2}, {"source": "Child2", "target": "Gavroche", "value": 2}, {"source": "Child2", "target": "Child1", "value": 3}, {"source": "Brujon", "target": "Babet", "value": 3}, {"source": "Brujon", "target": "Gueulemer", "value": 3}, {"source": "Brujon", "target": "Thenardier", "value": 3}, {"source": "Brujon", "target": "Gavroche", "value": 1}, {"source": "Brujon", "target": "Eponine", "value": 1}, {"source": "Brujon", "target": "Claquesous", "value": 1}, {"source": "Brujon", "target": "Montparnasse", "value": 1}, {"source": "Mme.Hucheloup", "target": "Bossuet", "value": 1}, {"source": "Mme.Hucheloup", "target": "Joly", "value": 1}, {"source": "Mme.Hucheloup", "target": "Grantaire", "value": 1}, {"source": "Mme.Hucheloup", "target": "Bahorel", "value": 1}, {"source": "Mme.Hucheloup", "target": "Courfeyrac", "value": 1}, {"source": "Mme.Hucheloup", "target": "Gavroche", "value": 1}, {"source": "Mme.Hucheloup", "target": "Enjolras", "value": 1} ] } """ struct Miserable: Codable { struct Node: Codable, Identifiable { let id: String let group: Int } struct Edge: Codable { let source: String let target: String let value: Int } let nodes: [Node] let links: [Edge] } func getData() -> Miserable { let jd = JSONDecoder() return try! jd.decode(Miserable.self, from: miserables.data(using: .utf8)!) } ================================================ FILE: Tests/ForceSimulationTests/MiserableGraphTest.swift ================================================ // // MiserableGraphTest.swift // // // Created by li3zhen1 on 10/4/23. // import XCTest // import ForceSimulation import simd @testable import ForceSimulation func getLinks() -> [EdgeID] { let data = getData() return data.links.map { l in EdgeID( source: data.nodes.firstIndex { n in n.id == l.source }!, target: data.nodes.firstIndex { n in n.id == l.target }! ) } } struct MyForceField: ForceField2D { var force = CompositedForce { Kinetics2D.ManyBodyForce(strength: -30) Kinetics2D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(35) ) Kinetics2D.CenterForce(center: .zero, strength: 1) Kinetics2D.CollideForce(radius: .constant(3)) } } struct MySealedForce: ForceField2D { var force = SealedForce2D { Kinetics2D.ManyBodyForce(strength: -30) Kinetics2D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(35) ) Kinetics2D.CenterForce(center: .zero, strength: 1) Kinetics2D.CollideForce(radius: .constant(3)) } } struct MyLatticeForce: ForceField2D { var force = SealedForce2D { Kinetics2D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(1) ) Kinetics2D.ManyBodyForce(strength: -1) } } struct MyForceField3D: ForceField3D { var force = CompositedForce { Kinetics3D.ManyBodyForce(strength: -30) Kinetics3D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(35) ) Kinetics3D.CenterForce(center: .zero, strength: 1) Kinetics3D.CollideForce(radius: .constant(3)) } } final class MiserableGraphTest: XCTestCase { #if DEBUG let iter = 3 #else let iter = 120 #endif func testLattice() { let myForce = SealedForce2D { Kinetics2D.ManyBodyForce(strength: -30) Kinetics2D.LinkForce( stiffness: .weightedByDegree(k: { _, _ in 1.0 }), originalLength: .constant(35) ) Kinetics2D.CenterForce(center: .zero, strength: 1) Kinetics2D.CollideForce(radius: .constant(3)) } let width = 20 var edge = [(Int, Int)]() for i in 0..( @GraphContentBuilder _ builder: () -> some GraphContent ) -> some GraphContent where NodeID: Hashable { let result = builder() return result } func testForLoop() { let _ = buildGraph { Series(0..<10) { i in NodeMark(id: i) } } } func testMixed() { let _ = buildGraph { LinkMark(from: 0, to: 1) NodeMark(id: 3) NodeMark(id: 4) // AnyGraphContent( // NodeMark(id: 5) // ) } } func testConditional() { // let _ = buildGraph { // if true { // NodeMark(id: 0) // } else { // NodeMark(id: 1) // } // } } struct ID: Identifiable { var id: Int } func testForEach() { let arr = [ ID(id: 0), ID(id: 1), ID(id: 2), ] let _ = buildGraph { Series(arr) { i in NodeMark(id: i.id) } } } func testComposing() { struct MyGraphContent: GraphContent { var body: some GraphContent { NodeMark(id: 1) AnnotationNodeMark(id: 3, radius: 4.0) { EmptyView() } } } let _ = buildGraph { MyGraphContent() } } } ================================================ FILE: Tests/GrapeTests/GraphContentBuilderTests.swift ================================================ import SwiftUI import XCTest import simd @testable import Grape func buildGraph( @GraphContentBuilder _ builder: () -> some GraphContent ) -> some GraphContent where NodeID: Hashable { let result = builder() return result } final class GraphContentBuilderTests: XCTestCase { func testSyntaxes() { struct ID: Identifiable { var id: Int } let arr = [ ID(id: 0), ID(id: 1), ID(id: 2), ] let _ = Series(arr) { i in NodeMark(id: i.id) } let _ = buildGraph { NodeMark(id: 0) Series(arr) { i in NodeMark(id: i.id) } } let _ = buildGraph { NodeMark(id: 0) Series(0..<10) { i in NodeMark(id: 0) } } let t = 1 let d = buildGraph { if true { NodeMark(id: 0) Series(0..<10) { NodeMark(id: $0) } } else { LinkMark(from: 0, to: 1) NodeMark(id: 0) } if t == 1 { LinkMark(from: 0, to: 1) } } var gc = _GraphRenderingContext() d._attachToGraphRenderingContext(&gc) XCTAssert( gc.nodes.count == 11, "Expected 1 node, got \(gc.nodes.count)" ) } func testForLoop() { let gc = buildGraph { Series(0..<10) { i in NodeMark(id: i) } } var ctx = _GraphRenderingContext() gc._attachToGraphRenderingContext(&ctx) XCTAssert( ctx.nodes.count == 10, "Expected 10 nodes, got \(ctx.nodes.count)" ) } func testMixed() { let gc = buildGraph { LinkMark(from: 0, to: 1) NodeMark(id: 3) NodeMark(id: 4) NodeMark(id: 5) } var ctx = _GraphRenderingContext() gc._attachToGraphRenderingContext(&ctx) XCTAssert( ctx.nodes.count == 3, "Expected 3 nodes, got \(ctx.nodes.count)" ) XCTAssert( ctx.edges.count == 1, "Expected 1 edge, got \(ctx.edges.count)" ) XCTAssert( ctx.nodes[0].id == 3) XCTAssert( ctx.nodes[1].id == 4) XCTAssert( ctx.nodes[2].id == 5) } func testConditional() { let gc = buildGraph { if true { NodeMark(id: 0) .foregroundStyle(.red) // .opacity(0.2) } else { NodeMark(id: 1) } } var ctx = _GraphRenderingContext() gc._attachToGraphRenderingContext(&ctx) XCTAssert( ctx.nodes.count == 1, "Expected 1 node, got \(ctx.nodes.count)" ) XCTAssert( ctx.nodes[0].id == 0, "Expected 0 edges, got \(ctx.edges.count)" ) XCTAssert( ctx.edges.count == 0, "Expected 0 edges, got \(ctx.edges.count)" ) } struct ID: Identifiable { var id: Int } func testForEach() { let arr = [ ID(id: 0), ID(id: 1), ID(id: 2), ] let gc = buildGraph { Series(arr) { i in NodeMark(id: i.id) // .opacity(0.2) } } var ctx = _GraphRenderingContext() gc._attachToGraphRenderingContext(&ctx) XCTAssert( ctx.nodes.count == 3, "Expected 3 nodes, got \(ctx.nodes.count)" ) } struct MyGraphComponent: GraphContent { var body: some GraphContent { NodeMark(id: 0) // .opacity(0.6) NodeMark(id: 1) NodeMark(id: 2) } } func testCustomComponent() { let gc = buildGraph { MyGraphComponent() // .opacity(0.2) } var ctx = _GraphRenderingContext() gc._attachToGraphRenderingContext(&ctx) XCTAssert( ctx.nodes.count == 3, "Expected 3 nodes, got \(ctx.nodes.count)" ) } } ================================================ FILE: Tests/KDTreeTests/BufferedKDTreeTests.swift ================================================ import XCTest @testable import ForceSimulation struct CountKDTreeDelegate: KDTreeDelegate { mutating func didAddNode(_ node: Int, at position: SIMD2) { count += 1 } mutating func didRemoveNode(_ node: Int, at position: SIMD2) { count -= 1 } typealias NodeID = Int typealias Vector = SIMD2 var count = 0 func spawn() -> CountKDTreeDelegate { return .init(count: 0) } } class BufferedKDTreeTests: XCTestCase { private func buildTree( box: KDBox>, points: [SIMD2] ) -> BufferedKDTree, CountKDTreeDelegate> { var t = BufferedKDTree( rootBox: box, nodeCapacity: points.count, rootDelegate: CountKDTreeDelegate() ) for i in points.indices { t.add(nodeIndex: i, at: points[i]) } return t } func testCorner() { let t = buildTree( box: .init(p0: [0, 0], p1: [1, 1]), points: [ [0, 0] ]) XCTAssert(t.root.nodeIndices!.index == 0) XCTAssert(t.root.childrenBufferPointer == nil) XCTAssert(t.root.delegate.count == 1) } func testCorner2() { let t = buildTree( box: .init(p0: [0, 0], p1: [1, 1]), points: [ [1, 1] ]) XCTAssert(t.root.nodeIndices == nil) XCTAssert(t.root.delegate.count == 1) XCTAssert(t.root.childrenBufferPointer![3].delegate.count == 1) XCTAssert(t.root.box.p1 == [2, 2]) } func testRandomTree() { let randomPoints = (0..<1000).map { _ in SIMD2([Double.random(in: 0..<100), Double.random(in: 0..<100)]) } let t = buildTree(box: .init(p0: [0, 0], p1: [100, 100]), points: randomPoints) XCTAssert(t.root.delegate.count == randomPoints.count) XCTAssert( Array(0..(0.125, 0.125)]) t.add(nodeIndex: 1, at: [0.1251, 0.1251]) } func testExpand() { var t = buildTree(box: .init(p0: [0, 0], p1: [1, 1]), points: [SIMD2(0.5, 0.5)]) t.add(nodeIndex: 1, at: [1.5, 1.5]) XCTAssert(t.root.box.p1 == [2, 2]) XCTAssert( Array(1..<5).reduce( 0, { partialResult, n in partialResult + t.treeNodeBuffer[n].delegate.count }) == 2) t.add(nodeIndex: 2, at: [1.5, 0.5]) XCTAssert(t.root.box.p1 == [2, 2]) XCTAssert( Array(1..<5).reduce( 0, { partialResult, n in partialResult + t.treeNodeBuffer[n].delegate.count }) == 3) t.add(nodeIndex: 3, at: [0.51, 0.51]) XCTAssert(t.root.box.p1 == [2, 2]) XCTAssert( Array(1..<5).reduce( 0, { partialResult, n in partialResult + t.treeNodeBuffer[n].delegate.count }) == 4) XCTAssert( Array(5..<9).reduce( 0, { partialResult, n in partialResult + t.treeNodeBuffer[n].delegate.count }) == 2) XCTAssert( Array(0..