From 4d069a6efa3dcb28e842d65e0bdfbeb0eb8152bc Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Wed, 17 Jul 2024 23:39:16 +0200 Subject: [PATCH] s2: cellid --- .github/workflows/_test.yml | 24 +++ .github/workflows/pull_request.yml | 7 + .github/workflows/push.yml | 5 + .gitignore | 2 + .prettierrc | 5 + int/uint64.ts | 169 +++++++++++++++++++ int/uint64_test.ts | 70 ++++++++ int/uint8.ts | 257 +++++++++++++++++++++++++++++ package.json | 14 ++ s2/cellid.ts | 107 ++++++++++++ s2/cellid_test.ts | 108 ++++++++++++ 11 files changed, 768 insertions(+) create mode 100644 .github/workflows/_test.yml create mode 100644 .github/workflows/pull_request.yml create mode 100644 .github/workflows/push.yml create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 int/uint64.ts create mode 100644 int/uint64_test.ts create mode 100644 int/uint8.ts create mode 100644 package.json create mode 100644 s2/cellid.ts create mode 100644 s2/cellid_test.ts diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml new file mode 100644 index 0000000..a82341d --- /dev/null +++ b/.github/workflows/_test.yml @@ -0,0 +1,24 @@ +name: Unit Tests +on: workflow_call +jobs: + unit-tests: + runs-on: '${{ matrix.os }}' + timeout-minutes: 10 + strategy: + matrix: + os: + - 'ubuntu-22.04' + node-version: + - 22.x + - 20.x + - 18.x + steps: + - uses: actions/checkout@v4 + - name: 'Install node.js ${{ matrix.node-version }}' + uses: actions/setup-node@v3 + with: + node-version: '${{ matrix.node-version }}' + - name: Run unit tests + run: | + npm install + npm test diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml new file mode 100644 index 0000000..c9533fc --- /dev/null +++ b/.github/workflows/pull_request.yml @@ -0,0 +1,7 @@ +name: Continuous Integration +on: pull_request +jobs: + unit-tests: + # only run this job for forks + if: github.event.pull_request.head.repo.full_name != github.repository + uses: ./.github/workflows/_test.yml diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 0000000..b650bf9 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,5 @@ +name: Continuous Integration +on: push +jobs: + unit-tests: + uses: ./.github/workflows/_test.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5f19d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..31ba22d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 120 +} diff --git a/int/uint64.ts b/int/uint64.ts new file mode 100644 index 0000000..42bb889 --- /dev/null +++ b/int/uint64.ts @@ -0,0 +1,169 @@ +import type { uint8 } from './uint8' + +/** + * @module uint64 + * + * 64-bit integer manipulation/conversion methods + */ + +/** + * valid returns false if i is the wrong type or exceeds 64 bits + */ +export const valid = (i: bigint): boolean => { + return typeof i === 'bigint' && BigInt.asUintN(64, i) == i +} + +/** + * byte returns the byte at offset n (left-to-right) + */ +export const byte = (i: bigint, o: offset): uint8 => { + const offset = BigInt((7 - o) * 8) + const mask = 0b11111111n << offset + return Number((i & mask) >> offset) as uint8 +} + +/** + * trailingZeros8 returns the number of trailing zero bits in byte + */ +const trailingZeros8 = (byte: uint8): number => { + const lsb = byte & -byte + if (lsb === 0) return 8 + return 31 - Math.clz32(lsb) +} + +/** + * trailingZeros returns the number of trailing zero bits in an uint64 + */ +export const trailingZeros = (i: bigint): position => { + for (let n = 7; n >= 0; n--) { + const z = trailingZeros8(byte(i, n as offset)) + if (z < 8) return ((7 - n) * 8 + z) as position + } + return 64 as position +} + +/** + * flip flip the bit at position p in uint64 + */ +export const flip = (i: bigint, p: position): bigint => { + return i ^ (1n << BigInt(p)) +} + +/** + * set sets the bit at position p in uint64 to v + */ +export const set = (i: bigint, p: position, v: bit): bigint => { + const mask = 1n << BigInt(p) + return v ? i | mask : i & ~mask +} + +/** + * setTrailingBits fills the rightmost n bits in uint64 with v + */ +export const setTrailingBits = (i: bigint, p: position, v: bit): bigint => { + const mask = (1n << BigInt(p + 1)) - 1n + return v ? i | mask : i & ~mask +} + +/** + * lsb returns the least significant bit that is set + */ +export const lsb = (i: bigint) => i & -i + +/** + * marshal writes an uint64 to an Uint8Array + */ +export const marshal = (i: bigint): Uint8Array => { + const arr = new Uint8Array(8) + new DataView(arr.buffer, 0, 8).setBigUint64(0, i, false) + return arr +} + +/** + * unmarshal reads an uint64 from an Uint8Array + */ +export const unmarshal = (arr: Uint8Array): bigint => { + return new DataView(arr.buffer, 0, 8).getBigUint64(0, false) +} + +/** + * toBinaryString returns the binary string representation of an uint64 + */ +export const toBinaryString = (i: bigint): string => i.toString(2).padStart(64, '0') + +/** + * Types + */ + +/** bit value */ +export type bit = 0 | 1 + +/** byte offset */ +export type offset = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 + +/** bit position */ +export type position = + | 0 + | 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 diff --git a/int/uint64_test.ts b/int/uint64_test.ts new file mode 100644 index 0000000..03f6467 --- /dev/null +++ b/int/uint64_test.ts @@ -0,0 +1,70 @@ +import test from 'node:test' +import { equal } from 'node:assert/strict' +import * as uint64 from './uint64.ts' + +test('flip', (t) => { + const i = 0b0000000000000000000000000000000000000000000000000000000000000000n + equal(uint64.flip(i, 0), 0b0000000000000000000000000000000000000000000000000000000000000001n) + equal(uint64.flip(i, 1), 0b0000000000000000000000000000000000000000000000000000000000000010n) + equal(uint64.flip(i, 7), 0b0000000000000000000000000000000000000000000000000000000010000000n) + equal(uint64.flip(i, 9), 0b0000000000000000000000000000000000000000000000000000001000000000n) + equal(uint64.flip(i, 61), 0b0010000000000000000000000000000000000000000000000000000000000000n) + equal(uint64.flip(i, 63), 0b1000000000000000000000000000000000000000000000000000000000000000n) +}) + +test('byte', (t) => { + const i = 0b1111111101111111001111110001111100001111000001110000001100000001n + equal(uint64.byte(i, 0), 0b11111111) + equal(uint64.byte(i, 1), 0b01111111) + equal(uint64.byte(i, 2), 0b00111111) + equal(uint64.byte(i, 3), 0b00011111) + equal(uint64.byte(i, 4), 0b00001111) + equal(uint64.byte(i, 5), 0b00000111) + equal(uint64.byte(i, 6), 0b00000011) + equal(uint64.byte(i, 7), 0b00000001) +}) + +test('trailingZeros', (t) => { + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000000000001n), 0) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000000000010n), 1) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000000000100n), 2) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000000001000n), 3) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000000010000n), 4) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000000100000n), 5) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000001000000n), 6) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000010000000n), 7) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000100000000n), 8) + equal(uint64.trailingZeros(0b0000000000000000000000000000000010000000000000000000000000000000n), 31) + equal(uint64.trailingZeros(0b0000000000000000000000000000000001000000000000000000000000000000n), 30) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000100000000000000000000000000000n), 29) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000010000000000000000000000000000n), 28) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000001000000000000000000000000000n), 27) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000100000000000000000000000000n), 26) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000010000000000000000000000000n), 25) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000001000000000000000000000000n), 24) + equal(uint64.trailingZeros(0b0000000000000000000000000000000000000000000000000000000000000000n), 64) +}) + +test('setTrailingBits', (t) => { + const i = 0b0000000000000000000000000000000000000000000000000000000000000000n + equal(uint64.setTrailingBits(i, 0, 1), 0b0000000000000000000000000000000000000000000000000000000000000001n) + equal(uint64.setTrailingBits(i, 1, 1), 0b0000000000000000000000000000000000000000000000000000000000000011n) + equal(uint64.setTrailingBits(i, 2, 1), 0b0000000000000000000000000000000000000000000000000000000000000111n) + equal(uint64.setTrailingBits(i, 5, 1), 0b0000000000000000000000000000000000000000000000000000000000111111n) + equal(uint64.setTrailingBits(i, 63, 1), 0b1111111111111111111111111111111111111111111111111111111111111111n) +}) + +test('setBit', (t) => { + equal(uint64.set(0b00000000000000000000000000000000n, 0, 1), 0b00000000000000000000000000000001n) + equal(uint64.set(0b00000000000000000000000000000000n, 1, 1), 0b00000000000000000000000000000010n) + equal(uint64.set(0b00000000000000000000000000000000n, 9, 1), 0b00000000000000000000001000000000n) + equal(uint64.set(0b00000000000000000000000000000000n, 17, 1), 0b00000000000000100000000000000000n) + equal(uint64.set(0b00000000000000000000000000000000n, 25, 1), 0b00000010000000000000000000000000n) + equal(uint64.set(0b00000000000000000000000000000000n, 31, 1), 0b10000000000000000000000000000000n) + equal(uint64.set(0b11111111111111111111111111111111n, 0, 0), 0b11111111111111111111111111111110n) + equal(uint64.set(0b11111111111111111111111111111111n, 1, 0), 0b11111111111111111111111111111101n) + equal(uint64.set(0b11111111111111111111111111111111n, 9, 0), 0b11111111111111111111110111111111n) + equal(uint64.set(0b11111111111111111111111111111111n, 17, 0), 0b11111111111111011111111111111111n) + equal(uint64.set(0b11111111111111111111111111111111n, 25, 0), 0b11111101111111111111111111111111n) + equal(uint64.set(0b11111111111111111111111111111111n, 31, 0), 0b01111111111111111111111111111111n) +}) diff --git a/int/uint8.ts b/int/uint8.ts new file mode 100644 index 0000000..e80ce29 --- /dev/null +++ b/int/uint8.ts @@ -0,0 +1,257 @@ +export type uint8 = + | 0 + | 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 diff --git a/package.json b/package.json new file mode 100644 index 0000000..e21c81c --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "s2", + "version": "1.0.0", + "scripts": { + "test": "node --import tsx --test **/*_test.ts" + }, + "author": "", + "license": "ISC", + "description": "javascript port of s2 geometry", + "devDependencies": { + "@types/node": "^20.14.11", + "tsx": "^4.16.2" + } +} diff --git a/s2/cellid.ts b/s2/cellid.ts new file mode 100644 index 0000000..80d6ad3 --- /dev/null +++ b/s2/cellid.ts @@ -0,0 +1,107 @@ +import * as uint64 from '../int/uint64' + +// FACE_BITS is the number of bits used to encode the face number +export const FACE_BITS = 3 + +// NUM_FACES is the number of faces +export const NUM_FACES = 6 + +// MAX_LEVEL is the number of levels needed to specify a leaf cell +export const MAX_LEVEL = 30 + +// POS_BITS is the total number of position bits. The extra bit (61 rather +// than 60) lets us encode each cell as its Hilbert curve position at the +// cell center (which is halfway along the portion of the Hilbert curve that +// fills that cell). +export const POS_BITS = 2 * MAX_LEVEL + 1 + +// face returns the cube face for this cell ID, in the range [0,5] +// https://s2geometry.io/resources/earthcube +export const face = (i: bigint): number => { + return Number(i >> BigInt(POS_BITS)) +} + +// pos returns the position along the Hilbert curve of this cell ID, in the range [0,2^POS_BITS-1] +export const pos = (i: bigint): bigint => { + return i & (~0n >> BigInt(FACE_BITS)) +} + +// level returns the subdivision level of this cell ID, in the range [0, MAX_LEVEL] +// https://s2geometry.io/devguide/s2cell_hierarchy +export const level = (i: bigint): number => { + return MAX_LEVEL - (uint64.trailingZeros(i) >>> 1) +} + +// parent returns the cell at the given level, which must be no greater than the current level +export const parent = (i: bigint, level: number): bigint => { + const lsb = lsbForLevel(level) + return (i & -lsb) | lsb +} + +// valid reports whether ci represents a valid cell +export const valid = (i: bigint): boolean => { + return uint64.valid(i) && face(i) <= NUM_FACES && (uint64.lsb(i) & 0x1555555555555555n) != 0n +} + +// Bitwise + +// lsbForLevel returns the lowest-numbered bit that is on for cells at the given level. +export const lsbForLevel = (level: number): bigint => { + return 1n << BigInt(2 * (MAX_LEVEL - level)) +} + +// Ranges + +// rangeMin returns the minimum CellID that is contained within this cell. +export const rangeMin = (i: bigint) => { + return i - (uint64.lsb(i) - 1n) +} + +// rangeMax returns the maximum CellID that is contained within this cell. +export const rangeMax = (i: bigint): bigint => { + return i + (uint64.lsb(i) - 1n) +} + +// contains returns true iff i contains oci +export const contains = (i: bigint, oci: bigint) => { + return i !== oci && rangeMin(i) <= oci && oci <= rangeMax(i) +} + +// intersects returns true iff i intersects oci +export const intersects = (i: bigint, oci: bigint) => { + return rangeMin(oci) <= rangeMax(i) && rangeMax(oci) >= rangeMin(i) +} + +// Token + +/** + * toToken returns a hex-encoded string of the uint64 cell id, with leading + * zeros included but trailing zeros stripped + */ +export const toToken = (i: bigint): string => { + const s = i.toString(16).replace(/0+$/, '') + if (s.length === 0) return 'X' + return s +} + +/** + * fromToken returns a cell given a hex-encoded string of its uint64 ID + */ +export const fromToken = (t: string): bigint => { + if (t.length > 16) return 0n + + let i = BigInt('0x' + t) + if (t.length < 16) i = i << BigInt(4 * (16 - t.length)) + return i +} + +// Constructors + +// CellIDFromFacePosLevel returns a cell given its face in the range +// [0,5], the 61-bit Hilbert curve position pos within that face, and +// the level in the range [0,MAX_LEVEL]. The position in the cell ID +// will be truncated to correspond to the Hilbert curve position at +// the center of the returned cell. +export const fromFacePosLevel = (face: number, pos: number, level: number): bigint => { + return parent((BigInt(face) << BigInt(POS_BITS)) + BigInt(pos || 1), level) +} diff --git a/s2/cellid_test.ts b/s2/cellid_test.ts new file mode 100644 index 0000000..871a360 --- /dev/null +++ b/s2/cellid_test.ts @@ -0,0 +1,108 @@ +import test from 'node:test' +import { equal, ok } from 'node:assert/strict' +import * as cellid from './cellid.ts' + +test('face', (t) => { + equal(cellid.face(0b0001111111111111111111111111111111111111111111111111111111111111n), 0) + equal(cellid.face(0b0011111111111111111111111111111111111111111111111111111111111111n), 1) + equal(cellid.face(0b0101111111111111111111111111111111111111111111111111111111111111n), 2) + equal(cellid.face(0b0111111111111111111111111111111111111111111111111111111111111111n), 3) + equal(cellid.face(0b1001111111111111111111111111111111111111111111111111111111111111n), 4) + equal(cellid.face(0b1011111111111111111111111111111111111111111111111111111111111111n), 5) +}) + +test('level', (t) => { + equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000000000001n), 30) + equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000000000100n), 29) + equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000000010000n), 28) + equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000001000000n), 27) + equal(cellid.level(0b0000000000000000000000000000000000000000000000000000000100000000n), 26) + equal(cellid.level(0b0000000000000000000000000000000000000000000000000000010000000000n), 25) + equal(cellid.level(0b0000000000000000000000000000000000000000000000000001000000000000n), 24) + equal(cellid.level(0b0000000000000000000000000000000000000000000000000100000000000000n), 23) + equal(cellid.level(0b0000000000000000000000000000000000000000000000010000000000000000n), 22) + equal(cellid.level(0b0000000000000000000000000000000000000000000001000000000000000000n), 21) + equal(cellid.level(0b0000000000000000000000000000000000000000000100000000000000000000n), 20) + equal(cellid.level(0b0000000000000000000000000000000000000000010000000000000000000000n), 19) + equal(cellid.level(0b0000000000000000000000000000000000000001000000000000000000000000n), 18) + equal(cellid.level(0b0000000000000000000000000000000000000100000000000000000000000000n), 17) + equal(cellid.level(0b0000000000000000000000000000000000010000000000000000000000000000n), 16) + equal(cellid.level(0b0000000000000000000000000000000001000000000000000000000000000000n), 15) + equal(cellid.level(0b0000000000000000000000000000000100000000000000000000000000000000n), 14) + equal(cellid.level(0b0000000000000000000000000000010000000000000000000000000000000000n), 13) + equal(cellid.level(0b0000000000000000000000000001000000000000000000000000000000000000n), 12) + equal(cellid.level(0b0000000000000000000000000100000000000000000000000000000000000000n), 11) + equal(cellid.level(0b0000000000000000000000010000000000000000000000000000000000000000n), 10) + equal(cellid.level(0b0000000000000000000001000000000000000000000000000000000000000000n), 9) + equal(cellid.level(0b0000000000000000000100000000000000000000000000000000000000000000n), 8) + equal(cellid.level(0b0000000000000000010000000000000000000000000000000000000000000000n), 7) + equal(cellid.level(0b0000000000000001000000000000000000000000000000000000000000000000n), 6) + equal(cellid.level(0b0000000000000100000000000000000000000000000000000000000000000000n), 5) + equal(cellid.level(0b0000000000010000000000000000000000000000000000000000000000000000n), 4) + equal(cellid.level(0b0000000001000000000000000000000000000000000000000000000000000000n), 3) + equal(cellid.level(0b0000000100000000000000000000000000000000000000000000000000000000n), 2) + equal(cellid.level(0b0000010000000000000000000000000000000000000000000000000000000000n), 1) + equal(cellid.level(0b0001000000000000000000000000000000000000000000000000000000000000n), 0) +}) + +test('parent', (t) => { + const c1 = /* */ 0b0011110000111100001111000011110000000000000000000000000000000000n + equal(cellid.parent(c1, 9), 0b0011110000111100001111000000000000000000000000000000000000000000n) + equal(cellid.parent(c1, 5), 0b0011110000111100000000000000000000000000000000000000000000000000n) + equal(cellid.parent(c1, 1), 0b0011110000000000000000000000000000000000000000000000000000000000n) + equal(c1, /* */ 0b0011110000111100001111000011110000000000000000000000000000000000n) + + const c2 = /* */ 0b0011110000111100001111000011110000111100001111000011110000111101n + equal(cellid.parent(c2, 30), 0b0011110000111100001111000011110000111100001111000011110000111101n) + equal(cellid.parent(c2, 29), 0b0011110000111100001111000011110000111100001111000011110000111100n) + equal(cellid.parent(c2, 15), 0b0011110000111100001111000011110001000000000000000000000000000000n) + equal(cellid.parent(c2, 14), 0b0011110000111100001111000011110100000000000000000000000000000000n) + equal(cellid.parent(c2, 1), 0b0011110000000000000000000000000000000000000000000000000000000000n) + equal(cellid.parent(c2, 0), 0b0011000000000000000000000000000000000000000000000000000000000000n) +}) + +test('range', (t) => { + const c1 = /* */ 0b0011110000111100000001000000000000000000000000000000000000000000n + equal(cellid.rangeMin(c1), 0b0011110000111100000000000000000000000000000000000000000000000001n) + equal(cellid.rangeMax(c1), 0b0011110000111100000001111111111111111111111111111111111111111111n) + + const c2 = /* */ 0b0011110000111100001111000011110001000000000000000000000000000000n + equal(cellid.rangeMin(c2), 0b0011110000111100001111000011110000000000000000000000000000000001n) + equal(cellid.rangeMax(c2), 0b0011110000111100001111000011110001111111111111111111111111111111n) +}) + +test('contains', (t) => { + const c1 = 0b0011110000111100001111000011110000111100001111000011110000111101n + ok(!cellid.contains(c1, cellid.parent(c1, 10))) + ok(cellid.contains(cellid.parent(c1, 10), c1)) + + const c2 = 0b1011111111111111111111111111111111111111111111111111111111111111n + ok(!cellid.contains(c2, cellid.parent(c2, 10))) + ok(cellid.contains(cellid.parent(c2, 10), c2)) + + ok(!cellid.contains(c1, c2)) + ok(!cellid.contains(c2, c1)) +}) + +test('intersects', (t) => { + const c1 = 0b0011110000111100001111000011110000111100001111000011110000111101n + ok(cellid.intersects(c1, cellid.parent(c1, 10))) + ok(cellid.intersects(cellid.parent(c1, 10), c1)) + + const c2 = 0b1011111111111111111111111111111111111111111111111111111111111111n + ok(cellid.intersects(c2, cellid.parent(c2, 10))) + ok(cellid.intersects(cellid.parent(c2, 10), c2)) + + ok(cellid.intersects(c1, c1)) + ok(!cellid.intersects(c1, c2)) + ok(!cellid.intersects(c1, c2)) +}) + +test('valid', (t) => { + ok(cellid.valid(0b0000000000000000000000000000000000000000000000000000000000000001n)) + ok(!cellid.valid(0b1110000000000000000000000000000000000000000000000000000000000001n), 'face') + ok(!cellid.valid(0b0000000000000000000000000000000000000000000000000000000000000010n), 'level') + + ok(!cellid.valid(0b0000000000000000000000000000000000000000000000000000000000000010n)) + ok(!cellid.valid(0b0000000000000000000000000000000000000000000000000000000000001000n)) +})