/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 |