@buley/hexgrid-3d 1.0.0 → 1.1.1
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/build_log.txt +500 -0
- package/build_src_log.txt +8 -0
- package/examples/basic-usage.tsx +19 -19
- package/package.json +1 -1
- package/public/hexgrid-worker.js +2350 -1638
- package/site/.eslintrc.json +3 -0
- package/site/DEPLOYMENT.md +196 -0
- package/site/INDEX.md +127 -0
- package/site/QUICK_START.md +86 -0
- package/site/README.md +85 -0
- package/site/SITE_SUMMARY.md +180 -0
- package/site/next.config.js +12 -0
- package/site/package.json +26 -0
- package/site/src/app/docs/page.tsx +148 -0
- package/site/src/app/examples/page.tsx +133 -0
- package/site/src/app/globals.css +160 -0
- package/site/src/app/layout.tsx +29 -0
- package/site/src/app/page.tsx +163 -0
- package/site/tsconfig.json +29 -0
- package/site/vercel.json +6 -0
- package/src/Snapshot.ts +790 -585
- package/src/adapters/DashAdapter.ts +57 -0
- 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 +567 -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 +140 -52
- package/src/components/index.ts +2 -1
- package/src/features.ts +31 -31
- package/src/index.ts +11 -11
- package/src/lib/narration.ts +17 -0
- package/src/lib/stats-tracker.ts +25 -0
- package/src/lib/theme-colors.ts +12 -0
- package/src/math/HexCoordinates.ts +849 -4
- package/src/math/Matrix4.ts +2 -12
- package/src/math/Vector3.ts +49 -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/shared-utils.d.ts +10 -0
- package/src/types.ts +110 -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/tsconfig.json +21 -14
package/src/math/Matrix4.ts
CHANGED
|
@@ -8,21 +8,11 @@ export class Matrix4 {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
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
|
-
]);
|
|
11
|
+
return new Matrix4([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
|
|
17
12
|
}
|
|
18
13
|
|
|
19
14
|
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
|
-
]);
|
|
15
|
+
return new Matrix4([1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1]);
|
|
26
16
|
}
|
|
27
17
|
|
|
28
18
|
transformPoint(point: Vector3): Vector3 {
|
package/src/math/Vector3.ts
CHANGED
|
@@ -49,7 +49,11 @@ export class Vector3 {
|
|
|
49
49
|
this.z = z;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
static fromLatLng(
|
|
52
|
+
static fromLatLng(
|
|
53
|
+
latitude: number,
|
|
54
|
+
longitude: number,
|
|
55
|
+
radius: number = 1
|
|
56
|
+
): Vector3 {
|
|
53
57
|
const latRad = (latitude * Math.PI) / 180;
|
|
54
58
|
const lonRad = (longitude * Math.PI) / 180;
|
|
55
59
|
|
|
@@ -60,6 +64,50 @@ export class Vector3 {
|
|
|
60
64
|
return new Vector3(x, y, z);
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
clone(): Vector3 {
|
|
68
|
+
return new Vector3(this.x, this.y, this.z);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
add(other: Vector3): Vector3 {
|
|
72
|
+
return new Vector3(this.x + other.x, this.y + other.y, this.z + other.z);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
subtract(other: Vector3): Vector3 {
|
|
76
|
+
return new Vector3(this.x - other.x, this.y - other.y, this.z - other.z);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
scale(factor: number): Vector3 {
|
|
80
|
+
return new Vector3(this.x * factor, this.y * factor, this.z * factor);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
dot(other: Vector3): number {
|
|
84
|
+
return this.x * other.x + this.y * other.y + this.z * other.z;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
cross(other: Vector3): Vector3 {
|
|
88
|
+
return new Vector3(
|
|
89
|
+
this.y * other.z - this.z * other.y,
|
|
90
|
+
this.z * other.x - this.x * other.z,
|
|
91
|
+
this.x * other.y - this.y * other.x
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
length(): number {
|
|
96
|
+
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
magnitude(): number {
|
|
100
|
+
return this.length();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
normalize(): Vector3 {
|
|
104
|
+
const len = this.length();
|
|
105
|
+
if (len === 0) {
|
|
106
|
+
return new Vector3(0, 0, 0);
|
|
107
|
+
}
|
|
108
|
+
return new Vector3(this.x / len, this.y / len, this.z / len);
|
|
109
|
+
}
|
|
110
|
+
|
|
63
111
|
distanceTo(other: Vector3): number {
|
|
64
112
|
const dx = this.x - other.x;
|
|
65
113
|
const dy = this.y - other.y;
|
package/src/math/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Math Module Exports
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* @module math
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export * from './Vector3'
|
|
8
|
-
export * from './Matrix4'
|
|
9
|
-
export * from './Quaternion'
|
|
10
|
-
export * from './HexCoordinates'
|
|
11
|
-
export * from './SpatialIndex'
|
|
7
|
+
export * from './Vector3';
|
|
8
|
+
export * from './Matrix4';
|
|
9
|
+
export * from './Quaternion';
|
|
10
|
+
export * from './HexCoordinates';
|
|
11
|
+
export * from './SpatialIndex';
|
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;
|