@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,124 @@
1
+ /**
2
+ * Adapter for embedding notes directly in the grid
3
+ *
4
+ * This adapter allows notes (Cyrano's internal notes, user notes, etc.)
5
+ * to be embedded directly in GridItems with full metadata preservation.
6
+ */
7
+
8
+ import type { GridItem } from './types'
9
+ import type { ItemAdapter, AdapterOptions } from './adapters'
10
+
11
+ /**
12
+ * Note content structure
13
+ */
14
+ export interface NoteContent {
15
+ title?: string
16
+ text: string
17
+ summary?: string
18
+ }
19
+
20
+ /**
21
+ * Note metadata structure
22
+ */
23
+ export interface NoteMetadata {
24
+ type?: string
25
+ category?: string
26
+ subcategory?: string
27
+ tags?: string[]
28
+ targetUserId?: string
29
+ status?: 'active' | 'archived' | 'draft'
30
+ priority?: 'low' | 'medium' | 'high' | 'urgent'
31
+ context?: string
32
+ permissions?: 'private' | 'shared' | 'public'
33
+ sharedWith?: string[]
34
+ }
35
+
36
+ /**
37
+ * Note structure matching Affectively's note format
38
+ */
39
+ export interface Note {
40
+ id: string
41
+ content: NoteContent
42
+ metadata?: NoteMetadata
43
+ date: string
44
+ }
45
+
46
+ /**
47
+ * Calculate velocity for a note based on recency, priority, and metadata richness
48
+ */
49
+ function calculateNoteVelocity(note: Note): number {
50
+ let velocity = 0.1 // Base minimum
51
+
52
+ // Priority contribution (0-0.3)
53
+ const priorityMap: Record<string, number> = {
54
+ urgent: 0.3,
55
+ high: 0.2,
56
+ medium: 0.1,
57
+ low: 0.05,
58
+ }
59
+ if (note.metadata?.priority) {
60
+ velocity += priorityMap[note.metadata.priority] || 0.1
61
+ }
62
+
63
+ // Recency contribution (0-0.4)
64
+ if (note.date) {
65
+ const ageMs = Date.now() - new Date(note.date).getTime()
66
+ const ageHours = ageMs / (1000 * 60 * 60)
67
+ const recencyFactor = Math.max(0, 1 - ageHours / 168) // Decay over 1 week
68
+ velocity += recencyFactor * 0.4
69
+ }
70
+
71
+ // Metadata richness contribution (0-0.2)
72
+ let contextScore = 0
73
+ if (note.metadata?.type) contextScore += 0.05
74
+ if (note.metadata?.category) contextScore += 0.05
75
+ if (note.metadata?.tags && note.metadata.tags.length > 0) contextScore += 0.05
76
+ if (note.metadata?.context) contextScore += 0.05
77
+ velocity += Math.min(contextScore, 0.2)
78
+
79
+ // Clamp to [0.1, 1.0]
80
+ return Math.max(0.1, Math.min(1.0, velocity))
81
+ }
82
+
83
+ /**
84
+ * Adapter for embedding notes directly
85
+ */
86
+ export const noteAdapter: ItemAdapter<Note> = {
87
+ toGridItem(note: Note, options?: AdapterOptions): GridItem<Note> {
88
+ const velocity = options?.velocity ?? calculateNoteVelocity(note)
89
+
90
+ return {
91
+ id: note.id,
92
+ type: 'note',
93
+ title: note.content.title || 'Untitled Note',
94
+ description: note.content.summary || note.content.text.substring(0, 200),
95
+ data: note,
96
+ createdAt: note.date,
97
+ velocity,
98
+ // Notes can have generated visualizations
99
+ imageUrl: options?.visualUrl || `/api/notes/${note.id}/visualization`,
100
+ category: note.metadata?.category,
101
+ // Store metadata in metrics for filtering/sorting
102
+ metrics: {
103
+ priority: note.metadata?.priority === 'urgent' ? 4 : note.metadata?.priority === 'high' ? 3 : note.metadata?.priority === 'medium' ? 2 : 1,
104
+ tagCount: note.metadata?.tags?.length || 0,
105
+ },
106
+ }
107
+ },
108
+
109
+ fromGridItem(item: GridItem<Note>): Note {
110
+ if (!item.data) {
111
+ throw new Error('GridItem missing note data')
112
+ }
113
+ return item.data
114
+ },
115
+
116
+ calculateVelocity(note: Note): number {
117
+ return calculateNoteVelocity(note)
118
+ },
119
+
120
+ extractVisualUrl(note: Note): string | undefined {
121
+ // Notes can have generated visualizations
122
+ return `/api/notes/${note.id}/visualization`
123
+ },
124
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Adapter for embedding ontology entities directly in the grid
3
+ *
4
+ * This adapter allows ontology entities from @emotions-app/shared-utils/ontology
5
+ * to be embedded directly in GridItems with full metadata preservation.
6
+ */
7
+
8
+ import type { OntologyEntity } from '@emotions-app/shared-utils/ontology/types'
9
+ import type { GridItem } from './types'
10
+ import type { ItemAdapter, AdapterOptions } from './adapters'
11
+
12
+ /**
13
+ * Calculate velocity for an ontology entity based on provenance confidence
14
+ * and recency
15
+ */
16
+ function calculateEntityVelocity(entity: OntologyEntity): number {
17
+ let velocity = 0.1 // Base minimum
18
+
19
+ // Use provenance confidence (0-1) as primary factor
20
+ const confidence = entity.metadata.provenance.confidence
21
+ velocity += confidence * 0.5
22
+
23
+ // Recency factor based on lastModified
24
+ if (entity.metadata.lastModified) {
25
+ const ageMs = Date.now() - new Date(entity.metadata.lastModified).getTime()
26
+ const ageHours = ageMs / (1000 * 60 * 60)
27
+ const recencyFactor = Math.max(0, 1 - ageHours / 168) // Decay over 1 week
28
+ velocity += recencyFactor * 0.4
29
+ }
30
+
31
+ // Clamp to [0.1, 1.0]
32
+ return Math.max(0.1, Math.min(1.0, velocity))
33
+ }
34
+
35
+ /**
36
+ * Adapter for embedding ontology entities directly
37
+ */
38
+ export const ontologyEntityAdapter: ItemAdapter<OntologyEntity> = {
39
+ toGridItem(entity: OntologyEntity, options?: AdapterOptions): GridItem<OntologyEntity> {
40
+ const velocity = options?.velocity ?? calculateEntityVelocity(entity)
41
+
42
+ return {
43
+ id: entity['@id'],
44
+ type: 'ontology-entity',
45
+ title: entity.label,
46
+ description: (entity.properties.description as string | undefined) || entity.label,
47
+ data: entity,
48
+ ontologyMetadata: {
49
+ entityId: entity['@id'],
50
+ entityType: Array.isArray(entity['@type'])
51
+ ? entity['@type']
52
+ : [entity['@type']],
53
+ properties: entity.properties,
54
+ provenance: entity.metadata.provenance,
55
+ },
56
+ velocity,
57
+ createdAt: entity.metadata.lastModified || entity.metadata.provenance.extractedAt,
58
+ // Extract visual URL if available in properties
59
+ imageUrl: options?.visualUrl || (entity.properties.imageUrl as string | undefined),
60
+ }
61
+ },
62
+
63
+ fromGridItem(item: GridItem<OntologyEntity>): OntologyEntity {
64
+ if (!item.data) {
65
+ throw new Error('GridItem missing ontology entity data')
66
+ }
67
+ return item.data
68
+ },
69
+
70
+ calculateVelocity(entity: OntologyEntity): number {
71
+ return calculateEntityVelocity(entity)
72
+ },
73
+
74
+ extractVisualUrl(entity: OntologyEntity): string | undefined {
75
+ return entity.properties.imageUrl as string | undefined
76
+ },
77
+ }
@@ -0,0 +1 @@
1
+ export const hexgridStores = {};
@@ -0,0 +1,85 @@
1
+ type UIState = {
2
+ debugOpen: boolean
3
+ showStats: boolean
4
+ cameraOpen?: boolean
5
+ showNarration?: boolean
6
+ }
7
+
8
+ // Safe localStorage helpers that never throw
9
+ const safeGetItem = (key: string): string | null => {
10
+ // istanbul ignore next
11
+ if (typeof window === 'undefined') return null
12
+ try {
13
+ return window.localStorage.getItem(key)
14
+ } catch {
15
+ // istanbul ignore next
16
+ return null
17
+ }
18
+ }
19
+
20
+ const safeSetItem = (key: string, value: string): void => {
21
+ // istanbul ignore next
22
+ if (typeof window === 'undefined') return
23
+ try {
24
+ window.localStorage.setItem(key, value)
25
+ } catch {
26
+ // istanbul ignore next - private browsing, quota exceeded
27
+ }
28
+ }
29
+
30
+ // Initialize showNarration from localStorage if available
31
+ const savedNarration = safeGetItem('hexgrid.showNarration')
32
+ const initialShowNarration = savedNarration === 'true'
33
+
34
+ const state: UIState = {
35
+ debugOpen: false,
36
+ showStats: false,
37
+ cameraOpen: false,
38
+ showNarration: initialShowNarration
39
+ }
40
+
41
+ const listeners = new Set<(s: UIState) => void>()
42
+
43
+ const uiStore = {
44
+ getState(): UIState {
45
+ return { ...state }
46
+ },
47
+ set(partial: Partial<UIState>) {
48
+ let changed = false
49
+ for (const k of Object.keys(partial) as (keyof UIState)[]) {
50
+ if (partial[k] !== undefined && state[k] !== partial[k]) {
51
+ // @ts-ignore
52
+ state[k] = partial[k]
53
+ changed = true
54
+ }
55
+ }
56
+ if (changed) {
57
+ // Persist showNarration to localStorage for cross-refresh consistency
58
+ if (partial.showNarration !== undefined) {
59
+ safeSetItem('hexgrid.showNarration', String(!!partial.showNarration))
60
+ }
61
+ for (const cb of Array.from(listeners)) cb({ ...state })
62
+ }
63
+ },
64
+ subscribe(cb: (s: UIState) => void) {
65
+ listeners.add(cb)
66
+ // emit current immediately
67
+ cb({ ...state })
68
+ return () => listeners.delete(cb)
69
+ },
70
+ toggleDebug() {
71
+ this.set({ debugOpen: !state.debugOpen })
72
+ },
73
+ toggleStats() {
74
+ this.set({ showStats: !state.showStats })
75
+ },
76
+ toggleCamera() {
77
+ this.set({ cameraOpen: !state.cameraOpen })
78
+ },
79
+ toggleNarration() {
80
+ this.set({ showNarration: !state.showNarration })
81
+ }
82
+ }
83
+
84
+ export { uiStore }
85
+ export default uiStore
@@ -0,0 +1,3 @@
1
+ export interface WorkerDebug {
2
+ enabled: boolean;
3
+ }
package/src/types.ts ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Type definitions for HexGrid Visualization
3
+ */
4
+
5
+ import type { RefObject } from 'react'
6
+ import type { HexGridFeatureFlags } from './features'
7
+
8
+ /**
9
+ * Photo type for HexGrid visualization
10
+ *
11
+ * This is the unified Photo type used throughout the hexgrid-3d package.
12
+ * It includes all fields needed for display, media playback, and analytics.
13
+ */
14
+ export interface Photo {
15
+ id: string
16
+ title: string
17
+
18
+ // Image URLs - imageUrl is primary, url is for backward compatibility
19
+ imageUrl: string
20
+ url?: string // Alias for imageUrl (backward compatibility)
21
+ thumbnailUrl?: string
22
+
23
+ // Display metadata
24
+ alt: string
25
+ category: string
26
+ description?: string
27
+
28
+ // Source information
29
+ source: string
30
+ sourceUrl?: string
31
+ createdAt?: string
32
+
33
+ // Shop integration
34
+ shopUrl?: string
35
+ location?: string
36
+
37
+ // Media type flags
38
+ isVideo?: boolean
39
+ videoUrl?: string
40
+ isTweet?: boolean
41
+ tweetUrl?: string
42
+ redditUrl?: string
43
+ durationSeconds?: number
44
+
45
+ // Competition/ranking system
46
+ velocity?: number // Normalized velocity [0.1, 1.0] for meritocratic competition
47
+
48
+ // User info
49
+ userId?: string
50
+ username?: string
51
+ platform?: string
52
+ author?: string
53
+ authorUrl?: string
54
+
55
+ // Metrics for analytics
56
+ views?: number
57
+ likes?: number
58
+ comments?: number
59
+ shares?: number
60
+ upvotes?: number
61
+ retweets?: number
62
+ replies?: number
63
+ age_in_hours?: number
64
+
65
+ // Visual
66
+ dominantColor?: string
67
+ }
68
+
69
+ /**
70
+ * Generic item that can represent any data object in the grid
71
+ * Extends Photo for backward compatibility while adding generic data support
72
+ */
73
+ export interface GridItem<T = unknown> {
74
+ // Core identification
75
+ id: string
76
+ type: string // 'photo' | 'note' | 'emotion' | 'ontology-entity' | 'custom'
77
+
78
+ // Visual representation (optional - allows non-visual items)
79
+ imageUrl?: string
80
+ thumbnailUrl?: string
81
+ videoUrl?: string
82
+
83
+ // Display metadata
84
+ title?: string
85
+ alt?: string
86
+ description?: string
87
+ category?: string
88
+
89
+ // Embedded data object (preserves original type)
90
+ data?: T
91
+
92
+ // Ontology metadata (optional, for ontology-aware items)
93
+ ontologyMetadata?: {
94
+ entityId?: string
95
+ entityType?: string | string[]
96
+ properties?: Record<string, unknown>
97
+ provenance?: {
98
+ source: string
99
+ extractedAt: string
100
+ confidence: number
101
+ }
102
+ }
103
+
104
+ // Grid behavior
105
+ velocity?: number
106
+ source?: string
107
+ sourceUrl?: string
108
+ createdAt?: string
109
+
110
+ // Metrics (flexible for any data type)
111
+ metrics?: Record<string, number>
112
+
113
+ // Legacy Photo fields (for backward compatibility)
114
+ url?: string // Maps to imageUrl
115
+ userId?: string
116
+ username?: string
117
+ platform?: string
118
+ author?: string
119
+ authorUrl?: string
120
+ likes?: number
121
+ views?: number
122
+ comments?: number
123
+ dominantColor?: string
124
+ }
125
+
126
+ export interface HexGridProps {
127
+ photos: Photo[]
128
+ onHexClick?: (photo: Photo) => void
129
+ spacing?: number
130
+ canvasRef?: RefObject<HTMLCanvasElement>
131
+ onLeaderboardUpdate?: (leaderboard: any) => void
132
+ autoplayQueueLimit?: number
133
+ onAutoplayQueueLimitChange?: (limit: number) => void
134
+ modalOpen?: boolean
135
+ userId?: string
136
+ username?: string
137
+ featureFlags?: HexGridFeatureFlags
138
+ }
139
+
140
+ export interface UIState {
141
+ debugOpen: boolean
142
+ showStats: boolean
143
+ cameraOpen: boolean
144
+ showNarration: boolean
145
+ }
146
+
147
+ export interface WorkerDebug {
148
+ curveUDeg: number
149
+ curveVDeg: number
150
+ batchPerFrame: number
151
+ [key: string]: any
152
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure utility functions for HexGrid component.
3
+ * These functions have NO side effects and are deterministic.
4
+ */
5
+
6
+ /**
7
+ * Get proxied image URL for preview.redd.it images (CORS issues).
8
+ * External preview URLs work fine without proxying.
9
+ * @pure - No side effects, deterministic
10
+ * @param imageUrl Original image URL
11
+ * @returns Proxied URL if needed, otherwise original URL
12
+ */
13
+ export function getProxiedImageUrl(imageUrl: string): string {
14
+ if (!imageUrl || typeof imageUrl !== 'string') {
15
+ return imageUrl
16
+ }
17
+
18
+ // Only proxy preview.redd.it URLs (they have CORS issues)
19
+ // external-preview.redd.it URLs work fine, so don't proxy them
20
+ if (imageUrl.includes('preview.redd.it') && !imageUrl.includes('external-preview.redd.it')) {
21
+ return `/api/proxy-image?url=${encodeURIComponent(imageUrl)}`
22
+ }
23
+
24
+ return imageUrl
25
+ }