Skip to content

Commit 46ef6b9

Browse files
ajfriendholoskii
andauthored
Vec3d indexing refactor (#1145)
* Add Vec3d math functions: dot, cross, normalize, mag, distSq, latLngToVec3 From #1052. * Refactor faceijk.c: Vec3d as core coordinate type Extract _vec3dToClosestFace, _vec3dToHex2d, _vec3dToFaceIjk from LatLng wrappers. Add _vec3dAzimuthRads (3D tangent-plane azimuth) and _hex2dToVec3 (inverse via Rodrigues' rotation). Add vec3ToCell and cellToVec3 public API. Existing LatLng API is unchanged — wrappers delegate to the new Vec3d functions. From #1052. * Add Vec3d unit tests From #1052. * Route latLngToCell/cellToLatLng through Vec3d; remove dead LatLng-only code * cell boundary code to Vec3d path; remove _hex2dToGeo, _geoAzDistanceRads, and tests * vec3ToCell validation tests * align function names and struct: hex2d -> vec2d * use Vec2 and Vec3 everywhere * normalize * h3 -> cell; years * bench * update bench * correctness * test vertices and along edges * _vec3TangentBasis helper * vec3LinComb helper * return by value * pass by value * clean * test for inlining * header-only; suffering on LTO * header only * clean up benchmark script * _vec3ToFaceIjk * output params * header version of _ijkToVec2 * revert * revert * redo * inline ijk ops * going header only * directedEdgeToBoundary bench * justfile * revert coorijk header change * comments * update benchmarks * again * reduce digits on testCliEdgeLengthM * vertexToLatLng benchmark * cover line in latLng.c * remove temporary benchmarks and assignment tests * _faceIjkToCell back to _faceIjkToH3 * restore Vec3d * missed a few * test files * vec2d.h * Vec2d * vec2 names * more vec2 names * names * comments * years * years * boop * years * old test * haven't i done this one already? * _hex2dToVec3 * pass output by reference * _vec3ToHex2d * forward declare * comment * bah * bah bah * move around _vec3ToClosestFace * todo * bring benchmarks back in * nits * pass by value * expression * _vec3TangentBasis * nits * boop * i swear... * boop * 9 digits * move * 8 digits * comments align * better names for vec3LinComb * dead code: remove faceCenterGeo definition * remove benchmarks and justfile * camel_case * fix more snakeCase * small norm handling * static void _hex2dToVec3 * _faceIjkToVec3 g * fix doc comment on _faceIjkToVec3 * avoid Vec3d casts * unit vector comments * add vec3ToCell tests --------- Co-authored-by: Makar Ivashko <1212makar1212@gmail.com>
1 parent 9257661 commit 46ef6b9

16 files changed

Lines changed: 429 additions & 346 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,6 @@ set(LIB_SOURCE_FILES
180180
src/h3lib/lib/polyfill.c
181181
src/h3lib/lib/h3Index.c
182182
src/h3lib/lib/vec2d.c
183-
src/h3lib/lib/vec3d.c
184183
src/h3lib/lib/vertex.c
185184
src/h3lib/lib/linkedGeo.c
186185
src/h3lib/lib/localij.c
@@ -255,6 +254,7 @@ set(OTHER_SOURCE_FILES
255254
src/apps/testapps/testPolyfillInternal.c
256255
src/apps/testapps/testVec2dInternal.c
257256
src/apps/testapps/testVec3dInternal.c
257+
src/apps/testapps/testVec3.c
258258
src/apps/testapps/testDirectedEdge.c
259259
src/apps/testapps/testDirectedEdgeExhaustive.c
260260
src/apps/testapps/testLinkedGeoInternal.c

CMakeTests.cmake

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ add_h3_test(testPolygonInternal src/apps/testapps/testPolygonInternal.c)
242242
add_h3_test(testPolyfillInternal src/apps/testapps/testPolyfillInternal.c)
243243
add_h3_test(testVec2dInternal src/apps/testapps/testVec2dInternal.c)
244244
add_h3_test(testVec3dInternal src/apps/testapps/testVec3dInternal.c)
245+
add_h3_test(testVec3 src/apps/testapps/testVec3.c)
245246
add_h3_test(testCellToLocalIj src/apps/testapps/testCellToLocalIj.c)
246247
add_h3_test(testCellToLocalIjInternal
247248
src/apps/testapps/testCellToLocalIjInternal.c)

src/apps/filters/h3.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2642,7 +2642,7 @@ SUBCOMMAND(edgeLengthM,
26422642
if (err) {
26432643
return err;
26442644
}
2645-
printf("%.10lf\n", length);
2645+
printf("%.8lf\n", length);
26462646
return E_SUCCESS;
26472647
}
26482648

src/apps/miscapps/generateFaceCenterPoint.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2018, 2020-2021 Uber Technologies, Inc.
2+
* Copyright 2018, 2020-2021, 2026 Uber Technologies, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -56,8 +56,7 @@ static void generate(void) {
5656
printf("static const Vec3d faceCenterPoint[NUM_ICOSA_FACES] = {\n");
5757
for (int i = 0; i < NUM_ICOSA_FACES; i++) {
5858
LatLng centerCoords = faceCenterGeoCopy[i];
59-
Vec3d centerPoint;
60-
_geoToVec3d(&centerCoords, &centerPoint);
59+
Vec3d centerPoint = latLngToVec3(centerCoords);
6160
printf(" {%.16f, %.16f, %.16f}, // face %2d\n", centerPoint.x,
6261
centerPoint.y, centerPoint.z, i);
6362
}
Lines changed: 3 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2017-2021 Uber Technologies, Inc.
2+
* Copyright 2017-2021, 2026 Uber Technologies, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -64,94 +64,7 @@ SUITE(latLngInternal) {
6464
t_assert(constrainLng(2 * M_PI) == 0, "lng 2pi");
6565
t_assert(constrainLng(3 * M_PI) == M_PI, "lng 2pi");
6666
t_assert(constrainLng(4 * M_PI) == 0, "lng 4pi");
67-
}
68-
69-
TEST(_geoAzDistanceRads_noop) {
70-
LatLng start = {15, 10};
71-
LatLng out;
72-
LatLng expected = {15, 10};
73-
74-
_geoAzDistanceRads(&start, 0, 0, &out);
75-
t_assert(geoAlmostEqual(&expected, &out),
76-
"0 distance produces same point");
77-
}
78-
79-
TEST(_geoAzDistanceRads_dueNorthSouth) {
80-
LatLng start;
81-
LatLng out;
82-
LatLng expected;
83-
84-
// Due north to north pole
85-
setGeoDegs(&start, 45, 1);
86-
setGeoDegs(&expected, 90, 0);
87-
_geoAzDistanceRads(&start, 0, H3_EXPORT(degsToRads)(45), &out);
88-
t_assert(geoAlmostEqual(&expected, &out),
89-
"due north to north pole produces north pole");
90-
91-
// Due north to south pole, which doesn't get wrapped correctly
92-
setGeoDegs(&start, 45, 1);
93-
setGeoDegs(&expected, 270, 1);
94-
_geoAzDistanceRads(&start, 0, H3_EXPORT(degsToRads)(45 + 180), &out);
95-
t_assert(geoAlmostEqual(&expected, &out),
96-
"due north to south pole produces south pole");
97-
98-
// Due south to south pole
99-
setGeoDegs(&start, -45, 2);
100-
setGeoDegs(&expected, -90, 0);
101-
_geoAzDistanceRads(&start, H3_EXPORT(degsToRads)(180),
102-
H3_EXPORT(degsToRads)(45), &out);
103-
t_assert(geoAlmostEqual(&expected, &out),
104-
"due south to south pole produces south pole");
105-
106-
// Due north to non-pole
107-
setGeoDegs(&start, -45, 10);
108-
setGeoDegs(&expected, -10, 10);
109-
_geoAzDistanceRads(&start, 0, H3_EXPORT(degsToRads)(35), &out);
110-
t_assert(geoAlmostEqual(&expected, &out),
111-
"due north produces expected result");
112-
}
113-
114-
TEST(_geoAzDistanceRads_poleToPole) {
115-
LatLng start;
116-
LatLng out;
117-
LatLng expected;
118-
119-
// Azimuth doesn't really matter in this case. Any azimuth from the
120-
// north pole is south, any azimuth from the south pole is north.
121-
122-
setGeoDegs(&start, 90, 0);
123-
setGeoDegs(&expected, -90, 0);
124-
_geoAzDistanceRads(&start, H3_EXPORT(degsToRads)(12),
125-
H3_EXPORT(degsToRads)(180), &out);
126-
t_assert(geoAlmostEqual(&expected, &out),
127-
"some direction to south pole produces south pole");
128-
129-
setGeoDegs(&start, -90, 0);
130-
setGeoDegs(&expected, 90, 0);
131-
_geoAzDistanceRads(&start, H3_EXPORT(degsToRads)(34),
132-
H3_EXPORT(degsToRads)(180), &out);
133-
t_assert(geoAlmostEqual(&expected, &out),
134-
"some direction to north pole produces north pole");
135-
}
136-
137-
TEST(_geoAzDistanceRads_invertible) {
138-
LatLng start;
139-
setGeoDegs(&start, 15, 10);
140-
LatLng out;
141-
142-
double azimuth = H3_EXPORT(degsToRads)(20);
143-
double degrees180 = H3_EXPORT(degsToRads)(180);
144-
double distance = H3_EXPORT(degsToRads)(15);
145-
146-
_geoAzDistanceRads(&start, azimuth, distance, &out);
147-
t_assert(fabs(H3_EXPORT(greatCircleDistanceRads)(&start, &out) -
148-
distance) < EPSILON_RAD,
149-
"moved distance is as expected");
150-
151-
LatLng start2 = out;
152-
_geoAzDistanceRads(&start2, azimuth + degrees180, distance, &out);
153-
// TODO: Epsilon is relatively large
154-
t_assert(H3_EXPORT(greatCircleDistanceRads)(&start, &out) < 0.01,
155-
"moved back to origin");
67+
t_assert(constrainLng(-2 * M_PI) == 0, "lng -2pi");
68+
t_assert(constrainLng(-3 * M_PI) == -M_PI, "lng -3pi");
15669
}
15770
}

src/apps/testapps/testVec3.c

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/*
2+
* Copyright 2026 Uber Technologies, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/** @file testVec3.c
18+
* @brief Tests the Vec3d helpers used by the geodesic polyfill path.
19+
*/
20+
21+
#include <float.h>
22+
#include <math.h>
23+
24+
#include "h3Index.h"
25+
#include "test.h"
26+
#include "vec3d.h"
27+
28+
SUITE(Vec3d) {
29+
TEST(dotProduct) {
30+
Vec3d a = {.x = 1.0, .y = 0.0, .z = 0.0};
31+
Vec3d b = {.x = -1.0, .y = 0.0, .z = 0.0};
32+
t_assert(vec3Dot(a, b) == -1.0, "dot product matches expected value");
33+
}
34+
35+
TEST(crossProductOrthogonality) {
36+
Vec3d i = {.x = 1.0, .y = 0.0, .z = 0.0};
37+
Vec3d j = {.x = 0.0, .y = 1.0, .z = 0.0};
38+
Vec3d k = vec3Cross(i, j);
39+
t_assert(fabs(k.x - 0.0) < DBL_EPSILON, "x component zero");
40+
t_assert(fabs(k.y - 0.0) < DBL_EPSILON, "y component zero");
41+
t_assert(fabs(k.z - 1.0) < DBL_EPSILON, "z component one");
42+
t_assert(fabs(vec3Dot(k, i)) < DBL_EPSILON, "cross is orthogonal to i");
43+
t_assert(fabs(vec3Dot(k, j)) < DBL_EPSILON, "cross is orthogonal to j");
44+
}
45+
46+
TEST(normalizeAndMagnitude) {
47+
Vec3d v = {.x = 3.0, .y = -4.0, .z = 12.0};
48+
double magSq = vec3NormSq(v);
49+
t_assert(fabs(magSq - 169.0) < DBL_EPSILON,
50+
"magnitude squared matches");
51+
t_assert(fabs(vec3Norm(v) - 13.0) < DBL_EPSILON, "magnitude matches");
52+
53+
vec3Normalize(&v);
54+
t_assert(fabs(vec3Norm(v) - 1.0) < DBL_EPSILON,
55+
"normalized vector is unit");
56+
57+
Vec3d zero = {.x = 0.0, .y = 0.0, .z = 0.0};
58+
vec3Normalize(&zero);
59+
t_assert(zero.x == 0.0 && zero.y == 0.0 && zero.z == 0.0,
60+
"zero vector remains unchanged when normalizing");
61+
}
62+
63+
TEST(distance) {
64+
Vec3d a = {.x = 0.0, .y = 0.0, .z = 0.0};
65+
Vec3d b = {.x = 1.0, .y = 2.0, .z = 2.0};
66+
t_assert(fabs(vec3DistSq(a, b) - 9.0) < DBL_EPSILON,
67+
"distance squared matches");
68+
}
69+
70+
TEST(latLngToVec3_unitSphere) {
71+
LatLng geo = {.lat = 0.5, .lng = -1.3};
72+
Vec3d v = latLngToVec3(geo);
73+
t_assert(fabs(vec3Norm(v) - 1.0) < DBL_EPSILON,
74+
"converted vector lives on the unit sphere");
75+
}
76+
77+
TEST(vec3ToCell_invalidRes) {
78+
Vec3d v = {.x = 1.0, .y = 0.0, .z = 0.0};
79+
H3Index out;
80+
t_assert(vec3ToCell(&v, -1, &out) == E_RES_DOMAIN,
81+
"negative resolution is rejected");
82+
t_assert(vec3ToCell(&v, 16, &out) == E_RES_DOMAIN,
83+
"resolution above max is rejected");
84+
}
85+
86+
TEST(cellToVec3_unitSphere) {
87+
// cellToVec3 should return a point on the unit sphere.
88+
LatLng p = {.lat = 0.6, .lng = -1.2};
89+
H3Index h;
90+
t_assertSuccess(H3_EXPORT(latLngToCell)(&p, 5, &h));
91+
92+
Vec3d v;
93+
t_assertSuccess(cellToVec3(h, &v));
94+
t_assert(fabs(vec3Norm(v) - 1.0) < DBL_EPSILON,
95+
"cellToVec3 result is on the unit sphere");
96+
}
97+
98+
TEST(cellToVec3_matchesCellToLatLng) {
99+
// vec3ToLatLng(cellToVec3(cell)) should agree with cellToLatLng.
100+
LatLng p = {.lat = 0.3, .lng = 2.1};
101+
H3Index h;
102+
t_assertSuccess(H3_EXPORT(latLngToCell)(&p, 7, &h));
103+
104+
Vec3d v;
105+
t_assertSuccess(cellToVec3(h, &v));
106+
LatLng fromVec3 = vec3ToLatLng(v);
107+
108+
LatLng fromCell;
109+
t_assertSuccess(H3_EXPORT(cellToLatLng)(h, &fromCell));
110+
111+
t_assert(fabs(fromVec3.lat - fromCell.lat) < DBL_EPSILON,
112+
"lat matches cellToLatLng");
113+
t_assert(fabs(fromVec3.lng - fromCell.lng) < DBL_EPSILON,
114+
"lng matches cellToLatLng");
115+
}
116+
117+
TEST(cellToVec3_roundTrip) {
118+
// vec3ToCell(cellToVec3(cell)) should return the same cell.
119+
LatLng p = {.lat = -0.4, .lng = 0.8};
120+
H3Index h;
121+
t_assertSuccess(H3_EXPORT(latLngToCell)(&p, 9, &h));
122+
123+
Vec3d v;
124+
t_assertSuccess(cellToVec3(h, &v));
125+
126+
H3Index h2;
127+
t_assertSuccess(vec3ToCell(&v, 9, &h2));
128+
t_assert(h2 == h, "round-trip through Vec3d returns same cell");
129+
}
130+
131+
TEST(cellToVec3_invalidCell) {
132+
Vec3d v;
133+
t_assert(cellToVec3(0x7fffffffffffffff, &v) == E_CELL_INVALID,
134+
"invalid cell gives E_CELL_INVALID");
135+
}
136+
137+
TEST(vec3ToCell_nonFinite) {
138+
H3Index out;
139+
Vec3d nanX = {.x = NAN, .y = 0.0, .z = 0.0};
140+
t_assert(vec3ToCell(&nanX, 0, &out) == E_DOMAIN, "NaN x is rejected");
141+
Vec3d infY = {.x = 0.0, .y = INFINITY, .z = 0.0};
142+
t_assert(vec3ToCell(&infY, 0, &out) == E_DOMAIN,
143+
"infinite y is rejected");
144+
Vec3d infZ = {.x = 0.0, .y = 0.0, .z = -INFINITY};
145+
t_assert(vec3ToCell(&infZ, 0, &out) == E_DOMAIN,
146+
"infinite z is rejected");
147+
}
148+
}
Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2018, 2020-2021 Uber Technologies, Inc.
2+
* Copyright 2018, 2020-2021, 2026 Uber Technologies, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,50 +16,70 @@
1616

1717
#include <float.h>
1818
#include <math.h>
19-
#include <stdlib.h>
2019

2120
#include "test.h"
2221
#include "vec3d.h"
2322

2423
SUITE(Vec3dInternal) {
25-
TEST(_pointSquareDist) {
24+
TEST(vec3DistSq) {
2625
Vec3d v1 = {0, 0, 0};
2726
Vec3d v2 = {1, 0, 0};
2827
Vec3d v3 = {0, 1, 1};
2928
Vec3d v4 = {1, 1, 1};
3029
Vec3d v5 = {1, 1, 2};
3130

32-
t_assert(fabs(_pointSquareDist(&v1, &v1)) < DBL_EPSILON,
31+
t_assert(fabs(vec3DistSq(v1, v1)) < DBL_EPSILON,
3332
"distance to self is 0");
34-
t_assert(fabs(_pointSquareDist(&v1, &v2) - 1) < DBL_EPSILON,
33+
t_assert(fabs(vec3DistSq(v1, v2) - 1) < DBL_EPSILON,
3534
"distance to <1,0,0> is 1");
36-
t_assert(fabs(_pointSquareDist(&v1, &v3) - 2) < DBL_EPSILON,
35+
t_assert(fabs(vec3DistSq(v1, v3) - 2) < DBL_EPSILON,
3736
"distance to <0,1,1> is 2");
38-
t_assert(fabs(_pointSquareDist(&v1, &v4) - 3) < DBL_EPSILON,
37+
t_assert(fabs(vec3DistSq(v1, v4) - 3) < DBL_EPSILON,
3938
"distance to <1,1,1> is 3");
40-
t_assert(fabs(_pointSquareDist(&v1, &v5) - 6) < DBL_EPSILON,
39+
t_assert(fabs(vec3DistSq(v1, v5) - 6) < DBL_EPSILON,
4140
"distance to <1,1,2> is 6");
4241
}
4342

44-
TEST(_geoToVec3d) {
43+
TEST(vec3Normalize_smallNonzero) {
44+
// 1e-163 squared underflows to 0, so norm == 0.
45+
// vec3Normalize should produce the zero vector.
46+
Vec3d v = {1e-163, 0, 0};
47+
48+
t_assert(v.x != 0.0, "vector is nonzero");
49+
t_assert(vec3Norm(v) == 0.0, "norm underflows to zero");
50+
51+
vec3Normalize(&v);
52+
t_assert(v.x == 0.0 && v.y == 0.0 && v.z == 0.0,
53+
"underflowed vector normalizes to zero");
54+
}
55+
56+
TEST(vec3Normalize_dblEpsilonHalf) {
57+
// DBL_EPSILON/2 is small but normalizes fine.
58+
Vec3d v = {DBL_EPSILON / 2.0, 0, 0};
59+
60+
t_assert(vec3Norm(v) < DBL_EPSILON, "norm is small but nonzero");
61+
62+
vec3Normalize(&v);
63+
t_assert(fabs(v.x - 1.0) < DBL_EPSILON && v.y == 0 && v.z == 0,
64+
"still normalizable to unit vector");
65+
}
66+
67+
TEST(latLngToVec3) {
4568
Vec3d origin = {0};
4669

4770
LatLng c1 = {0, 0};
48-
Vec3d p1;
49-
_geoToVec3d(&c1, &p1);
50-
t_assert(fabs(_pointSquareDist(&origin, &p1) - 1) < EPSILON_RAD,
71+
Vec3d p1 = latLngToVec3(c1);
72+
t_assert(fabs(vec3DistSq(origin, p1) - 1) < EPSILON_RAD,
5173
"Geo point is on the unit sphere");
5274

5375
LatLng c2 = {M_PI_2, 0};
54-
Vec3d p2;
55-
_geoToVec3d(&c2, &p2);
56-
t_assert(fabs(_pointSquareDist(&p1, &p2) - 2) < EPSILON_RAD,
76+
Vec3d p2 = latLngToVec3(c2);
77+
t_assert(fabs(vec3DistSq(p1, p2) - 2) < EPSILON_RAD,
5778
"Geo point is on another axis");
5879

5980
LatLng c3 = {M_PI, 0};
60-
Vec3d p3;
61-
_geoToVec3d(&c3, &p3);
62-
t_assert(fabs(_pointSquareDist(&p1, &p3) - 4) < EPSILON_RAD,
81+
Vec3d p3 = latLngToVec3(c3);
82+
t_assert(fabs(vec3DistSq(p1, p3) - 4) < EPSILON_RAD,
6383
"Geo point is the other side of the sphere");
6484
}
6585
}

0 commit comments

Comments
 (0)