summaryrefslogtreecommitdiff
path: root/src/text/rotation_range.cpp
blob: 943253a3da63b530cce87d277bf79233e705562c (plain)
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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#include <mbgl/text/rotation_range.hpp>

#include <cassert>
#include <algorithm>

namespace mbgl {

/*
 * Combine an array of collision ranges to form a continuous
 * range that includes 0. Collisions within the ignoreRange are ignored
 */
CollisionRange mergeCollisions(const CollisionList &collisions,
                               PlacementRange ignoreRange) {
    // find continuous interval including 0 that doesn't have any collisions
    float min = 2.0f * M_PI;
    float max = 0.0f;

    for (CollisionRange collision : collisions) {
        bool entryOutside =
            ignoreRange[0] <= collision[0] && collision[0] <= ignoreRange[1];
        bool exitOutside =
            ignoreRange[0] <= collision[1] && collision[1] <= ignoreRange[1];

        if (entryOutside && exitOutside) {
            // no collision, since blocker is out of range
        } else if (entryOutside) {
            min = util::min(min, ignoreRange[1]);
            max = util::max(max, collision[1]);
        } else if (exitOutside) {
            min = util::min(min, collision[0]);
            max = util::max(max, ignoreRange[0]);
        } else {
            min = util::min(min, collision[0]);
            max = util::max(max, collision[1]);
        }
    }

    return {{min, max}};
}

/*
 *  Calculate collision ranges for two rotating boxes.
 */
CollisionList
rotatingRotatingCollisions(const CollisionRect &a, const CollisionRect &b,
                           const CollisionAnchor &anchorToAnchor) {
    const float d = util::mag<float>(anchorToAnchor);
    const float d_sq = d * d;

    const CollisionAnchor horizontal = {1, 0};
    const float angleBetweenAnchors =
        util::angle_between<float>(anchorToAnchor, horizontal);

    // Calculate angles at which collisions may occur
    const std::array<float, 8> c = {{
        // top/bottom
        /*[0]*/ static_cast<float>(std::asin((float)(a.br.y - b.tl.y) / d)),
        /*[1]*/ static_cast<float>(std::asin((float)(a.br.y - b.tl.y) / d) + M_PI),
        /*[2]*/ static_cast<float>(2 * M_PI -
                                   std::asin((float)(-a.tl.y + b.br.y) / d)),
        /*[3]*/ static_cast<float>(M_PI - std::asin((float)(-a.tl.y + b.br.y) / d)),

        // left/right
        /*[4]*/ static_cast<float>(2 * M_PI -
                                   std::acos((float)(a.br.x - b.tl.x) / d)),
        /*[5]*/ static_cast<float>(std::acos((float)(a.br.x - b.tl.x) / d)),
        /*[6]*/ static_cast<float>(M_PI - std::acos((float)(-a.tl.x + b.br.x) / d)),
        /*[7]*/ static_cast<float>(M_PI +
                                   std::acos((float)(-a.tl.x + b.br.x) / d))}};

    const float rl = a.br.x - b.tl.x;
    const float lr = -a.tl.x + b.br.x;
    const float tb = a.br.y - b.tl.y;
    const float bt = -a.tl.y + b.br.y;

    // Calculate the distance squared of the diagonal which will be used
    // to check if the boxes are close enough for collisions to occur at each
    // angle
    // todo, triple check these
    const std::array<float, 8> e = {{
        // top/bottom
        /*[0]*/ static_cast<float>(rl * rl + tb * tb),
        /*[1]*/ static_cast<float>(lr * lr + tb * tb),
        /*[2]*/ static_cast<float>(rl * rl + bt * bt),
        /*[3]*/ static_cast<float>(lr * lr + bt * bt),

        // left/right
        /*[4]*/ static_cast<float>(rl * rl + tb * tb),
        /*[5]*/ static_cast<float>(rl * rl + bt * bt),
        /*[6]*/ static_cast<float>(lr * lr + bt * bt),
        /*[7]*/ static_cast<float>(lr * lr + tb * tb)}};

    std::vector<float> f;
    for (size_t i = 0; i < c.size(); i++) {
        // Check if they are close enough to collide
        if (!isnan(c[i]) && d_sq <= e[i]) {
            // So far, angles have been calulated as relative to the vector
            // between anchors.
            // Convert the angles to angles from north.
            f.push_back(
                std::fmod((c[i] + angleBetweenAnchors + 2 * M_PI), (2 * M_PI)));
        }
    }

    assert(f.size() % 2 == 0);

    // Group the collision angles by two
    // each group represents a range where the two boxes collide
    CollisionList collisions;
    std::sort(f.begin(), f.end());
    for (size_t k = 0; k < f.size(); k += 2) {
        collisions.push_back({{f[k], f[k + 1]}});
    }

    return collisions;
}

double getAngle(const CollisionPoint &p1, const CollisionPoint &p2,
                CollisionAngle d, const CollisionPoint &corner) {
    return std::fmod(util::angle_between(util::interp(p1.x, p2.x, d),
                                    util::interp(p1.y, p2.y, d), corner.x,
                                    corner.y) +
                    2 * M_PI,
                2 * M_PI);
}

/*
 * Return the intersection points of a circle and a line segment;
 */
void circleEdgeCollisions(std::back_insert_iterator<CollisionAngles> angles,
                          const CollisionPoint &corner, float radius,
                          const CollisionPoint &p1, const CollisionPoint &p2) {
    const CollisionPoint::Type edgeX = p2.x - p1.x;
    const CollisionPoint::Type edgeY = p2.y - p1.y;

    const CollisionAngle a = edgeX * edgeX + edgeY * edgeY;
    const CollisionAngle b = (edgeX * p1.x + edgeY * p1.y) * 2;
    const CollisionAngle c = p1.x * p1.x + p1.y * p1.y - radius * radius;

    const CollisionAngle discriminant = b * b - 4 * a * c;

    // a collision exists only if line intersects circle at two points
    if (discriminant > 0) {
        CollisionAngle x1 = (-b - std::sqrt(discriminant)) / (2 * a);
        CollisionAngle x2 = (-b + std::sqrt(discriminant)) / (2 * a);

        // only add points if within line segment
        // hack to handle floating point representations of 0 and 1
        if (0 < x1 && x1 < 1) {
            angles = getAngle(p1, p2, x1, corner);
        }

        if (0 < x2 && x2 < 1) {
            angles = getAngle(p1, p2, x2, corner);
        }
    }
}

/*
 *  Calculate the ranges for which the corner,
 *  rotatated around the anchor, is within the box;
 */
void cornerBoxCollisions(std::back_insert_iterator<CollisionList> collisions,
                         const CollisionPoint &corner,
                         const CollisionCorners &boxCorners, bool flip) {
    float radius = util::mag<float>(corner);

    CollisionAngles angles;

    // Calculate the points at which the corners intersect with the edges
    for (size_t i = 0, j = 3; i < 4; j = i++) {
        circleEdgeCollisions(std::back_inserter(angles), corner, radius,
                             boxCorners[j], boxCorners[i]);
    }

    if (angles.size() % 2 != 0) {
        // TODO fix
        // This could get hit when a point intersects very close to a corner
        // and floating point issues cause only one of the entry or exit to be
        // counted
        throw std::runtime_error("expecting an even number of intersections");
    }

    std::sort(angles.begin(), angles.end());

    // Group by pairs, where each represents a range where a collision occurs
    for (size_t k = 0; k < angles.size(); k += 2) {
        CollisionRange range = {{angles[k], angles[k + 1]}};
        if (flip) {
            range = util::flip(range);
        }
        collisions = range;
    }
}

CollisionCorners getCorners(const CollisionRect &a) {
    return {{{a.tl.x, a.tl.y},
             {a.tl.x, a.br.y},
             {a.br.x, a.br.y},
             {a.br.x, a.tl.y}}};
}

/*
 *  Calculate collision ranges for a rotating box and a fixed box;
 */
CollisionList rotatingFixedCollisions(const CollisionRect &rotating,
                                      const CollisionRect &fixed) {
    const auto cornersR = getCorners(rotating);
    const auto cornersF = getCorners(fixed);

    // A collision occurs when, and only at least one corner from one of the
    // boxes is within the other box. Calculate these ranges for each corner.

    CollisionList collisions;

    for (size_t i = 0; i < 4; i++) {
        cornerBoxCollisions(std::back_inserter(collisions), cornersR[i],
                            cornersF);
        cornerBoxCollisions(std::back_inserter(collisions), cornersF[i],
                            cornersR, true);
    }

    return collisions;
}

/*
 * Calculate the range a box conflicts with a second box
 */
CollisionRange rotationRange(const GlyphBox &inserting,
                             const PlacementBox &blocker, float scale) {
    CollisionList collisions;

    const GlyphBox &a = inserting;
    const PlacementBox &b = blocker;

    // Instead of scaling the boxes, we move the anchors
    CollisionAnchor relativeAnchor{
        static_cast<float>((b.anchor.x - a.anchor.x) * scale),
        static_cast<float>((b.anchor.y - a.anchor.y) * scale)};

    // Generate a list of collision interval
    if (a.rotate && b.rotate) {
        collisions = rotatingRotatingCollisions(a.box, b.box, relativeAnchor);
    } else if (a.rotate) {
        const CollisionRect box {
            b.box.tl.x + relativeAnchor.x, b.box.tl.y + relativeAnchor.y,
            b.box.br.x + relativeAnchor.x, b.box.br.y + relativeAnchor.y};
        collisions = rotatingFixedCollisions(a.box, box);
    } else if (b.rotate) {
        const CollisionRect box {
            a.box.tl.x - relativeAnchor.x, a.box.tl.y - relativeAnchor.y,
            a.box.br.x - relativeAnchor.x, a.box.br.y - relativeAnchor.y};
        collisions = rotatingFixedCollisions(b.box, box);
    } else {
        // collisions remains empty
    }

    // Find and return the continous are around 0 where there are no collisions
    return mergeCollisions(collisions, blocker.placementRange);
}
}