@buley/hexgrid-3d 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.eslintrc.json +28 -0
  2. package/LICENSE +39 -0
  3. package/README.md +291 -0
  4. package/examples/basic-usage.tsx +52 -0
  5. package/package.json +65 -0
  6. package/public/hexgrid-worker.js +1763 -0
  7. package/rust/Cargo.toml +41 -0
  8. package/rust/src/lib.rs +740 -0
  9. package/rust/src/math.rs +574 -0
  10. package/rust/src/spatial.rs +245 -0
  11. package/rust/src/statistics.rs +496 -0
  12. package/src/HexGridEnhanced.ts +16 -0
  13. package/src/Snapshot.ts +1402 -0
  14. package/src/adapters.ts +65 -0
  15. package/src/algorithms/AdvancedStatistics.ts +328 -0
  16. package/src/algorithms/BayesianStatistics.ts +317 -0
  17. package/src/algorithms/FlowField.ts +126 -0
  18. package/src/algorithms/FluidSimulation.ts +99 -0
  19. package/src/algorithms/GraphAlgorithms.ts +184 -0
  20. package/src/algorithms/OutlierDetection.ts +391 -0
  21. package/src/algorithms/ParticleSystem.ts +85 -0
  22. package/src/algorithms/index.ts +13 -0
  23. package/src/compat.ts +96 -0
  24. package/src/components/HexGrid.tsx +31 -0
  25. package/src/components/NarrationOverlay.tsx +221 -0
  26. package/src/components/index.ts +2 -0
  27. package/src/features.ts +125 -0
  28. package/src/index.ts +30 -0
  29. package/src/math/HexCoordinates.ts +15 -0
  30. package/src/math/Matrix4.ts +35 -0
  31. package/src/math/Quaternion.ts +37 -0
  32. package/src/math/SpatialIndex.ts +114 -0
  33. package/src/math/Vector3.ts +69 -0
  34. package/src/math/index.ts +11 -0
  35. package/src/note-adapter.ts +124 -0
  36. package/src/ontology-adapter.ts +77 -0
  37. package/src/stores/index.ts +1 -0
  38. package/src/stores/uiStore.ts +85 -0
  39. package/src/types/index.ts +3 -0
  40. package/src/types.ts +152 -0
  41. package/src/utils/image-utils.ts +25 -0
  42. package/src/wasm/HexGridWasmWrapper.ts +753 -0
  43. package/src/wasm/index.ts +7 -0
  44. package/src/workers/hexgrid-math.ts +177 -0
  45. package/src/workers/hexgrid-worker.worker.ts +1807 -0
  46. package/tsconfig.json +18 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Narration Overlay Component
3
+ *
4
+ * Displays play-by-play narration messages with sparklines in a NOC dashboard style.
5
+ */
6
+
7
+ import React, { useEffect, useRef } from 'react'
8
+ import { NarrationMessage } from '@/lib/narration'
9
+ import { StatsTracker } from '@/lib/stats-tracker'
10
+ import { getAccentRgba, getAccentHex } from '@/lib/theme-colors'
11
+
12
+ export interface NarrationOverlayProps {
13
+ messages: NarrationMessage[]
14
+ statsTracker: StatsTracker | null
15
+ isVisible: boolean
16
+ onClose: () => void
17
+ }
18
+
19
+ export const NarrationOverlay: React.FC<NarrationOverlayProps> = ({
20
+ messages,
21
+ statsTracker,
22
+ isVisible,
23
+ onClose
24
+ }) => {
25
+ const messagesEndRef = useRef<HTMLDivElement>(null)
26
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
27
+
28
+ // Auto-scroll to latest message
29
+ useEffect(() => {
30
+ if (messagesEndRef.current && scrollContainerRef.current) {
31
+ scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight
32
+ }
33
+ }, [messages])
34
+
35
+ // Always render the overlay so fade-out can animate smoothly.
36
+ // Toggle visibility via styles.
37
+
38
+ const currentStats = statsTracker?.getCurrentStats()
39
+ const allTimeRecords = statsTracker?.getAllTimeRecords()
40
+ const leaderboard = statsTracker?.getLeaderboard(10)
41
+
42
+ return (
43
+ <div
44
+ style={{
45
+ position: 'fixed',
46
+ right: 12,
47
+ top: 80,
48
+ width: 400,
49
+ maxHeight: 'calc(100vh - 100px)',
50
+ background: 'rgba(0, 0, 0, 0.85)',
51
+ border: `1px solid ${getAccentRgba(0.3)}`,
52
+ borderRadius: 8,
53
+ padding: '12px',
54
+ zIndex: 10000,
55
+ display: 'flex',
56
+ flexDirection: 'column',
57
+ fontFamily: "'Courier New', monospace",
58
+ fontSize: 12,
59
+ color: getAccentHex(),
60
+ boxShadow: `0 0 20px ${getAccentRgba(0.2)}`,
61
+ // Fade transition
62
+ transition: 'opacity 220ms ease, transform 220ms ease',
63
+ opacity: isVisible ? 1 : 0,
64
+ transform: isVisible ? 'translateY(0px)' : 'translateY(-6px)',
65
+ pointerEvents: isVisible ? 'auto' as const : 'none' as const
66
+ }}
67
+ >
68
+ {/* Header */}
69
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
70
+ <div style={{ fontWeight: 'bold', fontSize: 14 }}>Play-by-Play Narration</div>
71
+ <button
72
+ onClick={onClose}
73
+ style={{
74
+ background: 'transparent',
75
+ border: '1px solid rgba(0, 255, 255, 0.3)',
76
+ color: '#00ffff',
77
+ cursor: 'pointer',
78
+ padding: '4px 8px',
79
+ borderRadius: 4,
80
+ fontSize: 11
81
+ }}
82
+ >
83
+ ×
84
+ </button>
85
+ </div>
86
+
87
+ {/* Stats Dashboard (Collapsible) */}
88
+ {currentStats && (
89
+ <details style={{ marginBottom: 8, fontSize: 11 }}>
90
+ <summary style={{ cursor: 'pointer', color: '#00ffff', marginBottom: 4 }}>
91
+ Stats Dashboard
92
+ </summary>
93
+ <div style={{ padding: '8px', background: 'rgba(0, 255, 255, 0.05)', borderRadius: 4 }}>
94
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
95
+ <span>Generation:</span>
96
+ <span style={{ fontVariantNumeric: 'tabular-nums' }}>{currentStats.generation}</span>
97
+ </div>
98
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
99
+ <span>Active Memes:</span>
100
+ <span style={{ fontVariantNumeric: 'tabular-nums' }}>{currentStats.activeMemesCount}</span>
101
+ </div>
102
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
103
+ <span>Total Hexes:</span>
104
+ <span style={{ fontVariantNumeric: 'tabular-nums' }}>{currentStats.totalHexesInfected}</span>
105
+ </div>
106
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
107
+ <span>Birth/Death Ratio:</span>
108
+ <span style={{ fontVariantNumeric: 'tabular-nums' }}>
109
+ {currentStats.populationStability.toFixed(2)}
110
+ </span>
111
+ </div>
112
+ {allTimeRecords && (
113
+ <div style={{ marginTop: 8, paddingTop: 8, borderTop: '1px solid rgba(0, 255, 255, 0.2)' }}>
114
+ <div style={{ fontSize: 10, color: '#00ffff', marginBottom: 4 }}>All-Time Records:</div>
115
+ <div style={{ fontSize: 10, marginBottom: 2 }}>
116
+ Highest Territory: {allTimeRecords.highestTerritory.value}
117
+ </div>
118
+ <div style={{ fontSize: 10, marginBottom: 2 }}>
119
+ Longest Streak: {allTimeRecords.longestSurvivalStreak.value}
120
+ </div>
121
+ </div>
122
+ )}
123
+ </div>
124
+ </details>
125
+ )}
126
+
127
+ {/* Leaderboard (Collapsible) */}
128
+ {leaderboard && leaderboard.length > 0 && (
129
+ <details style={{ marginBottom: 8, fontSize: 11 }}>
130
+ <summary style={{ cursor: 'pointer', color: '#00ffff', marginBottom: 4 }}>
131
+ Top 10 Leaderboard
132
+ </summary>
133
+ <div style={{ padding: '8px', background: 'rgba(0, 255, 255, 0.05)', borderRadius: 4 }}>
134
+ {leaderboard.map((entry, i) => (
135
+ <div
136
+ key={entry.photoId}
137
+ style={{
138
+ display: 'flex',
139
+ justifyContent: 'space-between',
140
+ marginBottom: 2,
141
+ fontSize: 10,
142
+ color: i < 3 ? '#00ff00' : '#00ffff'
143
+ }}
144
+ >
145
+ <span>
146
+ {i + 1}. {entry.photoId.slice(0, 20)}...
147
+ </span>
148
+ <span style={{ fontVariantNumeric: 'tabular-nums' }}>{entry.territory} hexes</span>
149
+ </div>
150
+ ))}
151
+ </div>
152
+ </details>
153
+ )}
154
+
155
+ {/* Messages Feed */}
156
+ <div
157
+ ref={scrollContainerRef}
158
+ style={{
159
+ flex: 1,
160
+ overflowY: 'auto',
161
+ maxHeight: '400px',
162
+ padding: '8px',
163
+ background: 'rgba(0, 0, 0, 0.3)',
164
+ borderRadius: 4
165
+ }}
166
+ >
167
+ {messages.length === 0 ? (
168
+ <div style={{ color: 'rgba(0, 255, 255, 0.5)', fontStyle: 'italic', textAlign: 'center', padding: '20px' }}>
169
+ No narration yet. Evolution in progress...
170
+ </div>
171
+ ) : (
172
+ messages.map((msg, index) => (
173
+ <div
174
+ key={`${msg.generation}-${index}`}
175
+ style={{
176
+ marginBottom: 8,
177
+ padding: '6px',
178
+ background: msg.priority >= 8 ? 'rgba(255, 165, 0, 0.1)' : 'rgba(0, 255, 255, 0.05)',
179
+ borderRadius: 4,
180
+ borderLeft: `2px solid ${msg.priority >= 8 ? '#ffaa00' : '#00ffff'}`,
181
+ fontSize: 11,
182
+ lineHeight: 1.4
183
+ }}
184
+ >
185
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 2 }}>
186
+ <span style={{ color: 'rgba(0, 255, 255, 0.7)', fontSize: 10 }}>
187
+ Gen {msg.generation}
188
+ </span>
189
+ <span style={{ color: 'rgba(0, 255, 255, 0.5)', fontSize: 9 }}>
190
+ {new Date(msg.timestamp).toLocaleTimeString()}
191
+ </span>
192
+ </div>
193
+ <div style={{ fontFamily: "'Courier New', monospace" }}>
194
+ {msg.text}
195
+ {msg.sparkline && (
196
+ <div
197
+ style={{
198
+ marginTop: 4,
199
+ fontFamily: "'Courier New', monospace",
200
+ fontSize: 14,
201
+ color: msg.eventType === 'slam_dunk' || msg.eventType === 'on_fire'
202
+ ? '#00ff00'
203
+ : msg.eventType === 'decline' || msg.eventType === 'missed_shot'
204
+ ? '#ff4444'
205
+ : '#00ffff',
206
+ letterSpacing: '2px'
207
+ }}
208
+ >
209
+ {msg.sparkline}
210
+ </div>
211
+ )}
212
+ </div>
213
+ </div>
214
+ ))
215
+ )}
216
+ <div ref={messagesEndRef} />
217
+ </div>
218
+ </div>
219
+ )
220
+ }
221
+
@@ -0,0 +1,2 @@
1
+ export { HexGrid } from './HexGrid';
2
+ export type { Photo, HexGridProps } from './HexGrid';
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Feature flags for HexGrid 3D
3
+ *
4
+ * Allows enabling/disabling features at runtime for different client environments
5
+ */
6
+
7
+ export interface HexGridFeatureFlags {
8
+ /** Enable/disable play-by-play narration overlay */
9
+ enableNarration?: boolean
10
+
11
+ /** Enable/disable statistics tracking and display */
12
+ enableStats?: boolean
13
+
14
+ /** Enable/disable debug panel */
15
+ enableDebugPanel?: boolean
16
+
17
+ /** Enable/disable camera controls UI */
18
+ enableCameraControls?: boolean
19
+
20
+ /** Enable/disable worker-based rendering */
21
+ enableWorker?: boolean
22
+
23
+ /** Enable/disable texture/image loading */
24
+ enableTextures?: boolean
25
+
26
+ /** Enable/disable evolution/animation system */
27
+ enableEvolution?: boolean
28
+
29
+ /** Enable/disable autoplay functionality */
30
+ enableAutoplay?: boolean
31
+
32
+ /** Enable/disable user interactions (clicks, drags) */
33
+ enableInteractions?: boolean
34
+
35
+ /** Enable/disable keyboard shortcuts */
36
+ enableKeyboardShortcuts?: boolean
37
+
38
+ /** Enable/disable performance telemetry */
39
+ enableTelemetry?: boolean
40
+
41
+ /** Enable/disable sheen/visual effects */
42
+ enableVisualEffects?: boolean
43
+
44
+ /** Enable/disable leaderboard system */
45
+ enableLeaderboard?: boolean
46
+ }
47
+
48
+ /**
49
+ * Default feature flags - all features enabled
50
+ */
51
+ export const DEFAULT_FEATURE_FLAGS: Required<HexGridFeatureFlags> = {
52
+ enableNarration: true,
53
+ enableStats: true,
54
+ enableDebugPanel: true,
55
+ enableCameraControls: true,
56
+ enableWorker: true,
57
+ enableTextures: true,
58
+ enableEvolution: true,
59
+ enableAutoplay: true,
60
+ enableInteractions: true,
61
+ enableKeyboardShortcuts: true,
62
+ enableTelemetry: true,
63
+ enableVisualEffects: true,
64
+ enableLeaderboard: true,
65
+ }
66
+
67
+ /**
68
+ * Minimal feature flags - only core visualization
69
+ */
70
+ export const MINIMAL_FEATURE_FLAGS: Required<HexGridFeatureFlags> = {
71
+ enableNarration: false,
72
+ enableStats: false,
73
+ enableDebugPanel: false,
74
+ enableCameraControls: false,
75
+ enableWorker: true,
76
+ enableTextures: true,
77
+ enableEvolution: false,
78
+ enableAutoplay: false,
79
+ enableInteractions: true,
80
+ enableKeyboardShortcuts: false,
81
+ enableTelemetry: false,
82
+ enableVisualEffects: false,
83
+ enableLeaderboard: false,
84
+ }
85
+
86
+ /**
87
+ * Performance-focused feature flags
88
+ */
89
+ export const PERFORMANCE_FEATURE_FLAGS: Required<HexGridFeatureFlags> = {
90
+ enableNarration: false,
91
+ enableStats: true,
92
+ enableDebugPanel: false,
93
+ enableCameraControls: true,
94
+ enableWorker: true,
95
+ enableTextures: true,
96
+ enableEvolution: true,
97
+ enableAutoplay: false,
98
+ enableInteractions: true,
99
+ enableKeyboardShortcuts: true,
100
+ enableTelemetry: false,
101
+ enableVisualEffects: false,
102
+ enableLeaderboard: false,
103
+ }
104
+
105
+ /**
106
+ * Merge user-provided flags with defaults
107
+ */
108
+ export function mergeFeatureFlags(
109
+ userFlags?: Partial<HexGridFeatureFlags>
110
+ ): Required<HexGridFeatureFlags> {
111
+ return {
112
+ ...DEFAULT_FEATURE_FLAGS,
113
+ ...userFlags,
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Check if a feature is enabled
119
+ */
120
+ export function isFeatureEnabled(
121
+ flags: HexGridFeatureFlags,
122
+ feature: keyof HexGridFeatureFlags
123
+ ): boolean {
124
+ return flags[feature] !== false
125
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ // Main package exports
2
+ export * from './components'
3
+ export * from './stores'
4
+ export * from './features'
5
+
6
+ // Export pure mathematical functions
7
+ export * from './workers/hexgrid-math'
8
+ export * from './utils/image-utils'
9
+
10
+ // Export additional types that aren't in components/stores
11
+ export type { WorkerDebug, Photo, GridItem } from './types'
12
+
13
+ // ═══════════════════════════════════════════════════════════════════════════
14
+ // ENHANCED HEXGRID EXPORTS
15
+ // ═══════════════════════════════════════════════════════════════════════════
16
+
17
+ // Math library
18
+ export * from './math'
19
+
20
+ // Algorithms (graph, clustering, flow, particles, fluid)
21
+ export * from './algorithms'
22
+
23
+ // WASM acceleration layer
24
+ export * from './wasm'
25
+
26
+ // Unified Snapshot API
27
+ export * from './Snapshot'
28
+
29
+ // Enhanced HexGrid engine with all features integrated
30
+ export * from './HexGridEnhanced'
@@ -0,0 +1,15 @@
1
+ export class Axial {
2
+ q: number;
3
+ r: number;
4
+
5
+ constructor(q: number, r: number) {
6
+ this.q = q;
7
+ this.r = r;
8
+ }
9
+
10
+ static fromPixel(x: number, y: number, hexSize: number): Axial {
11
+ const q = (Math.sqrt(3) / 3 * x - (1 / 3) * y) / hexSize;
12
+ const r = ((2 / 3) * y) / hexSize;
13
+ return new Axial(Math.round(q), Math.round(r));
14
+ }
15
+ }
@@ -0,0 +1,35 @@
1
+ import { Vector3 } from './Vector3';
2
+
3
+ export class Matrix4 {
4
+ private elements: number[];
5
+
6
+ constructor(elements?: number[]) {
7
+ this.elements = elements ?? Matrix4.identity().elements;
8
+ }
9
+
10
+ static identity(): Matrix4 {
11
+ return new Matrix4([
12
+ 1, 0, 0, 0,
13
+ 0, 1, 0, 0,
14
+ 0, 0, 1, 0,
15
+ 0, 0, 0, 1,
16
+ ]);
17
+ }
18
+
19
+ static translation(x: number, y: number, z: number): Matrix4 {
20
+ return new Matrix4([
21
+ 1, 0, 0, x,
22
+ 0, 1, 0, y,
23
+ 0, 0, 1, z,
24
+ 0, 0, 0, 1,
25
+ ]);
26
+ }
27
+
28
+ transformPoint(point: Vector3): Vector3 {
29
+ const e = this.elements;
30
+ const x = point.x * e[0] + point.y * e[1] + point.z * e[2] + e[3];
31
+ const y = point.x * e[4] + point.y * e[5] + point.z * e[6] + e[7];
32
+ const z = point.x * e[8] + point.y * e[9] + point.z * e[10] + e[11];
33
+ return new Vector3(x, y, z);
34
+ }
35
+ }
@@ -0,0 +1,37 @@
1
+ import { Vector3 } from './Vector3';
2
+
3
+ export class Quaternion {
4
+ x: number;
5
+ y: number;
6
+ z: number;
7
+ w: number;
8
+
9
+ constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) {
10
+ this.x = x;
11
+ this.y = y;
12
+ this.z = z;
13
+ this.w = w;
14
+ }
15
+
16
+ static identity(): Quaternion {
17
+ return new Quaternion(0, 0, 0, 1);
18
+ }
19
+
20
+ rotateVector(vector: Vector3): Vector3 {
21
+ const qx = this.x;
22
+ const qy = this.y;
23
+ const qz = this.z;
24
+ const qw = this.w;
25
+
26
+ const ix = qw * vector.x + qy * vector.z - qz * vector.y;
27
+ const iy = qw * vector.y + qz * vector.x - qx * vector.z;
28
+ const iz = qw * vector.z + qx * vector.y - qy * vector.x;
29
+ const iw = -qx * vector.x - qy * vector.y - qz * vector.z;
30
+
31
+ return new Vector3(
32
+ ix * qw + iw * -qx + iy * -qz - iz * -qy,
33
+ iy * qw + iw * -qy + iz * -qx - ix * -qz,
34
+ iz * qw + iw * -qz + ix * -qy - iy * -qx
35
+ );
36
+ }
37
+ }
@@ -0,0 +1,114 @@
1
+ import { Vector2 } from './Vector3';
2
+
3
+ type Point = [number, number];
4
+
5
+ export interface KDTreeResult<T> {
6
+ data: T;
7
+ distance: number;
8
+ }
9
+
10
+ export class KDTree<T> {
11
+ private points: Point[];
12
+ private data: T[];
13
+
14
+ private constructor(points: Point[], data: T[]) {
15
+ this.points = points;
16
+ this.data = data;
17
+ }
18
+
19
+ static build<T>(points: Point[], data: T[], _dimensions: number): KDTree<T> {
20
+ return new KDTree(points, data);
21
+ }
22
+
23
+ kNearest(target: Point, k: number): Array<KDTreeResult<T>> {
24
+ const results = this.points.map((point, index) => ({
25
+ data: this.data[index],
26
+ distance: KDTree.distance(point, target),
27
+ }));
28
+
29
+ return results.sort((a, b) => a.distance - b.distance).slice(0, k);
30
+ }
31
+
32
+ rangeQuery(target: Point, radius: number): Array<KDTreeResult<T>> {
33
+ return this.points
34
+ .map((point, index) => ({
35
+ data: this.data[index],
36
+ distance: KDTree.distance(point, target),
37
+ }))
38
+ .filter((result) => result.distance <= radius);
39
+ }
40
+
41
+ private static distance(a: Point, b: Point): number {
42
+ const dx = a[0] - b[0];
43
+ const dy = a[1] - b[1];
44
+ return Math.sqrt(dx * dx + dy * dy);
45
+ }
46
+ }
47
+
48
+ export interface SpatialHashEntry<T> {
49
+ data: T;
50
+ position: Point;
51
+ }
52
+
53
+ export class SpatialHashGrid<T> {
54
+ private cellSize: number;
55
+ private grid: Map<string, SpatialHashEntry<T>[]> = new Map();
56
+
57
+ constructor(cellSize: number, _dimensions: number) {
58
+ this.cellSize = cellSize;
59
+ }
60
+
61
+ insert(position: Point, data: T): void {
62
+ const key = this.keyFor(position);
63
+ const bucket = this.grid.get(key) ?? [];
64
+ bucket.push({ data, position });
65
+ this.grid.set(key, bucket);
66
+ }
67
+
68
+ query(position: Point, radius: number): Array<SpatialHashEntry<T>> {
69
+ const cellsToCheck = this.nearbyKeys(position, radius);
70
+ const results: Array<SpatialHashEntry<T>> = [];
71
+
72
+ for (const key of cellsToCheck) {
73
+ const bucket = this.grid.get(key);
74
+ if (!bucket) continue;
75
+ for (const entry of bucket) {
76
+ const distance = Math.hypot(
77
+ entry.position[0] - position[0],
78
+ entry.position[1] - position[1]
79
+ );
80
+ if (distance <= radius) {
81
+ results.push(entry);
82
+ }
83
+ }
84
+ }
85
+
86
+ return results;
87
+ }
88
+
89
+ private keyFor(position: Point): string {
90
+ const x = Math.floor(position[0] / this.cellSize);
91
+ const y = Math.floor(position[1] / this.cellSize);
92
+ return `${x},${y}`;
93
+ }
94
+
95
+ private nearbyKeys(position: Point, radius: number): string[] {
96
+ const minX = Math.floor((position[0] - radius) / this.cellSize);
97
+ const maxX = Math.floor((position[0] + radius) / this.cellSize);
98
+ const minY = Math.floor((position[1] - radius) / this.cellSize);
99
+ const maxY = Math.floor((position[1] + radius) / this.cellSize);
100
+
101
+ const keys: string[] = [];
102
+ for (let x = minX; x <= maxX; x++) {
103
+ for (let y = minY; y <= maxY; y++) {
104
+ keys.push(`${x},${y}`);
105
+ }
106
+ }
107
+ return keys;
108
+ }
109
+ }
110
+
111
+ export interface SpatialNode {
112
+ position: Vector2;
113
+ data: unknown;
114
+ }
@@ -0,0 +1,69 @@
1
+ export class Vector2 {
2
+ x: number;
3
+ y: number;
4
+
5
+ constructor(x: number = 0, y: number = 0) {
6
+ this.x = x;
7
+ this.y = y;
8
+ }
9
+
10
+ add(other: Vector2): Vector2 {
11
+ return new Vector2(this.x + other.x, this.y + other.y);
12
+ }
13
+
14
+ subtract(other: Vector2): Vector2 {
15
+ return new Vector2(this.x - other.x, this.y - other.y);
16
+ }
17
+
18
+ scale(factor: number): Vector2 {
19
+ return new Vector2(this.x * factor, this.y * factor);
20
+ }
21
+
22
+ length(): number {
23
+ return Math.sqrt(this.x * this.x + this.y * this.y);
24
+ }
25
+
26
+ normalize(): Vector2 {
27
+ const len = this.length();
28
+ if (len === 0) {
29
+ return new Vector2(0, 0);
30
+ }
31
+ return new Vector2(this.x / len, this.y / len);
32
+ }
33
+
34
+ distanceTo(other: Vector2): number {
35
+ const dx = this.x - other.x;
36
+ const dy = this.y - other.y;
37
+ return Math.sqrt(dx * dx + dy * dy);
38
+ }
39
+ }
40
+
41
+ export class Vector3 {
42
+ x: number;
43
+ y: number;
44
+ z: number;
45
+
46
+ constructor(x: number = 0, y: number = 0, z: number = 0) {
47
+ this.x = x;
48
+ this.y = y;
49
+ this.z = z;
50
+ }
51
+
52
+ static fromLatLng(latitude: number, longitude: number, radius: number = 1): Vector3 {
53
+ const latRad = (latitude * Math.PI) / 180;
54
+ const lonRad = (longitude * Math.PI) / 180;
55
+
56
+ const x = radius * Math.cos(latRad) * Math.cos(lonRad);
57
+ const y = radius * Math.cos(latRad) * Math.sin(lonRad);
58
+ const z = radius * Math.sin(latRad);
59
+
60
+ return new Vector3(x, y, z);
61
+ }
62
+
63
+ distanceTo(other: Vector3): number {
64
+ const dx = this.x - other.x;
65
+ const dy = this.y - other.y;
66
+ const dz = this.z - other.z;
67
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
68
+ }
69
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Math Module Exports
3
+ *
4
+ * @module math
5
+ */
6
+
7
+ export * from './Vector3'
8
+ export * from './Matrix4'
9
+ export * from './Quaternion'
10
+ export * from './HexCoordinates'
11
+ export * from './SpatialIndex'