-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathradiant_vector.dart
143 lines (127 loc) · 5 KB
/
radiant_vector.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import 'package:flutter/material.dart';
enum LineSide { left, right, onTopOf }
extension NormalizeOffset on Offset {
Offset normalized() {
return Offset(dx / distance, dy / distance);
}
}
/// A radiant vector is like a line segment but it might have infinite length in
/// exactly one direction. It's an optionally infinite ray. I have decided this
class RadiantVector implements Comparable {
late final Offset from;
late final Offset towards;
late final bool infiniteLength;
RadiantVector(this.from, this.towards, this.infiniteLength);
@override
String toString() {
return "RadiantVector{"
"from $from, towards $towards, ${infiniteLength ? '' : 'not '}infinite}";
}
double _computeLength() {
return infiniteLength ? double.infinity : (towards - from).distance;
}
double? _length;
double get length => _length ??= _computeLength();
Offset _computePositionVector() {
return towards - from;
}
Offset? _positionVector;
Offset get positionVector => _positionVector ??= _computePositionVector();
RadiantVector normalized() {
return RadiantVector(from, positionVector / length + towards, false);
}
RadiantVector translate(Offset direction) {
return RadiantVector(from + direction, towards + direction, infiniteLength);
}
/// Based on vector length. Implementation note: uses [Offset.distanceSquared]
/// for performance
@override
int compareTo(covariant RadiantVector p2) {
final thisDS =
infiniteLength ? double.infinity : (towards - from).distanceSquared;
final otherDS = p2.infiniteLength
? double.infinity
: (p2.towards - p2.from).distanceSquared;
if (thisDS == otherDS) {
return 0;
} else if (thisDS < otherDS) {
return -1;
} else {
return 1;
}
}
/// Assumes we're standing at [from] and look at [towards]. WARNING: this
/// method cheats and assumes this vector extends in both directions
/// infinitely because the use case is specific enough to allow that for now.
/// Fixing that would just require checking if a perpendicular vector
/// extending from point would intersect this vector (presumably via a call to
/// [intersectWith] like at the beginning of [closestPointOn]) and returning a
/// new value if not (i.e. if [intersectWith] returns null.)
LineSide onSideOf(Offset point) {
final det = (towards.dx - from.dx) * (point.dy - from.dy) -
(towards.dy - from.dy) * (point.dx - from.dx);
if (det.abs() < 0.0001) {
return LineSide.onTopOf;
} else if (det < 0) {
return LineSide.left;
} else {
return LineSide.right;
}
}
/// Finds the intersection point between two [RadiantVector]s. Returns null
/// if no intersection can be found or if the vectors are colinear. Heavily
/// adapted from https://stackoverflow.com/a/565282/3962267 .
Offset? intersectWith(covariant RadiantVector other) {
double crossProduct(Offset a, Offset b) {
return a.dx * b.dy - a.dy * b.dx;
}
if (crossProduct(positionVector, other.positionVector) == 0) {
// vectors are parallel
return null;
}
final thisT = crossProduct(
(other.from - from),
other.positionVector /
(crossProduct(positionVector, other.positionVector)));
final otherT = crossProduct(from - other.from,
positionVector / crossProduct(other.positionVector, positionVector));
if (thisT < 0 || otherT < 0) {
// would need to go "backward" along the vector to find an intersection
return null;
} else if (thisT > 1 && !infiniteLength) {
return null;
} else if (otherT > 1 && !other.infiniteLength) {
return null;
} else {
final scaledPosition = positionVector * thisT;
return from + scaledPosition;
}
}
Offset closestPointOn(Offset point) {
// Theory: to find the closest point on a RadiantVector to an incoming
// point, project along the ray that passes through the incoming point and
// is perpendicular towards and aimed at this RadiantVector.
// in order to direction of such a ray, rotate this' position vector either
// clockwise or counterclockwise depending on what size of this point is on.
// n.b. rotation formulae are different from +y-up coordinate systems
final sideOfThis = onSideOf(point);
final rotatedPV = sideOfThis == LineSide.left
? Offset(-positionVector.dy, positionVector.dx)
: Offset(positionVector.dy, -positionVector.dx);
// The perpendicular ray is here represented by a RadiantVector. Because
// we only need to go in one direction
final projVector = RadiantVector(point, point + rotatedPV, true);
final projected = intersectWith(projVector);
if (projected == null) {
// If there is no intersection between the perpendicular ray through
// the point and this one, one of the endpoints is closest
if ((point - from).distanceSquared < (point - towards).distanceSquared) {
return from;
} else {
return towards;
}
} else {
return projected;
}
}
}