Skip to content

Commit 388c010

Browse files
authored
Update curve such that child cells overlap parent (#22)
1 parent 2a20c3c commit 388c010

9 files changed

Lines changed: 373 additions & 401 deletions

File tree

examples/website/lattice/app.tsx

Lines changed: 153 additions & 160 deletions
Large diffs are not rendered by default.

modules/core/hilbert.ts

Lines changed: 76 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,53 @@ export const quaternaryToFlips = (n: Quaternary): [Flip, Flip] => {
7878

7979
const FLIP_SHIFT = vec2.fromValues(-1, 1) as IJ;
8080

81+
82+
// Patterns used to rearrange the cells when shifting. This adjusts the layout so that
83+
// children always overlap with their parent cells.
84+
function reversePattern(pattern: number[]): number[] {
85+
return Array.from({length: pattern.length}, (_, i) => pattern.indexOf(i));
86+
}
87+
88+
const PATTERN = [0, 1, 3, 4, 5, 6, 7, 2];
89+
const PATTERN_FLIPPED = [0, 1, 2, 7, 3, 4, 5, 6];
90+
const PATTERN_REVERSED = reversePattern(PATTERN);
91+
const PATTERN_FLIPPED_REVERSED = reversePattern(PATTERN_FLIPPED);
92+
93+
const _shiftDigits = (
94+
digits: Quaternary[],
95+
i: number,
96+
flips: [Flip, Flip],
97+
invertJ: boolean,
98+
pattern: number[]
99+
): void => {
100+
if (i <= 0) return;
101+
102+
const parentK = digits[i] || 0;
103+
const childK = digits[i - 1];
104+
const F = flips[0] + flips[1];
105+
106+
// Detect when cells need to be shifted
107+
let needsShift: boolean = true;
108+
let first: boolean = true;
109+
110+
// The value of F which cells need to be shifted
111+
// The rule is flipped depending on the orientation, specifically on the value of invertJ
112+
if (invertJ !== (F === 0)) {
113+
needsShift = parentK === 1 || parentK === 2; // Second & third pentagons only
114+
first = parentK === 1; // Second pentagon is first
115+
} else {
116+
needsShift = parentK < 2; // First two pentagons only
117+
first = parentK === 0; // First pentagon is first
118+
}
119+
if (!needsShift) return;
120+
121+
// Apply the pattern by setting the digits based on the value provided
122+
const src = first ? childK : childK + 4;
123+
const dst = pattern[src];
124+
digits[i - 1] = dst % 4 as Quaternary;
125+
digits[i] = (parentK + 4 + Math.floor(dst / 4) - Math.floor(src / 4)) % 4 as Quaternary;
126+
}
127+
81128
export const sToAnchor = (s: number | bigint, resolution: number, orientation: Orientation): Anchor => {
82129
let input = BigInt(s);
83130
const reverse = orientation === 'vu' || orientation === 'wu' || orientation === 'vw';
@@ -86,7 +133,7 @@ export const sToAnchor = (s: number | bigint, resolution: number, orientation: O
86133
if (reverse) {
87134
input = (1n << BigInt(2 * resolution)) - input - 1n;
88135
}
89-
const anchor = _sToAnchor(input);
136+
const anchor = _sToAnchor(input, resolution, invertJ, flipIJ);
90137
if (flipIJ) {
91138
const { offset: [_i, _j], flips: [flipX, flipY] } = anchor;
92139
anchor.offset = [_j, _i] as IJ;
@@ -106,30 +153,39 @@ export const sToAnchor = (s: number | bigint, resolution: number, orientation: O
106153
return anchor;
107154
}
108155

109-
export const _sToAnchor = (s: number | bigint): Anchor => {
110-
const k = Number(s) % 4 as Quaternary;
156+
export const _sToAnchor = (s: number | bigint, resolution: number, invertJ: boolean, flipIJ: boolean): Anchor => {
111157
const offset = vec2.create() as KJ;
112158
const flips = [NO, NO] as [Flip, Flip];
113159
let input = BigInt(s);
114160

115161
// Get all quaternary digits first
116162
const digits: Quaternary[] = [];
117-
while (input > 0n) {
163+
while (input > 0n || digits.length < resolution) {
118164
digits.push(Number(input % 4n) as Quaternary);
119165
input = input >> 2n;
120166
}
121-
167+
168+
const pattern = flipIJ ? PATTERN_FLIPPED : PATTERN;
169+
122170
// Process digits from left to right (most significant first)
171+
for (let i = digits.length - 1; i >= 0; i--) {
172+
_shiftDigits(digits, i, flips, invertJ, pattern);
173+
vec2.multiply(flips, flips, quaternaryToFlips(digits[i]));
174+
}
175+
176+
flips[0] = NO; flips[1] = NO; // Reset flips for the next loop
123177
for (let i = digits.length - 1; i >= 0; i--) {
124178
// Scale up existing anchor
125179
vec2.scale(offset, offset, 2);
126-
180+
127181
// Get child anchor and combine with current anchor
128182
const childOffset = quaternaryToKJ(digits[i], flips);
129183
vec2.add(offset, offset, childOffset);
130184
vec2.multiply(flips, flips, quaternaryToFlips(digits[i]));
131185
}
132186

187+
const k = digits[0] || 0 as Quaternary;
188+
133189
return {flips, k, offset: KJToIJ(offset)};
134190
}
135191

@@ -182,26 +238,26 @@ export const IJToS = (input: IJ, resolution: number, orientation: Orientation =
182238
ij[1] = (1 << resolution) - (i + j);
183239
}
184240

185-
let S = _IJToS(ij);
241+
let S = _IJToS(ij, invertJ, flipIJ, resolution);
186242
if (reverse) {
187243
S = (1n << BigInt(2 * resolution)) - S - 1n;
188244
}
189245
return S;
190246
}
191247

192-
export const _IJToS = (input: IJ): bigint => {
248+
export const _IJToS = (input: IJ, invertJ: boolean, flipIJ: boolean, resolution: number): bigint => {
193249
// Get number of digits we need to process
194-
const numDigits = getRequiredDigits(input);
250+
const numDigits = resolution;
195251
const digits: Quaternary[] = new Array(numDigits);
196252

197253
const flips: [Flip, Flip] = [NO, NO];
198254
const pivot = vec2.create() as IJ;
199255

200256
// Process digits from left to right (most significant first)
201-
for (let i = 0; i < numDigits; i++) {
257+
for (let i = numDigits - 1; i >= 0; i--) {
202258
const relativeOffset = vec2.subtract(vec2.create(), input, pivot) as IJ;
203259

204-
const scale = 1 << (numDigits - 1 - i);
260+
const scale = 1 << i;
205261
const scaledOffset = vec2.scale(vec2.create(), relativeOffset, 1 / scale) as IJ;
206262

207263
const digit = IJtoQuaternary(scaledOffset, flips);
@@ -214,9 +270,16 @@ export const _IJToS = (input: IJ): bigint => {
214270
vec2.multiply(flips, flips, quaternaryToFlips(digit));
215271
}
216272

273+
const pattern = flipIJ ? PATTERN_FLIPPED_REVERSED : PATTERN_REVERSED;
274+
275+
for (let i = 0; i < digits.length; i++) {
276+
vec2.multiply(flips, flips, quaternaryToFlips(digits[i]));
277+
_shiftDigits(digits, i, flips, invertJ, pattern);
278+
}
279+
217280
let output = 0n;
218-
for (let i = 0; i < numDigits; i++) {
219-
const scale = 1n << BigInt(2 * (numDigits - 1 - i));
281+
for (let i = numDigits - 1; i >= 0; i--) {
282+
const scale = 1n << BigInt(2 * i);
220283
output += BigInt(digits[i]) * scale;
221284
}
222285

modules/core/pentagon.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const v: Face = [L * Math.cos(V), L * Math.sin(V)] as Face;
5353

5454
const W = bisectorAngle - PI_OVER_5;
5555
const w: Face = [L * Math.cos(W), L * Math.sin(W)] as Face;
56-
const TRIANGLE = new PentagonShape([u, v, w, w, w]); // TODO hacky, don't pretend this is pentagon
56+
const TRIANGLE = new PentagonShape([u, v, w] as any); // TODO hacky, don't pretend this is pentagon
5757

5858
/**
5959
* Basis vectors used to layout primitive unit

modules/core/triangle.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

modules/core/utils.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44

55
import {vec2, mat2, mat2d, vec3, glMatrix, quat} from 'gl-matrix';
66
glMatrix.setMatrixArrayType(Float64Array as any);
7-
import type { Radians, Spherical, Cartesian, Face, Degrees, LonLat } from './coordinate-systems';
8-
import { Triangle } from './triangle';
7+
import type { Radians, Spherical, Face, Degrees, LonLat } from './coordinate-systems';
98
import { Orientation } from "./hilbert";
109

1110
export type Origin = {
@@ -22,7 +21,6 @@ export type Pentagon = [Face, Face, Face, Face, Face];
2221
export class PentagonShape {
2322
private vertices: Pentagon;
2423
public id: {i: number, j: number, k: number, resolution: number, segment?: number, origin?: Origin};
25-
private triangles?: Triangle[];
2624

2725
constructor(vertices: Pentagon) {
2826
this.vertices = vertices;
@@ -53,12 +51,18 @@ export class PentagonShape {
5351

5452
/**
5553
* Reflects the pentagon over the x-axis (equivalent to negating y)
54+
* and reverses the winding order to maintain consistent orientation
5655
* @returns The reflected pentagon
5756
*/
5857
reflectY(): PentagonShape {
58+
// First reflect all vertices
5959
for (const vertex of this.vertices) {
6060
vertex[1] = -vertex[1];
6161
}
62+
63+
// Then reverse the winding order to maintain consistent orientation
64+
this.vertices.reverse();
65+
6266
return this;
6367
}
6468

@@ -96,32 +100,34 @@ export class PentagonShape {
96100
}
97101

98102
/**
99-
* Tests if a point is inside the pentagon by checking if it's in any of the three triangles
100-
* that make up the pentagon. Assumes pentagon is convex.
103+
* Tests if a point is inside the pentagon by checking if it's on the correct side of all edges.
104+
* Assumes consistent winding order (counter-clockwise).
101105
* @param point The point to test
102106
* @returns true if the point is inside the pentagon
103107
*/
104108
containsPoint(point: vec2): boolean {
105-
// Split pentagon into three triangles from first vertex
106-
const v0 = this.vertices[0];
107-
108-
if (!this.triangles) {
109-
this.triangles = [];
110-
// Order triangles by size to increase chance of early return
111-
for (const i of [2, 1, 3]) {
112-
const v1 = this.vertices[i];
113-
const v2 = this.vertices[i + 1];
114-
this.triangles.push(new Triangle(v0, v1, v2));
115-
}
116-
}
117-
118-
for (const triangle of this.triangles) {
119-
if (triangle.containsPoint(point)) {
120-
return true;
109+
// For each edge of the pentagon
110+
const N = this.vertices.length;
111+
for (let i = 0; i < N; i++) {
112+
const v1 = this.vertices[i];
113+
const v2 = this.vertices[(i + 1) % N];
114+
115+
// Calculate the cross product to determine which side of the line the point is on
116+
// (v2 - v1) × (point - v1)
117+
const dx = v2[0] - v1[0];
118+
const dy = v2[1] - v1[1];
119+
const px = point[0] - v1[0];
120+
const py = point[1] - v1[1];
121+
122+
// Cross product: dx * py - dy * px
123+
// If positive, point is on the wrong side
124+
// If negative, point is on the correct side
125+
if (dx * py - dy * px > 0) {
126+
return false;
121127
}
122128
}
123-
124-
return false;
129+
130+
return true;
125131
}
126132

127133
/**

tests/hierarchy.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { PentagonShape, Pentagon } from 'a5/core/utils';
3+
import { Orientation } from 'a5/core/hilbert';
4+
import { sToAnchor } from 'a5/core/hilbert';
5+
import { getPentagonVertices } from 'a5/core/tiling';
6+
import { vec2 } from 'gl-matrix';
7+
8+
function generateCells(resolution: number, orientation: Orientation): vec2[][] {
9+
const sequence = Array.from({length: Math.pow(4, resolution)}, (_, i) => i);
10+
const anchors = sequence.map(s => sToAnchor(s, resolution, orientation));
11+
return anchors.map(anchor =>
12+
getPentagonVertices(resolution, 0, anchor).getVertices()
13+
);
14+
}
15+
16+
function verifyHierarchy(resolution: number, orientation: Orientation): void {
17+
const level1Cells = generateCells(resolution, orientation);
18+
const level2Cells = generateCells(resolution + 1, orientation);
19+
20+
let failedPentagon: PentagonShape | null = null;
21+
let failedChild: vec2[] | null = null;
22+
for (let i = 0; i < level2Cells.length; i++) {
23+
const child = level2Cells[i];
24+
const parent = level1Cells[Math.floor(i / 4)];
25+
const pentagon = new PentagonShape(parent as Pentagon);
26+
let contained = false;
27+
for (const vertex of child) {
28+
if (pentagon.containsPoint(vertex)) {
29+
contained = true;
30+
break;
31+
}
32+
}
33+
if (!contained) {
34+
failedPentagon = pentagon;
35+
failedChild = child;
36+
}
37+
}
38+
if (failedPentagon && failedChild) {
39+
console.log('Pentagon:', failedPentagon.getVertices());
40+
console.log('did not contain any of:', failedChild);
41+
}
42+
expect(failedPentagon).toBeNull();
43+
expect(failedChild).toBeNull();
44+
}
45+
46+
describe('Cell Hierarchy', () => {
47+
const orientations: Orientation[] = ['uv', 'vu', 'uw', 'wu', 'vw', 'wv'];
48+
orientations.forEach(orientation => {
49+
[1, 2, 3, 4, 5, 6].forEach(resolution => {
50+
it(`Hierarchy ${orientation} correct for resolution ${resolution}`, () => {
51+
verifyHierarchy(resolution, orientation);
52+
});
53+
});
54+
});
55+
});

0 commit comments

Comments
 (0)