Coverage Report

Created: 2026-08-31 06:51

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/PROJ/src/projections/polyhedral/unfold.h
Line
Count
Source
1
/******************************************************************************
2
 *
3
 * Project:  PROJ
4
 * Purpose:  Unfold a 3D polyhedron Mesh into a planar net Mesh by laying each
5
 *           face flat in its own local 2D frame and chaining 2D rigid
6
 *           transforms over a parents-tree of faces.
7
 * Author:   Felix Palmer
8
 *
9
 ****************************************************************************/
10
11
#ifndef POLYHEDRAL_UNFOLD_H
12
#define POLYHEDRAL_UNFOLD_H
13
14
#include "conway.h"
15
#include "nets/parent_tree.h"
16
#include "vec3.h"
17
18
#include "proj_internal.h"
19
20
#include <cassert>
21
#include <cmath>
22
23
namespace polyhedral {
24
25
// Number of net vertices produced by unfolding NF faces with NFV vertices each
26
0
template <int NF, int NFV> constexpr int unfolded_vertex_count() {
27
0
    return NFV + (NFV - 2) * (NF - 1);
28
0
}
Unexecuted instantiation: int polyhedral::unfolded_vertex_count<4, 3>()
Unexecuted instantiation: int polyhedral::unfolded_vertex_count<12, 5>()
Unexecuted instantiation: int polyhedral::unfolded_vertex_count<20, 3>()
29
30
// For a regular face inscribed on the unit sphere centred at the origin, the
31
// face's outward unit normal is just the centroid direction, so we don't
32
// need a separate face_normal helper.
33
template <int NV, int NFV>
34
6.80k
inline Vec3 face_centroid(const Vec3 (&vertices)[NV], const int (&face)[NFV]) {
35
6.80k
    Vec3 c{0.0, 0.0, 0.0};
36
27.2k
    for (int k = 0; k < NFV; k++)
37
20.4k
        c = vec3_add(c, vertices[face[k]]);
38
6.80k
    return vec3_scale(c, 1.0 / NFV);
39
6.80k
}
Unexecuted instantiation: Vec3 polyhedral::face_centroid<20, 5>(Vec3 const (&) [20], int const (&) [5])
Unexecuted instantiation: Vec3 polyhedral::face_centroid<38, 5>(Vec3 const (&) [38], int const (&) [5])
Vec3 polyhedral::face_centroid<12, 3>(Vec3 const (&) [12], int const (&) [3])
Line
Count
Source
34
6.48k
inline Vec3 face_centroid(const Vec3 (&vertices)[NV], const int (&face)[NFV]) {
35
6.48k
    Vec3 c{0.0, 0.0, 0.0};
36
25.9k
    for (int k = 0; k < NFV; k++)
37
19.4k
        c = vec3_add(c, vertices[face[k]]);
38
6.48k
    return vec3_scale(c, 1.0 / NFV);
39
6.48k
}
Vec3 polyhedral::face_centroid<22, 3>(Vec3 const (&) [22], int const (&) [3])
Line
Count
Source
34
324
inline Vec3 face_centroid(const Vec3 (&vertices)[NV], const int (&face)[NFV]) {
35
324
    Vec3 c{0.0, 0.0, 0.0};
36
1.29k
    for (int k = 0; k < NFV; k++)
37
972
        c = vec3_add(c, vertices[face[k]]);
38
324
    return vec3_scale(c, 1.0 / NFV);
39
324
}
Unexecuted instantiation: Vec3 polyhedral::face_centroid<4, 3>(Vec3 const (&) [4], int const (&) [3])
Unexecuted instantiation: Vec3 polyhedral::face_centroid<6, 3>(Vec3 const (&) [6], int const (&) [3])
40
41
// Rigid transform from a polyhedron face's plane to the 2D plane (z=0),
42
// sending poly_a → net_a and orienting poly_a→poly_b along net_a→net_b.
43
// poly_a and poly_b must lie on the face plane; only the direction of
44
// net_b − net_a is used (the 2D distance is derived from the 3D one).
45
inline Mat4 mat4_rigid_transform(const Vec3 &normal, const Vec3 &poly_a,
46
                                 const Vec3 &net_a, const Vec3 &poly_b,
47
6.48k
                                 const Vec3 &net_b) {
48
6.48k
    const Vec3 u = vec3_normalize(vec3_subtract(poly_b, poly_a));
49
6.48k
    const Vec3 v = vec3_cross(normal, u);
50
6.48k
    const Vec3 edge_direction = vec3_normalize(vec3_subtract(net_b, net_a));
51
6.48k
    const Vec3 r0 = vec3_subtract(vec3_scale(u, edge_direction.x),
52
6.48k
                                  vec3_scale(v, edge_direction.y));
53
6.48k
    const Vec3 r1 = vec3_add(vec3_scale(u, edge_direction.y),
54
6.48k
                             vec3_scale(v, edge_direction.x));
55
6.48k
    const double t0 = net_a.x - vec3_dot(r0, poly_a);
56
6.48k
    const double t1 = net_a.y - vec3_dot(r1, poly_a);
57
6.48k
    return {{{r0.x, r0.y, r0.z, t0},
58
6.48k
             {r1.x, r1.y, r1.z, t1},
59
6.48k
             {0.0, 0.0, 0.0, 0.0},
60
6.48k
             {0.0, 0.0, 0.0, 1.0}}};
61
6.48k
}
62
63
template <int NFV>
64
6.48k
inline double polygon_area_2d(const Vec3 *verts, const int (&face)[NFV]) {
65
6.48k
    double a = 0.0;
66
25.9k
    for (int k = 0; k < NFV; k++)
67
19.4k
        a += vec3_cross(verts[face[k]], verts[face[(k + 1) % NFV]]).z;
68
6.48k
    return std::fabs(a) * 0.5;
69
6.48k
}
Unexecuted instantiation: double polyhedral::polygon_area_2d<5>(Vec3 const*, int const (&) [5])
double polyhedral::polygon_area_2d<3>(Vec3 const*, int const (&) [3])
Line
Count
Source
64
6.48k
inline double polygon_area_2d(const Vec3 *verts, const int (&face)[NFV]) {
65
6.48k
    double a = 0.0;
66
25.9k
    for (int k = 0; k < NFV; k++)
67
19.4k
        a += vec3_cross(verts[face[k]], verts[face[(k + 1) % NFV]]).z;
68
6.48k
    return std::fabs(a) * 0.5;
69
6.48k
}
70
71
// Unit vector in Z
72
inline constexpr Vec3 z{0.0, 0.0, 1.0};
73
74
// Unfold a polyhedron Mesh into a planar net Mesh.
75
//
76
// Faces are referenced by 1-based IDs F1..F_NF (matching Snyder's paper
77
// numbering). `parents[i]` holds the parent of face F(i+1) in the unfolding
78
// tree as a 1-based face ID, or 0 if F(i+1) is the root. Exactly one entry
79
// must be 0. The C array indexing stays 0-based; only the values it stores
80
// are 1-based.
81
//
82
// The polyhedron faces must have consistent CCW winding from the outside.
83
// Each child face is hinged about its shared edge with its parent; the
84
// algorithm relies on shared edges being traversed in opposite directions
85
// in the two adjacent faces (which is the case for any closed polyhedron
86
// with consistent outward winding).
87
//
88
// The resulting net is auto-scaled so its total area equals 4π (i.e. the
89
// area of the unit sphere) and translated so the root face's centroid lies
90
// at the origin. The root face is laid out "north-up": geographic north
91
// (+z), projected onto its face plane, points along +y; for a root face
92
// centred on the rotation axis the slot[0] vertex is placed up instead.
93
template <int NV_p, int NF, int NFV>
94
inline Mesh<unfolded_vertex_count<NF, NFV>(), NF, NFV>
95
unfold_net(const Mesh<NV_p, NF, NFV> &polyhedron,
96
324
           const nets::ParentTree<NF> &parents, const Vec3 &up_dir = z) {
97
324
    constexpr int NV_n = unfolded_vertex_count<NF, NFV>();
98
324
    Mesh<NV_n, NF, NFV> net{};
99
324
    int n_verts = 0;
100
101
    // Find and place root face (the entry whose 1-based parent ID is 0).
102
324
    int root = 0;
103
2.59k
    while (parents[root] != 0)
104
2.26k
        root++;
105
106
    // Place the root face "up-direction-up": project up_dir onto the root
107
    // face's plane, then orient so that direction lands on +y. up_dir is
108
    // typically geographic north expressed in polyhedron coordinates (the
109
    // third column of the orient matrix), so the root face comes out with
110
    // true geographic north up. Polar fallback (face on the rotation
111
    // axis): aim from the centroid out toward slot[0] instead.
112
113
    // Destination in 2d net (origin and unit step in +y)
114
324
    const Vec3 origin_net = {0.0, 0.0, 0.0};
115
324
    const Vec3 offset_net = {0.0, 1.0, 0.0};
116
117
324
    const Vec3 origin_poly =
118
324
        face_centroid(polyhedron.vertices, polyhedron.faces[root]);
119
324
    const Vec3 normal = vec3_normalize(origin_poly);
120
121
    // Vector in up_dir direction constrained to the face plane
122
324
    const Vec3 step_poly =
123
324
        vec3_subtract(up_dir, vec3_scale(normal, vec3_dot(up_dir, normal)));
124
324
    Vec3 offset_poly;
125
324
    if (vec3_length(step_poly) < 1e-12)
126
        // Fallback to 1st vertex at pole
127
0
        offset_poly = polyhedron.vertices[polyhedron.faces[root][0]];
128
324
    else
129
324
        offset_poly = vec3_add(origin_poly, step_poly);
130
131
324
    const Mat4 root_face_to_plane = mat4_rigid_transform(
132
324
        normal, origin_poly, origin_net, offset_poly, offset_net);
133
1.29k
    for (int k = 0; k < NFV; k++) {
134
972
        Vec3 face_vertex = polyhedron.vertices[polyhedron.faces[root][k]];
135
972
        net.vertices[n_verts] =
136
972
            vec3_apply_mat4(root_face_to_plane, face_vertex);
137
972
        net.faces[root][k] = n_verts++;
138
972
    }
139
140
    // Initialize queue with direct children of parent face. `root` is a
141
    // 0-based array index; `parents[i]` is a 1-based face ID.
142
324
    int queue[NF];
143
324
    int q_head = 0, q_tail = 0;
144
6.80k
    for (int i = 0; i < NF; i++) {
145
6.48k
        if (parents[i] == root + 1)
146
972
            queue[q_tail++] = i;
147
6.48k
    }
148
149
    // Hinge each child onto its parent at the shared edge.
150
6.48k
    while (q_head < q_tail) {
151
6.15k
        const int c = queue[q_head++];
152
6.15k
        const int p = parents[c] - 1;
153
154
        // Find shared edge
155
6.15k
        int ia_c = -1, ib_c = -1, ia_p = -1, ib_p = -1;
156
24.6k
        for (int kc = 0; kc < NFV; kc++) {
157
73.8k
            for (int kp = 0; kp < NFV; kp++) {
158
55.4k
                if (polyhedron.faces[c][kc] == polyhedron.faces[p][kp]) {
159
12.3k
                    if (ia_c == -1) {
160
6.15k
                        ia_c = kc;
161
6.15k
                        ia_p = kp;
162
6.15k
                    } else if (ib_c == -1) {
163
6.15k
                        ib_c = kc;
164
6.15k
                        ib_p = kp;
165
6.15k
                    }
166
12.3k
                }
167
55.4k
            }
168
18.4k
        }
169
6.15k
        assert(ia_c >= 0 && ib_c >= 0 && ia_p >= 0 && ib_p >= 0);
170
171
        // Place face onto net using shared edge for orientation
172
6.15k
        const int na = net.faces[p][ia_p];
173
6.15k
        const int nb = net.faces[p][ib_p];
174
6.15k
        const Vec3 normal_c = vec3_normalize(
175
6.15k
            face_centroid(polyhedron.vertices, polyhedron.faces[c]));
176
6.15k
        const Mat4 face_to_plane = mat4_rigid_transform(
177
6.15k
            normal_c, polyhedron.vertices[polyhedron.faces[c][ia_c]],
178
6.15k
            net.vertices[na], polyhedron.vertices[polyhedron.faces[c][ib_c]],
179
6.15k
            net.vertices[nb]);
180
24.6k
        for (int k = 0; k < NFV; k++) {
181
18.4k
            if (k == ia_c) {
182
6.15k
                net.faces[c][k] = na;
183
12.3k
            } else if (k == ib_c) {
184
6.15k
                net.faces[c][k] = nb;
185
6.15k
            } else {
186
6.15k
                Vec3 face_vertex = polyhedron.vertices[polyhedron.faces[c][k]];
187
6.15k
                net.vertices[n_verts] =
188
6.15k
                    vec3_apply_mat4(face_to_plane, face_vertex);
189
6.15k
                net.faces[c][k] = n_verts++;
190
6.15k
            }
191
18.4k
        }
192
193
        // Append children of this face to queue (parents[i] is 1-based).
194
129k
        for (int i = 0; i < NF; i++) {
195
123k
            if (parents[i] == c + 1)
196
5.18k
                queue[q_tail++] = i;
197
123k
        }
198
6.15k
    }
199
200
    // Rescale so the net's total area equals the unit sphere's (4π).
201
324
    double total_area = 0.0;
202
6.80k
    for (int f = 0; f < NF; f++)
203
6.48k
        total_area += polygon_area_2d(net.vertices, net.faces[f]);
204
324
    const double scale = std::sqrt(4.0 * M_PI / total_area);
205
206
    // Translate so the root face's centroid is at origin, then scale.
207
324
    const Vec3 centroid = face_centroid(net.vertices, net.faces[root]);
208
7.45k
    for (int i = 0; i < NV_n; i++)
209
7.12k
        net.vertices[i] =
210
7.12k
            vec3_scale(vec3_subtract(net.vertices[i], centroid), scale);
211
212
324
    return net;
213
324
}
Unexecuted instantiation: polyhedral::Mesh<(unfolded_vertex_count<12, 5>)(), 12, 5> polyhedral::unfold_net<20, 12, 5>(polyhedral::Mesh<20, 12, 5> const&, nets::ParentTree<12> const&, Vec3 const&)
polyhedral::Mesh<(unfolded_vertex_count<20, 3>)(), 20, 3> polyhedral::unfold_net<12, 20, 3>(polyhedral::Mesh<12, 20, 3> const&, nets::ParentTree<20> const&, Vec3 const&)
Line
Count
Source
96
324
           const nets::ParentTree<NF> &parents, const Vec3 &up_dir = z) {
97
324
    constexpr int NV_n = unfolded_vertex_count<NF, NFV>();
98
324
    Mesh<NV_n, NF, NFV> net{};
99
324
    int n_verts = 0;
100
101
    // Find and place root face (the entry whose 1-based parent ID is 0).
102
324
    int root = 0;
103
2.59k
    while (parents[root] != 0)
104
2.26k
        root++;
105
106
    // Place the root face "up-direction-up": project up_dir onto the root
107
    // face's plane, then orient so that direction lands on +y. up_dir is
108
    // typically geographic north expressed in polyhedron coordinates (the
109
    // third column of the orient matrix), so the root face comes out with
110
    // true geographic north up. Polar fallback (face on the rotation
111
    // axis): aim from the centroid out toward slot[0] instead.
112
113
    // Destination in 2d net (origin and unit step in +y)
114
324
    const Vec3 origin_net = {0.0, 0.0, 0.0};
115
324
    const Vec3 offset_net = {0.0, 1.0, 0.0};
116
117
324
    const Vec3 origin_poly =
118
324
        face_centroid(polyhedron.vertices, polyhedron.faces[root]);
119
324
    const Vec3 normal = vec3_normalize(origin_poly);
120
121
    // Vector in up_dir direction constrained to the face plane
122
324
    const Vec3 step_poly =
123
324
        vec3_subtract(up_dir, vec3_scale(normal, vec3_dot(up_dir, normal)));
124
324
    Vec3 offset_poly;
125
324
    if (vec3_length(step_poly) < 1e-12)
126
        // Fallback to 1st vertex at pole
127
0
        offset_poly = polyhedron.vertices[polyhedron.faces[root][0]];
128
324
    else
129
324
        offset_poly = vec3_add(origin_poly, step_poly);
130
131
324
    const Mat4 root_face_to_plane = mat4_rigid_transform(
132
324
        normal, origin_poly, origin_net, offset_poly, offset_net);
133
1.29k
    for (int k = 0; k < NFV; k++) {
134
972
        Vec3 face_vertex = polyhedron.vertices[polyhedron.faces[root][k]];
135
972
        net.vertices[n_verts] =
136
972
            vec3_apply_mat4(root_face_to_plane, face_vertex);
137
972
        net.faces[root][k] = n_verts++;
138
972
    }
139
140
    // Initialize queue with direct children of parent face. `root` is a
141
    // 0-based array index; `parents[i]` is a 1-based face ID.
142
324
    int queue[NF];
143
324
    int q_head = 0, q_tail = 0;
144
6.80k
    for (int i = 0; i < NF; i++) {
145
6.48k
        if (parents[i] == root + 1)
146
972
            queue[q_tail++] = i;
147
6.48k
    }
148
149
    // Hinge each child onto its parent at the shared edge.
150
6.48k
    while (q_head < q_tail) {
151
6.15k
        const int c = queue[q_head++];
152
6.15k
        const int p = parents[c] - 1;
153
154
        // Find shared edge
155
6.15k
        int ia_c = -1, ib_c = -1, ia_p = -1, ib_p = -1;
156
24.6k
        for (int kc = 0; kc < NFV; kc++) {
157
73.8k
            for (int kp = 0; kp < NFV; kp++) {
158
55.4k
                if (polyhedron.faces[c][kc] == polyhedron.faces[p][kp]) {
159
12.3k
                    if (ia_c == -1) {
160
6.15k
                        ia_c = kc;
161
6.15k
                        ia_p = kp;
162
6.15k
                    } else if (ib_c == -1) {
163
6.15k
                        ib_c = kc;
164
6.15k
                        ib_p = kp;
165
6.15k
                    }
166
12.3k
                }
167
55.4k
            }
168
18.4k
        }
169
6.15k
        assert(ia_c >= 0 && ib_c >= 0 && ia_p >= 0 && ib_p >= 0);
170
171
        // Place face onto net using shared edge for orientation
172
6.15k
        const int na = net.faces[p][ia_p];
173
6.15k
        const int nb = net.faces[p][ib_p];
174
6.15k
        const Vec3 normal_c = vec3_normalize(
175
6.15k
            face_centroid(polyhedron.vertices, polyhedron.faces[c]));
176
6.15k
        const Mat4 face_to_plane = mat4_rigid_transform(
177
6.15k
            normal_c, polyhedron.vertices[polyhedron.faces[c][ia_c]],
178
6.15k
            net.vertices[na], polyhedron.vertices[polyhedron.faces[c][ib_c]],
179
6.15k
            net.vertices[nb]);
180
24.6k
        for (int k = 0; k < NFV; k++) {
181
18.4k
            if (k == ia_c) {
182
6.15k
                net.faces[c][k] = na;
183
12.3k
            } else if (k == ib_c) {
184
6.15k
                net.faces[c][k] = nb;
185
6.15k
            } else {
186
6.15k
                Vec3 face_vertex = polyhedron.vertices[polyhedron.faces[c][k]];
187
6.15k
                net.vertices[n_verts] =
188
6.15k
                    vec3_apply_mat4(face_to_plane, face_vertex);
189
6.15k
                net.faces[c][k] = n_verts++;
190
6.15k
            }
191
18.4k
        }
192
193
        // Append children of this face to queue (parents[i] is 1-based).
194
129k
        for (int i = 0; i < NF; i++) {
195
123k
            if (parents[i] == c + 1)
196
5.18k
                queue[q_tail++] = i;
197
123k
        }
198
6.15k
    }
199
200
    // Rescale so the net's total area equals the unit sphere's (4π).
201
324
    double total_area = 0.0;
202
6.80k
    for (int f = 0; f < NF; f++)
203
6.48k
        total_area += polygon_area_2d(net.vertices, net.faces[f]);
204
324
    const double scale = std::sqrt(4.0 * M_PI / total_area);
205
206
    // Translate so the root face's centroid is at origin, then scale.
207
324
    const Vec3 centroid = face_centroid(net.vertices, net.faces[root]);
208
7.45k
    for (int i = 0; i < NV_n; i++)
209
7.12k
        net.vertices[i] =
210
7.12k
            vec3_scale(vec3_subtract(net.vertices[i], centroid), scale);
211
212
324
    return net;
213
324
}
Unexecuted instantiation: polyhedral::Mesh<(unfolded_vertex_count<4, 3>)(), 4, 3> polyhedral::unfold_net<4, 4, 3>(polyhedral::Mesh<4, 4, 3> const&, nets::ParentTree<4> const&, Vec3 const&)
214
215
} // namespace polyhedral
216
217
#endif // POLYHEDRAL_UNFOLD_H