@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.
- package/.eslintrc.json +28 -0
- package/LICENSE +39 -0
- package/README.md +291 -0
- package/examples/basic-usage.tsx +52 -0
- package/package.json +65 -0
- package/public/hexgrid-worker.js +1763 -0
- package/rust/Cargo.toml +41 -0
- package/rust/src/lib.rs +740 -0
- package/rust/src/math.rs +574 -0
- package/rust/src/spatial.rs +245 -0
- package/rust/src/statistics.rs +496 -0
- package/src/HexGridEnhanced.ts +16 -0
- package/src/Snapshot.ts +1402 -0
- package/src/adapters.ts +65 -0
- package/src/algorithms/AdvancedStatistics.ts +328 -0
- package/src/algorithms/BayesianStatistics.ts +317 -0
- package/src/algorithms/FlowField.ts +126 -0
- package/src/algorithms/FluidSimulation.ts +99 -0
- package/src/algorithms/GraphAlgorithms.ts +184 -0
- package/src/algorithms/OutlierDetection.ts +391 -0
- package/src/algorithms/ParticleSystem.ts +85 -0
- package/src/algorithms/index.ts +13 -0
- package/src/compat.ts +96 -0
- package/src/components/HexGrid.tsx +31 -0
- package/src/components/NarrationOverlay.tsx +221 -0
- package/src/components/index.ts +2 -0
- package/src/features.ts +125 -0
- package/src/index.ts +30 -0
- package/src/math/HexCoordinates.ts +15 -0
- package/src/math/Matrix4.ts +35 -0
- package/src/math/Quaternion.ts +37 -0
- package/src/math/SpatialIndex.ts +114 -0
- package/src/math/Vector3.ts +69 -0
- package/src/math/index.ts +11 -0
- package/src/note-adapter.ts +124 -0
- package/src/ontology-adapter.ts +77 -0
- package/src/stores/index.ts +1 -0
- package/src/stores/uiStore.ts +85 -0
- package/src/types/index.ts +3 -0
- package/src/types.ts +152 -0
- package/src/utils/image-utils.ts +25 -0
- package/src/wasm/HexGridWasmWrapper.ts +753 -0
- package/src/wasm/index.ts +7 -0
- package/src/workers/hexgrid-math.ts +177 -0
- package/src/workers/hexgrid-worker.worker.ts +1807 -0
- 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
|
+
|
package/src/features.ts
ADDED
|
@@ -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
|
+
}
|