@buley/hexgrid-3d 1.0.0 → 1.1.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/examples/basic-usage.tsx +19 -19
- package/package.json +1 -1
- package/public/hexgrid-worker.js +2350 -1638
- package/src/Snapshot.ts +790 -585
- package/src/adapters.ts +16 -18
- package/src/algorithms/AdvancedStatistics.ts +58 -24
- package/src/algorithms/BayesianStatistics.ts +43 -12
- package/src/algorithms/FlowField.ts +30 -6
- package/src/algorithms/FlowField3D.ts +573 -0
- package/src/algorithms/FluidSimulation.ts +19 -3
- package/src/algorithms/FluidSimulation3D.ts +664 -0
- package/src/algorithms/GraphAlgorithms.ts +19 -12
- package/src/algorithms/OutlierDetection.ts +72 -38
- package/src/algorithms/ParticleSystem.ts +12 -2
- package/src/algorithms/ParticleSystem3D.ts +546 -0
- package/src/algorithms/index.ts +14 -8
- package/src/compat.ts +10 -10
- package/src/components/HexGrid.tsx +10 -23
- package/src/components/NarrationOverlay.tsx +139 -51
- package/src/components/index.ts +2 -1
- package/src/features.ts +31 -31
- package/src/index.ts +11 -11
- package/src/math/HexCoordinates.ts +1 -1
- package/src/math/Matrix4.ts +2 -12
- package/src/math/Vector3.ts +5 -1
- package/src/math/index.ts +6 -6
- package/src/note-adapter.ts +50 -42
- package/src/ontology-adapter.ts +30 -23
- package/src/stores/uiStore.ts +34 -34
- package/src/types.ts +109 -98
- package/src/utils/image-utils.ts +9 -6
- package/src/wasm/HexGridWasmWrapper.ts +436 -388
- package/src/wasm/index.ts +2 -2
- package/src/workers/hexgrid-math.ts +40 -35
- package/src/workers/hexgrid-worker.worker.ts +1992 -1018
package/src/note-adapter.ts
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Adapter for embedding notes directly in the grid
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This adapter allows notes (Cyrano's internal notes, user notes, etc.)
|
|
5
5
|
* to be embedded directly in GridItems with full metadata preservation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { GridItem } from './types'
|
|
9
|
-
import type { ItemAdapter, AdapterOptions } from './adapters'
|
|
8
|
+
import type { GridItem } from './types';
|
|
9
|
+
import type { ItemAdapter, AdapterOptions } from './adapters';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Note content structure
|
|
13
13
|
*/
|
|
14
14
|
export interface NoteContent {
|
|
15
|
-
title?: string
|
|
16
|
-
text: string
|
|
17
|
-
summary?: string
|
|
15
|
+
title?: string;
|
|
16
|
+
text: string;
|
|
17
|
+
summary?: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Note metadata structure
|
|
22
22
|
*/
|
|
23
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[]
|
|
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
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
37
|
* Note structure matching Affectively's note format
|
|
38
38
|
*/
|
|
39
39
|
export interface Note {
|
|
40
|
-
id: string
|
|
41
|
-
content: NoteContent
|
|
42
|
-
metadata?: NoteMetadata
|
|
43
|
-
date: string
|
|
40
|
+
id: string;
|
|
41
|
+
content: NoteContent;
|
|
42
|
+
metadata?: NoteMetadata;
|
|
43
|
+
date: string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Calculate velocity for a note based on recency, priority, and metadata richness
|
|
48
48
|
*/
|
|
49
49
|
function calculateNoteVelocity(note: Note): number {
|
|
50
|
-
let velocity = 0.1 // Base minimum
|
|
50
|
+
let velocity = 0.1; // Base minimum
|
|
51
51
|
|
|
52
52
|
// Priority contribution (0-0.3)
|
|
53
53
|
const priorityMap: Record<string, number> = {
|
|
@@ -55,29 +55,30 @@ function calculateNoteVelocity(note: Note): number {
|
|
|
55
55
|
high: 0.2,
|
|
56
56
|
medium: 0.1,
|
|
57
57
|
low: 0.05,
|
|
58
|
-
}
|
|
58
|
+
};
|
|
59
59
|
if (note.metadata?.priority) {
|
|
60
|
-
velocity += priorityMap[note.metadata.priority] || 0.1
|
|
60
|
+
velocity += priorityMap[note.metadata.priority] || 0.1;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// Recency contribution (0-0.4)
|
|
64
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
|
|
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
69
|
}
|
|
70
70
|
|
|
71
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)
|
|
76
|
-
|
|
77
|
-
|
|
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)
|
|
76
|
+
contextScore += 0.05;
|
|
77
|
+
if (note.metadata?.context) contextScore += 0.05;
|
|
78
|
+
velocity += Math.min(contextScore, 0.2);
|
|
78
79
|
|
|
79
80
|
// Clamp to [0.1, 1.0]
|
|
80
|
-
return Math.max(0.1, Math.min(1.0, velocity))
|
|
81
|
+
return Math.max(0.1, Math.min(1.0, velocity));
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
/**
|
|
@@ -85,7 +86,7 @@ function calculateNoteVelocity(note: Note): number {
|
|
|
85
86
|
*/
|
|
86
87
|
export const noteAdapter: ItemAdapter<Note> = {
|
|
87
88
|
toGridItem(note: Note, options?: AdapterOptions): GridItem<Note> {
|
|
88
|
-
const velocity = options?.velocity ?? calculateNoteVelocity(note)
|
|
89
|
+
const velocity = options?.velocity ?? calculateNoteVelocity(note);
|
|
89
90
|
|
|
90
91
|
return {
|
|
91
92
|
id: note.id,
|
|
@@ -100,25 +101,32 @@ export const noteAdapter: ItemAdapter<Note> = {
|
|
|
100
101
|
category: note.metadata?.category,
|
|
101
102
|
// Store metadata in metrics for filtering/sorting
|
|
102
103
|
metrics: {
|
|
103
|
-
priority:
|
|
104
|
+
priority:
|
|
105
|
+
note.metadata?.priority === 'urgent'
|
|
106
|
+
? 4
|
|
107
|
+
: note.metadata?.priority === 'high'
|
|
108
|
+
? 3
|
|
109
|
+
: note.metadata?.priority === 'medium'
|
|
110
|
+
? 2
|
|
111
|
+
: 1,
|
|
104
112
|
tagCount: note.metadata?.tags?.length || 0,
|
|
105
113
|
},
|
|
106
|
-
}
|
|
114
|
+
};
|
|
107
115
|
},
|
|
108
116
|
|
|
109
117
|
fromGridItem(item: GridItem<Note>): Note {
|
|
110
118
|
if (!item.data) {
|
|
111
|
-
throw new Error('GridItem missing note data')
|
|
119
|
+
throw new Error('GridItem missing note data');
|
|
112
120
|
}
|
|
113
|
-
return item.data
|
|
121
|
+
return item.data;
|
|
114
122
|
},
|
|
115
123
|
|
|
116
124
|
calculateVelocity(note: Note): number {
|
|
117
|
-
return calculateNoteVelocity(note)
|
|
125
|
+
return calculateNoteVelocity(note);
|
|
118
126
|
},
|
|
119
127
|
|
|
120
128
|
extractVisualUrl(note: Note): string | undefined {
|
|
121
129
|
// Notes can have generated visualizations
|
|
122
|
-
return `/api/notes/${note.id}/visualization
|
|
130
|
+
return `/api/notes/${note.id}/visualization`;
|
|
123
131
|
},
|
|
124
|
-
}
|
|
132
|
+
};
|
package/src/ontology-adapter.ts
CHANGED
|
@@ -1,49 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Adapter for embedding ontology entities directly in the grid
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This adapter allows ontology entities from @emotions-app/shared-utils/ontology
|
|
5
5
|
* to be embedded directly in GridItems with full metadata preservation.
|
|
6
6
|
*/
|
|
7
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'
|
|
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
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Calculate velocity for an ontology entity based on provenance confidence
|
|
14
14
|
* and recency
|
|
15
15
|
*/
|
|
16
16
|
function calculateEntityVelocity(entity: OntologyEntity): number {
|
|
17
|
-
let velocity = 0.1 // Base minimum
|
|
17
|
+
let velocity = 0.1; // Base minimum
|
|
18
18
|
|
|
19
19
|
// Use provenance confidence (0-1) as primary factor
|
|
20
|
-
const confidence = entity.metadata.provenance.confidence
|
|
21
|
-
velocity += confidence * 0.5
|
|
20
|
+
const confidence = entity.metadata.provenance.confidence;
|
|
21
|
+
velocity += confidence * 0.5;
|
|
22
22
|
|
|
23
23
|
// Recency factor based on lastModified
|
|
24
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
|
|
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
29
|
}
|
|
30
30
|
|
|
31
31
|
// Clamp to [0.1, 1.0]
|
|
32
|
-
return Math.max(0.1, Math.min(1.0, velocity))
|
|
32
|
+
return Math.max(0.1, Math.min(1.0, velocity));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Adapter for embedding ontology entities directly
|
|
37
37
|
*/
|
|
38
38
|
export const ontologyEntityAdapter: ItemAdapter<OntologyEntity> = {
|
|
39
|
-
toGridItem(
|
|
40
|
-
|
|
39
|
+
toGridItem(
|
|
40
|
+
entity: OntologyEntity,
|
|
41
|
+
options?: AdapterOptions
|
|
42
|
+
): GridItem<OntologyEntity> {
|
|
43
|
+
const velocity = options?.velocity ?? calculateEntityVelocity(entity);
|
|
41
44
|
|
|
42
45
|
return {
|
|
43
46
|
id: entity['@id'],
|
|
44
47
|
type: 'ontology-entity',
|
|
45
48
|
title: entity.label,
|
|
46
|
-
description:
|
|
49
|
+
description:
|
|
50
|
+
(entity.properties.description as string | undefined) || entity.label,
|
|
47
51
|
data: entity,
|
|
48
52
|
ontologyMetadata: {
|
|
49
53
|
entityId: entity['@id'],
|
|
@@ -54,24 +58,27 @@ export const ontologyEntityAdapter: ItemAdapter<OntologyEntity> = {
|
|
|
54
58
|
provenance: entity.metadata.provenance,
|
|
55
59
|
},
|
|
56
60
|
velocity,
|
|
57
|
-
createdAt:
|
|
61
|
+
createdAt:
|
|
62
|
+
entity.metadata.lastModified || entity.metadata.provenance.extractedAt,
|
|
58
63
|
// Extract visual URL if available in properties
|
|
59
|
-
imageUrl:
|
|
60
|
-
|
|
64
|
+
imageUrl:
|
|
65
|
+
options?.visualUrl ||
|
|
66
|
+
(entity.properties.imageUrl as string | undefined),
|
|
67
|
+
};
|
|
61
68
|
},
|
|
62
69
|
|
|
63
70
|
fromGridItem(item: GridItem<OntologyEntity>): OntologyEntity {
|
|
64
71
|
if (!item.data) {
|
|
65
|
-
throw new Error('GridItem missing ontology entity data')
|
|
72
|
+
throw new Error('GridItem missing ontology entity data');
|
|
66
73
|
}
|
|
67
|
-
return item.data
|
|
74
|
+
return item.data;
|
|
68
75
|
},
|
|
69
76
|
|
|
70
77
|
calculateVelocity(entity: OntologyEntity): number {
|
|
71
|
-
return calculateEntityVelocity(entity)
|
|
78
|
+
return calculateEntityVelocity(entity);
|
|
72
79
|
},
|
|
73
80
|
|
|
74
81
|
extractVisualUrl(entity: OntologyEntity): string | undefined {
|
|
75
|
-
return entity.properties.imageUrl as string | undefined
|
|
82
|
+
return entity.properties.imageUrl as string | undefined;
|
|
76
83
|
},
|
|
77
|
-
}
|
|
84
|
+
};
|
package/src/stores/uiStore.ts
CHANGED
|
@@ -1,85 +1,85 @@
|
|
|
1
1
|
type UIState = {
|
|
2
|
-
debugOpen: boolean
|
|
3
|
-
showStats: boolean
|
|
4
|
-
cameraOpen?: boolean
|
|
5
|
-
showNarration?: boolean
|
|
6
|
-
}
|
|
2
|
+
debugOpen: boolean;
|
|
3
|
+
showStats: boolean;
|
|
4
|
+
cameraOpen?: boolean;
|
|
5
|
+
showNarration?: boolean;
|
|
6
|
+
};
|
|
7
7
|
|
|
8
8
|
// Safe localStorage helpers that never throw
|
|
9
9
|
const safeGetItem = (key: string): string | null => {
|
|
10
10
|
// istanbul ignore next
|
|
11
|
-
if (typeof window === 'undefined') return null
|
|
11
|
+
if (typeof window === 'undefined') return null;
|
|
12
12
|
try {
|
|
13
|
-
return window.localStorage.getItem(key)
|
|
13
|
+
return window.localStorage.getItem(key);
|
|
14
14
|
} catch {
|
|
15
15
|
// istanbul ignore next
|
|
16
|
-
return null
|
|
16
|
+
return null;
|
|
17
17
|
}
|
|
18
|
-
}
|
|
18
|
+
};
|
|
19
19
|
|
|
20
20
|
const safeSetItem = (key: string, value: string): void => {
|
|
21
21
|
// istanbul ignore next
|
|
22
|
-
if (typeof window === 'undefined') return
|
|
22
|
+
if (typeof window === 'undefined') return;
|
|
23
23
|
try {
|
|
24
|
-
window.localStorage.setItem(key, value)
|
|
24
|
+
window.localStorage.setItem(key, value);
|
|
25
25
|
} catch {
|
|
26
26
|
// istanbul ignore next - private browsing, quota exceeded
|
|
27
27
|
}
|
|
28
|
-
}
|
|
28
|
+
};
|
|
29
29
|
|
|
30
30
|
// Initialize showNarration from localStorage if available
|
|
31
|
-
const savedNarration = safeGetItem('hexgrid.showNarration')
|
|
32
|
-
const initialShowNarration = savedNarration === 'true'
|
|
31
|
+
const savedNarration = safeGetItem('hexgrid.showNarration');
|
|
32
|
+
const initialShowNarration = savedNarration === 'true';
|
|
33
33
|
|
|
34
34
|
const state: UIState = {
|
|
35
35
|
debugOpen: false,
|
|
36
36
|
showStats: false,
|
|
37
37
|
cameraOpen: false,
|
|
38
|
-
showNarration: initialShowNarration
|
|
39
|
-
}
|
|
38
|
+
showNarration: initialShowNarration,
|
|
39
|
+
};
|
|
40
40
|
|
|
41
|
-
const listeners = new Set<(s: UIState) => void>()
|
|
41
|
+
const listeners = new Set<(s: UIState) => void>();
|
|
42
42
|
|
|
43
43
|
const uiStore = {
|
|
44
44
|
getState(): UIState {
|
|
45
|
-
return { ...state }
|
|
45
|
+
return { ...state };
|
|
46
46
|
},
|
|
47
47
|
set(partial: Partial<UIState>) {
|
|
48
|
-
let changed = false
|
|
48
|
+
let changed = false;
|
|
49
49
|
for (const k of Object.keys(partial) as (keyof UIState)[]) {
|
|
50
50
|
if (partial[k] !== undefined && state[k] !== partial[k]) {
|
|
51
51
|
// @ts-ignore
|
|
52
|
-
state[k] = partial[k]
|
|
53
|
-
changed = true
|
|
52
|
+
state[k] = partial[k];
|
|
53
|
+
changed = true;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
if (changed) {
|
|
57
57
|
// Persist showNarration to localStorage for cross-refresh consistency
|
|
58
58
|
if (partial.showNarration !== undefined) {
|
|
59
|
-
safeSetItem('hexgrid.showNarration', String(!!partial.showNarration))
|
|
59
|
+
safeSetItem('hexgrid.showNarration', String(!!partial.showNarration));
|
|
60
60
|
}
|
|
61
|
-
for (const cb of Array.from(listeners)) cb({ ...state })
|
|
61
|
+
for (const cb of Array.from(listeners)) cb({ ...state });
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
subscribe(cb: (s: UIState) => void) {
|
|
65
|
-
listeners.add(cb)
|
|
65
|
+
listeners.add(cb);
|
|
66
66
|
// emit current immediately
|
|
67
|
-
cb({ ...state })
|
|
68
|
-
return () => listeners.delete(cb)
|
|
67
|
+
cb({ ...state });
|
|
68
|
+
return () => listeners.delete(cb);
|
|
69
69
|
},
|
|
70
70
|
toggleDebug() {
|
|
71
|
-
this.set({ debugOpen: !state.debugOpen })
|
|
71
|
+
this.set({ debugOpen: !state.debugOpen });
|
|
72
72
|
},
|
|
73
73
|
toggleStats() {
|
|
74
|
-
this.set({ showStats: !state.showStats })
|
|
74
|
+
this.set({ showStats: !state.showStats });
|
|
75
75
|
},
|
|
76
76
|
toggleCamera() {
|
|
77
|
-
this.set({ cameraOpen: !state.cameraOpen })
|
|
77
|
+
this.set({ cameraOpen: !state.cameraOpen });
|
|
78
78
|
},
|
|
79
79
|
toggleNarration() {
|
|
80
|
-
this.set({ showNarration: !state.showNarration })
|
|
81
|
-
}
|
|
82
|
-
}
|
|
80
|
+
this.set({ showNarration: !state.showNarration });
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
83
|
|
|
84
|
-
export { uiStore }
|
|
85
|
-
export default uiStore
|
|
84
|
+
export { uiStore };
|
|
85
|
+
export default uiStore;
|
package/src/types.ts
CHANGED
|
@@ -2,68 +2,68 @@
|
|
|
2
2
|
* Type definitions for HexGrid Visualization
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { RefObject } from 'react'
|
|
6
|
-
import type { HexGridFeatureFlags } from './features'
|
|
5
|
+
import type { RefObject } from 'react';
|
|
6
|
+
import type { HexGridFeatureFlags } from './features';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Photo type for HexGrid visualization
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* This is the unified Photo type used throughout the hexgrid-3d package.
|
|
12
12
|
* It includes all fields needed for display, media playback, and analytics.
|
|
13
13
|
*/
|
|
14
14
|
export interface Photo {
|
|
15
|
-
id: string
|
|
16
|
-
title: string
|
|
17
|
-
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
|
|
18
18
|
// Image URLs - imageUrl is primary, url is for backward compatibility
|
|
19
|
-
imageUrl: string
|
|
20
|
-
url?: string
|
|
21
|
-
thumbnailUrl?: string
|
|
22
|
-
|
|
19
|
+
imageUrl: string;
|
|
20
|
+
url?: string; // Alias for imageUrl (backward compatibility)
|
|
21
|
+
thumbnailUrl?: string;
|
|
22
|
+
|
|
23
23
|
// Display metadata
|
|
24
|
-
alt: string
|
|
25
|
-
category: string
|
|
26
|
-
description?: string
|
|
27
|
-
|
|
24
|
+
alt: string;
|
|
25
|
+
category: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
|
|
28
28
|
// Source information
|
|
29
|
-
source: string
|
|
30
|
-
sourceUrl?: string
|
|
31
|
-
createdAt?: string
|
|
32
|
-
|
|
29
|
+
source: string;
|
|
30
|
+
sourceUrl?: string;
|
|
31
|
+
createdAt?: string;
|
|
32
|
+
|
|
33
33
|
// Shop integration
|
|
34
|
-
shopUrl?: string
|
|
35
|
-
location?: string
|
|
36
|
-
|
|
34
|
+
shopUrl?: string;
|
|
35
|
+
location?: string;
|
|
36
|
+
|
|
37
37
|
// Media type flags
|
|
38
|
-
isVideo?: boolean
|
|
39
|
-
videoUrl?: string
|
|
40
|
-
isTweet?: boolean
|
|
41
|
-
tweetUrl?: string
|
|
42
|
-
redditUrl?: string
|
|
43
|
-
durationSeconds?: number
|
|
44
|
-
|
|
38
|
+
isVideo?: boolean;
|
|
39
|
+
videoUrl?: string;
|
|
40
|
+
isTweet?: boolean;
|
|
41
|
+
tweetUrl?: string;
|
|
42
|
+
redditUrl?: string;
|
|
43
|
+
durationSeconds?: number;
|
|
44
|
+
|
|
45
45
|
// Competition/ranking system
|
|
46
|
-
velocity?: number
|
|
47
|
-
|
|
46
|
+
velocity?: number; // Normalized velocity [0.1, 1.0] for meritocratic competition
|
|
47
|
+
|
|
48
48
|
// User info
|
|
49
|
-
userId?: string
|
|
50
|
-
username?: string
|
|
51
|
-
platform?: string
|
|
52
|
-
author?: string
|
|
53
|
-
authorUrl?: string
|
|
54
|
-
|
|
49
|
+
userId?: string;
|
|
50
|
+
username?: string;
|
|
51
|
+
platform?: string;
|
|
52
|
+
author?: string;
|
|
53
|
+
authorUrl?: string;
|
|
54
|
+
|
|
55
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
|
-
|
|
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
65
|
// Visual
|
|
66
|
-
dominantColor?: string
|
|
66
|
+
dominantColor?: string;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
@@ -72,81 +72,92 @@ export interface Photo {
|
|
|
72
72
|
*/
|
|
73
73
|
export interface GridItem<T = unknown> {
|
|
74
74
|
// Core identification
|
|
75
|
-
id: string
|
|
76
|
-
type: string // 'photo' | 'note' | 'emotion' | 'ontology-entity' | 'custom'
|
|
75
|
+
id: string;
|
|
76
|
+
type: string; // 'photo' | 'note' | 'emotion' | 'ontology-entity' | 'custom'
|
|
77
77
|
|
|
78
78
|
// Visual representation (optional - allows non-visual items)
|
|
79
|
-
imageUrl?: string
|
|
80
|
-
thumbnailUrl?: string
|
|
81
|
-
videoUrl?: string
|
|
79
|
+
imageUrl?: string;
|
|
80
|
+
thumbnailUrl?: string;
|
|
81
|
+
videoUrl?: string;
|
|
82
82
|
|
|
83
83
|
// Display metadata
|
|
84
|
-
title?: string
|
|
85
|
-
alt?: string
|
|
86
|
-
description?: string
|
|
87
|
-
category?: string
|
|
84
|
+
title?: string;
|
|
85
|
+
alt?: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
category?: string;
|
|
88
88
|
|
|
89
89
|
// Embedded data object (preserves original type)
|
|
90
|
-
data?: T
|
|
90
|
+
data?: T;
|
|
91
91
|
|
|
92
92
|
// Ontology metadata (optional, for ontology-aware items)
|
|
93
93
|
ontologyMetadata?: {
|
|
94
|
-
entityId?: string
|
|
95
|
-
entityType?: string | string[]
|
|
96
|
-
properties?: Record<string, unknown
|
|
94
|
+
entityId?: string;
|
|
95
|
+
entityType?: string | string[];
|
|
96
|
+
properties?: Record<string, unknown>;
|
|
97
97
|
provenance?: {
|
|
98
|
-
source: string
|
|
99
|
-
extractedAt: string
|
|
100
|
-
confidence: number
|
|
101
|
-
}
|
|
102
|
-
}
|
|
98
|
+
source: string;
|
|
99
|
+
extractedAt: string;
|
|
100
|
+
confidence: number;
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
103
|
|
|
104
104
|
// Grid behavior
|
|
105
|
-
velocity?: number
|
|
106
|
-
source?: string
|
|
107
|
-
sourceUrl?: string
|
|
108
|
-
createdAt?: string
|
|
105
|
+
velocity?: number;
|
|
106
|
+
source?: string;
|
|
107
|
+
sourceUrl?: string;
|
|
108
|
+
createdAt?: string;
|
|
109
109
|
|
|
110
110
|
// Metrics (flexible for any data type)
|
|
111
|
-
metrics?: Record<string, number
|
|
111
|
+
metrics?: Record<string, number>;
|
|
112
112
|
|
|
113
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
|
|
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
124
|
}
|
|
125
125
|
|
|
126
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
|
|
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
|
+
enableEnhanced?: boolean;
|
|
139
|
+
enhancedConfig?: {
|
|
140
|
+
useWasm?: boolean;
|
|
141
|
+
enableParticles?: boolean;
|
|
142
|
+
enableFlowField?: boolean;
|
|
143
|
+
};
|
|
144
|
+
onTerritoryEvent?: (event: {
|
|
145
|
+
type: 'conquest' | 'birth' | 'death';
|
|
146
|
+
toOwner: number;
|
|
147
|
+
fromOwner?: number;
|
|
148
|
+
}) => Promise<void>;
|
|
138
149
|
}
|
|
139
150
|
|
|
140
151
|
export interface UIState {
|
|
141
|
-
debugOpen: boolean
|
|
142
|
-
showStats: boolean
|
|
143
|
-
cameraOpen: boolean
|
|
144
|
-
showNarration: boolean
|
|
152
|
+
debugOpen: boolean;
|
|
153
|
+
showStats: boolean;
|
|
154
|
+
cameraOpen: boolean;
|
|
155
|
+
showNarration: boolean;
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
export interface WorkerDebug {
|
|
148
|
-
curveUDeg: number
|
|
149
|
-
curveVDeg: number
|
|
150
|
-
batchPerFrame: number
|
|
151
|
-
[key: string]: any
|
|
159
|
+
curveUDeg: number;
|
|
160
|
+
curveVDeg: number;
|
|
161
|
+
batchPerFrame: number;
|
|
162
|
+
[key: string]: any;
|
|
152
163
|
}
|
package/src/utils/image-utils.ts
CHANGED
|
@@ -12,14 +12,17 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export function getProxiedImageUrl(imageUrl: string): string {
|
|
14
14
|
if (!imageUrl || typeof imageUrl !== 'string') {
|
|
15
|
-
return imageUrl
|
|
15
|
+
return imageUrl;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
// Only proxy preview.redd.it URLs (they have CORS issues)
|
|
19
19
|
// external-preview.redd.it URLs work fine, so don't proxy them
|
|
20
|
-
if (
|
|
21
|
-
|
|
20
|
+
if (
|
|
21
|
+
imageUrl.includes('preview.redd.it') &&
|
|
22
|
+
!imageUrl.includes('external-preview.redd.it')
|
|
23
|
+
) {
|
|
24
|
+
return `/api/proxy-image?url=${encodeURIComponent(imageUrl)}`;
|
|
22
25
|
}
|
|
23
|
-
|
|
24
|
-
return imageUrl
|
|
26
|
+
|
|
27
|
+
return imageUrl;
|
|
25
28
|
}
|