Skip to content

Commit d1f1a78

Browse files
justinhwangclaude
andcommitted
feat: add pure-Go local IJ coordinates & grid metrics to x/h3go
Add CellToLocalIJ/LocalIJToCell, GridDistance, GridPath (and Cell method forms) plus the CoordIJ type, implementing the local IJK/IJ coordinate machinery, cube-coordinate line interpolation, and the pentagon rotation tables. Verified against the cgo reference over the shared corpus with full in-package coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent af54dc9 commit d1f1a78

5 files changed

Lines changed: 1319 additions & 0 deletions

File tree

x/h3go/faceijk.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,49 @@ func (c coordIJK) rotate60cw() coordIJK {
227227
return out
228228
}
229229

230+
// sub returns the component-wise difference c - b.
231+
func (c coordIJK) sub(b coordIJK) coordIJK {
232+
return coordIJK{i: c.i - b.i, j: c.j - b.j, k: c.k - b.k}
233+
}
234+
235+
// distance returns the grid distance between two IJK coordinates: the largest
236+
// component of their normalized difference (normalization leaves all components
237+
// non-negative).
238+
func (c coordIJK) distance(b coordIJK) int {
239+
diff := c.sub(b)
240+
diff.normalize()
241+
242+
return max(diff.i, diff.j, diff.k)
243+
}
244+
245+
// unitToDigit maps an IJK coordinate to its digit if, once normalized, it is the
246+
// center or one of the six unit neighbors, returning invalidDigit otherwise.
247+
func (c coordIJK) unitToDigit() int {
248+
c.normalize()
249+
250+
if c.i < 0 || c.i > 1 || c.j < 0 || c.j > 1 || c.k < 0 || c.k > 1 {
251+
return invalidDigit
252+
}
253+
254+
return unitIjkToDigitLUT[c.i][c.j][c.k]
255+
}
256+
257+
// toCube converts an IJK coordinate in place to cube coordinates, suitable for
258+
// linear interpolation along a grid line.
259+
func (c *coordIJK) toCube() {
260+
c.i = -c.i + c.k
261+
c.j = c.j - c.k
262+
c.k = -c.i - c.j
263+
}
264+
265+
// fromCube converts cube coordinates in place back to a normalized IJK
266+
// coordinate, the inverse of toCube.
267+
func (c *coordIJK) fromCube() {
268+
c.i = -c.i
269+
c.k = 0
270+
c.normalize()
271+
}
272+
230273
// downAp3 transforms an IJK coordinate to the next finer resolution on the
231274
// Class II aperture-3 substrate grid (counterclockwise), then re-normalizes.
232275
func (c *coordIJK) downAp3() {
@@ -528,6 +571,27 @@ func (c Cell) rotatePent60ccw() Cell {
528571
return c
529572
}
530573

574+
// rotatePent60cw rotates c 60° clockwise about a pentagon base cell center,
575+
// skipping the deleted k-axis subsequence as the leading digit is first
576+
// encountered so the index stays canonical.
577+
func (c Cell) rotatePent60cw() Cell {
578+
foundFirstNonZero := false
579+
580+
for r := 1; r <= c.Resolution(); r++ {
581+
c = c.setIndexDigit(r, rotate60cw(c.indexDigit(r)))
582+
583+
if !foundFirstNonZero && c.indexDigit(r) != centerDigit {
584+
foundFirstNonZero = true
585+
586+
if c.leadingNonZeroDigit() == kAxesDigit {
587+
c = c.rotate60cw()
588+
}
589+
}
590+
}
591+
592+
return c
593+
}
594+
531595
// --- FaceIJK to H3 index ---
532596

533597
// toH3 encodes a face-centered IJK coordinate at the given resolution into an H3

x/h3go/h3go.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ const (
134134
ikAxesDigit = 5
135135
ijAxesDigit = 6
136136
invalidDigit = 7
137+
numDigits = 7
137138

138139
// H3 index bit-layout offsets and masks, shared with the cgo h3 package.
139140
cellMode = h3core.CellMode
@@ -892,3 +893,68 @@ var baseCellNeighbor60CCWRots = [numBaseCells][7]int{
892893
{0, 5, 0, 0, 5, 5, 0}, // base cell 120
893894
{0, 0, 1, 0, 1, 5, 1}, // base cell 121
894895
}
896+
897+
// pentagonRotations maps the base-cell direction (or leading digit) to the
898+
// number of 60° clockwise index rotations needed when unfolding a pentagon,
899+
// indexed by [origin leading digit][direction]. A -1 marks an invalid k-axis
900+
// combination.
901+
var pentagonRotations = [7][7]int{
902+
{0, -1, 0, 0, 0, 0, 0},
903+
{-1, -1, -1, -1, -1, -1, -1},
904+
{0, -1, 0, 0, 0, 1, 0},
905+
{0, -1, 0, 0, 1, 1, 0},
906+
{0, -1, 0, 5, 0, 0, 0},
907+
{0, -1, 5, 5, 0, 0, 0},
908+
{0, -1, 0, 0, 0, 0, 0},
909+
}
910+
911+
// pentagonRotationsReverse reverses the rotation pentagonRotations introduces
912+
// when the origin is on a pentagon, indexed by [leading digit][direction].
913+
var pentagonRotationsReverse = [7][7]int{
914+
{0, 0, 0, 0, 0, 0, 0},
915+
{-1, -1, -1, -1, -1, -1, -1},
916+
{0, 1, 0, 0, 0, 0, 0},
917+
{0, 1, 0, 0, 0, 1, 0},
918+
{0, 5, 0, 0, 0, 0, 0},
919+
{0, 5, 0, 5, 0, 0, 0},
920+
{0, 0, 0, 0, 0, 0, 0},
921+
}
922+
923+
// pentagonRotationsReverseNonpolar reverses the pentagon rotation when the index
924+
// is on a non-polar pentagon and the origin is not, indexed by
925+
// [reverse direction][leading digit].
926+
var pentagonRotationsReverseNonpolar = [7][7]int{
927+
{0, 0, 0, 0, 0, 0, 0},
928+
{-1, -1, -1, -1, -1, -1, -1},
929+
{0, 1, 0, 0, 0, 0, 0},
930+
{0, 1, 0, 0, 0, 1, 0},
931+
{0, 5, 0, 0, 0, 0, 0},
932+
{0, 1, 0, 5, 1, 1, 0},
933+
{0, 0, 0, 0, 0, 0, 0},
934+
}
935+
936+
// pentagonRotationsReversePolar reverses the pentagon rotation when the index is
937+
// on a polar pentagon and the origin is not, indexed by
938+
// [reverse direction][leading digit].
939+
var pentagonRotationsReversePolar = [7][7]int{
940+
{0, 0, 0, 0, 0, 0, 0},
941+
{-1, -1, -1, -1, -1, -1, -1},
942+
{0, 1, 1, 1, 1, 1, 1},
943+
{0, 1, 0, 0, 0, 1, 0},
944+
{0, 1, 0, 0, 1, 1, 1},
945+
{0, 1, 0, 5, 1, 1, 0},
946+
{0, 1, 1, 0, 1, 1, 1},
947+
}
948+
949+
// failedDirections marks the direction pairs that cannot be unfolded across a
950+
// pentagon (any unfolding across more than one icosahedron face), indexed by
951+
// [origin direction][index direction].
952+
var failedDirections = [7][7]bool{
953+
{false, false, false, false, false, false, false},
954+
{false, false, false, false, false, false, false},
955+
{false, false, false, false, true, true, false},
956+
{false, false, false, false, true, false, true},
957+
{false, false, true, true, false, false, false},
958+
{false, false, true, false, false, false, true},
959+
{false, false, false, true, false, true, false},
960+
}

0 commit comments

Comments
 (0)