Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DirectedGraphDemo/graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
{"source": "A", "target": "E", "value": 4},
{"source": "A", "target": "F", "value": 4},
{"source": "A", "target": "G", "value": 6},
{"source": "B", "target": "B", "value": 3},
{"source": "G", "target": "G1", "value": 4},
{"source": "G", "target": "G2", "value": 4},
{"source": "G", "target": "G3", "value": 4},
Expand Down
66 changes: 51 additions & 15 deletions Sources/Views/Arrow.swift
Original file line number Diff line number Diff line change
@@ -1,38 +1,74 @@
import SwiftUI

struct Arrow: Shape {
private let pointerLineLength: CGFloat = 30
private let arrowAngle = CGFloat(Double.pi / 6)
enum Constants {
static let pointerLineLength: CGFloat = 30
static let arrowAngle = CGFloat(Double.pi / 6)
static let circularAngle: Angle = .degrees(90)
static var circularRadius: CGFloat { pointerLineLength * 1.25 }
}

let start: CGPoint
let end: CGPoint
let end: CGPoint?
let thickness: CGFloat

func path(in rect: CGRect) -> Path {
var path = Path()

path.move(to: start)
path.addLine(to: end)

let delta = end - start
let angle = delta.angle
let arrowLine1 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - angle + arrowAngle),
y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - angle + arrowAngle))
let arrowLine2 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - angle - arrowAngle),
y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - angle - arrowAngle))
let angle: CGFloat
let lineEnd: CGPoint

if let end {
path.move(to: start)
path.addLine(to: end)
let delta = end - start
angle = delta.angle
lineEnd = end
} else {
path.move(to: start)
let startAngle = Constants.circularAngle
let endAngle = startAngle + .degrees(45)
let startPoint = CGPoint(
cos(startAngle.radians) * -Constants.circularRadius,
sin(startAngle.radians) * -Constants.circularRadius)
+ start

path.addArc(
center: startPoint,
radius: Constants.circularRadius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
// not quite tangential to endAngle because of curvature
angle = (endAngle - .degrees(75)).radians
lineEnd = path.currentPoint ?? start
}

let arrowLine1 = CGPoint(
x: lineEnd.x + Constants.pointerLineLength * cos(CGFloat(Double.pi) - angle + Constants.arrowAngle),
y: lineEnd.y - Constants.pointerLineLength * sin(CGFloat(Double.pi) - angle + Constants.arrowAngle))
let arrowLine2 = CGPoint(
x: lineEnd.x + Constants.pointerLineLength * cos(CGFloat(Double.pi) - angle - Constants.arrowAngle),
y: lineEnd.y - Constants.pointerLineLength * sin(CGFloat(Double.pi) - angle - Constants.arrowAngle))

path.move(to: arrowLine1)
path.addLine(to: end)
path.addLine(to: lineEnd)
path.addLine(to: arrowLine2)

return path.strokedPath(.init(lineWidth: thickness))
}
}

struct Arrow_Previews: PreviewProvider {
static let start = CGPoint(x: 80, y: 80)
static let start = CGPoint(x: 120, y: 160)
static let end = CGPoint(x: 300, y: 200)

static let circular = CGPoint(x: 120, y: 400)

static var previews: some View {
Arrow(start: start, end: end, thickness: 4)
ZStack {
Arrow(start: start, end: end, thickness: 4)
Arrow(start: circular, end: nil, thickness: 2)
}
}
}
13 changes: 11 additions & 2 deletions Sources/Views/EdgeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ final class EdgeViewModel: ObservableObject, Identifiable {
}
}

var middle: CGPoint { (source.position + target.position) / 2 }
var middle: CGPoint {
guard source.id != target.id else {
return CGPoint(
cos(Arrow.Constants.circularAngle.radians) * -Arrow.Constants.circularRadius,
sin(Arrow.Constants.circularAngle.radians) * -Arrow.Constants.circularRadius)
+ start
}
return (source.position + target.position) / 2
}

var start: CGPoint {
source.position
}

var end: CGPoint {
var end: CGPoint? {
guard source.id != target.id else { return nil }
let delta = target.position - start
let angle = delta.angle
let suppr = CGPoint(x: cos(angle) * (target.size.width + value) * 0.5, y: sin(angle) * (target.size.height + value) * 0.5)
Expand Down
1 change: 1 addition & 0 deletions Sources/Views/GraphView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ struct GraphView_Previews: PreviewProvider {
SimpleNode(id: "4", group: 2)]

private static let edges = [
SimpleEdge(source: "1", target: "1", value: 5),
SimpleEdge(source: "1", target: "2", value: 5),
SimpleEdge(source: "1", target: "3", value: 1),
SimpleEdge(source: "3", target: "4", value: 2),
Expand Down
9 changes: 7 additions & 2 deletions Tests/Views/EdgeViewModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ class EdgeViewModelTests: XCTestCase {
targetNode.position = CGPoint(25, 40)

let viewModel = EdgeViewModel(source: sourceNode, target: targetNode, value: .zero)

XCTAssertFalse(viewModel.end.x.isNaN)
XCTAssertNotNil(viewModel.end)
XCTAssertFalse(viewModel.end!.x.isNaN)
}

func testEndIsNilWhenSourceAndTargetAreSameNode() {
let viewModel = EdgeViewModel(source: sourceNode, target: sourceNode, value: .zero)
XCTAssertNil(viewModel.end)
}

func testSourceChangeTriggerEdgeChange() {
Expand Down
12 changes: 12 additions & 0 deletions Tests/Views/EdgeViewTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ class EdgeViewTests: XCTestCase {
XCTAssertEqual(arrow.end, viewModel.end)
}

func testStartAndEndPositionsSelfReferencing() throws {
let position1 = CGPoint(x: 10, y: 20)
nodeViewModel1.position = position1
let viewModel = EdgeViewModel(source: nodeViewModel1, target: nodeViewModel1, value: 40)
let view = makeView(viewModel)

let arrow = try view.inspect().zStack().view(Arrow.self, 0).actualView()

XCTAssertEqual(arrow.start, position1)
XCTAssertEqual(arrow.end, viewModel.end)
}

func testWhenShowValueDisplayText() throws {
let viewModel = EdgeViewModel(source: nodeViewModel1, target: nodeViewModel2, value: 40)
viewModel.showValue = true
Expand Down