@unrdf/spatial-kg 26.4.2

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.
@@ -0,0 +1,209 @@
1
+ /**
2
+ * @file Level-of-Detail Manager
3
+ * @module @unrdf/spatial-kg/lod-manager
4
+ * @description Optimize rendering with distance-based LOD
5
+ */
6
+
7
+ import { LODLevelSchema, SpatialGraphConfigSchema } from './schemas.mjs';
8
+ import { trace } from '@opentelemetry/api';
9
+
10
+ const tracer = trace.getTracer('@unrdf/spatial-kg/lod-manager');
11
+
12
+ /**
13
+ * LOD Manager
14
+ */
15
+ export class LODManager {
16
+ /**
17
+ * @param {Object} config - LOD configuration
18
+ */
19
+ constructor(config = {}) {
20
+ const validated = SpatialGraphConfigSchema.parse({ lod: config });
21
+ this.config = validated.lod;
22
+ this.nodeLevels = new Map();
23
+ this.cameraPosition = { x: 0, y: 0, z: 0 };
24
+ }
25
+
26
+ /**
27
+ * Update camera position
28
+ * @param {Object} position - Camera position {x, y, z}
29
+ * @returns {void}
30
+ */
31
+ updateCamera(position) {
32
+ this.cameraPosition = { ...position };
33
+ }
34
+
35
+ /**
36
+ * Calculate LOD level for node
37
+ * @param {Object} node - Node with position
38
+ * @returns {number} LOD level (0=high, 1=medium, 2=low)
39
+ */
40
+ calculateLevel(node) {
41
+ if (!this.config.enabled) return 0;
42
+
43
+ const dx = node.position.x - this.cameraPosition.x;
44
+ const dy = node.position.y - this.cameraPosition.y;
45
+ const dz = node.position.z - this.cameraPosition.z;
46
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
47
+
48
+ // Find appropriate LOD level
49
+ for (let i = 0; i < this.config.levels.length; i++) {
50
+ if (distance < this.config.levels[i].distance) {
51
+ return i;
52
+ }
53
+ }
54
+
55
+ return this.config.levels.length - 1;
56
+ }
57
+
58
+ /**
59
+ * Update all node LOD levels
60
+ * @param {Map<string, Object>} nodes - All graph nodes
61
+ * @returns {Array<Object>} Nodes that changed LOD level
62
+ */
63
+ updateLevels(nodes) {
64
+ return tracer.startActiveSpan('lod.update-levels', span => {
65
+ try {
66
+ const changes = [];
67
+
68
+ for (const [id, node] of nodes) {
69
+ const newLevel = this.calculateLevel(node);
70
+ const oldLevel = this.nodeLevels.get(id);
71
+
72
+ if (oldLevel !== newLevel) {
73
+ const dx = node.position.x - this.cameraPosition.x;
74
+ const dy = node.position.y - this.cameraPosition.y;
75
+ const dz = node.position.z - this.cameraPosition.z;
76
+ const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
77
+
78
+ const change = LODLevelSchema.parse({
79
+ nodeId: id,
80
+ currentLevel: newLevel,
81
+ distance,
82
+ shouldUpdate: true,
83
+ });
84
+
85
+ changes.push(change);
86
+ this.nodeLevels.set(id, newLevel);
87
+ }
88
+ }
89
+
90
+ span.setAttributes({
91
+ 'lod.changes': changes.length,
92
+ 'lod.total_nodes': nodes.size,
93
+ });
94
+
95
+ return changes;
96
+ } finally {
97
+ span.end();
98
+ }
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Get LOD level for node
104
+ * @param {string} nodeId - Node ID
105
+ * @returns {number} LOD level
106
+ */
107
+ getLevel(nodeId) {
108
+ return this.nodeLevels.get(nodeId) || 0;
109
+ }
110
+
111
+ /**
112
+ * Get complexity for LOD level
113
+ * @param {number} level - LOD level
114
+ * @returns {string} Complexity ('high', 'medium', 'low')
115
+ */
116
+ getComplexity(level) {
117
+ if (level >= this.config.levels.length) {
118
+ return this.config.levels[this.config.levels.length - 1].complexity;
119
+ }
120
+ return this.config.levels[level].complexity;
121
+ }
122
+
123
+ /**
124
+ * Get all nodes at LOD level
125
+ * @param {number} level - LOD level
126
+ * @returns {Array<string>} Node IDs
127
+ */
128
+ getNodesAtLevel(level) {
129
+ const nodes = [];
130
+ for (const [id, nodeLevel] of this.nodeLevels) {
131
+ if (nodeLevel === level) {
132
+ nodes.push(id);
133
+ }
134
+ }
135
+ return nodes;
136
+ }
137
+
138
+ /**
139
+ * Get LOD statistics
140
+ * @returns {Object} Statistics
141
+ */
142
+ getStats() {
143
+ const stats = {
144
+ enabled: this.config.enabled,
145
+ levels: {},
146
+ total: this.nodeLevels.size,
147
+ };
148
+
149
+ for (let i = 0; i < this.config.levels.length; i++) {
150
+ const count = this.getNodesAtLevel(i).length;
151
+ stats.levels[i] = {
152
+ count,
153
+ complexity: this.getComplexity(i),
154
+ distance: this.config.levels[i].distance,
155
+ };
156
+ }
157
+
158
+ return stats;
159
+ }
160
+
161
+ /**
162
+ * Reset LOD state
163
+ * @returns {void}
164
+ */
165
+ reset() {
166
+ this.nodeLevels.clear();
167
+ this.cameraPosition = { x: 0, y: 0, z: 0 };
168
+ }
169
+
170
+ /**
171
+ * Determine if node should be rendered
172
+ * @param {string} nodeId - Node ID
173
+ * @param {Object} _frustum - Camera frustum (optional)
174
+ * @returns {boolean} Should render
175
+ */
176
+ shouldRender(nodeId, _frustum = null) {
177
+ const level = this.getLevel(nodeId);
178
+
179
+ // Always render high detail
180
+ if (level === 0) return true;
181
+
182
+ // Medium detail - render 50%
183
+ if (level === 1) {
184
+ const hash = this._hashString(nodeId);
185
+ return hash % 2 === 0;
186
+ }
187
+
188
+ // Low detail - render 25%
189
+ if (level === 2) {
190
+ const hash = this._hashString(nodeId);
191
+ return hash % 4 === 0;
192
+ }
193
+
194
+ return false;
195
+ }
196
+
197
+ /**
198
+ * Simple string hash for deterministic culling
199
+ * @private
200
+ */
201
+ _hashString(str) {
202
+ let hash = 0;
203
+ for (let i = 0; i < str.length; i++) {
204
+ hash = (hash << 5) - hash + str.charCodeAt(i);
205
+ hash = hash & hash;
206
+ }
207
+ return Math.abs(hash);
208
+ }
209
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @file Zod schemas for Spatial Knowledge Graph
3
+ * @module @unrdf/spatial-kg/schemas
4
+ */
5
+
6
+ import { z } from 'zod';
7
+
8
+ /**
9
+ * Vector3 schema
10
+ */
11
+ export const Vector3Schema = z.object({
12
+ x: z.number().finite(),
13
+ y: z.number().finite(),
14
+ z: z.number().finite(),
15
+ });
16
+
17
+ /**
18
+ * Node3D schema
19
+ */
20
+ export const Node3DSchema = z.object({
21
+ id: z.string().min(1),
22
+ uri: z.string().optional(),
23
+ label: z.string().optional(),
24
+ position: Vector3Schema,
25
+ velocity: Vector3Schema.optional(),
26
+ mass: z.number().positive().default(1),
27
+ fixed: z.boolean().default(false),
28
+ metadata: z.record(z.unknown()).optional(),
29
+ });
30
+
31
+ /**
32
+ * Edge3D schema
33
+ */
34
+ export const Edge3DSchema = z.object({
35
+ id: z.string().min(1),
36
+ source: z.string().min(1),
37
+ target: z.string().min(1),
38
+ predicate: z.string().optional(),
39
+ strength: z.number().min(0).max(1).default(1),
40
+ metadata: z.record(z.unknown()).optional(),
41
+ });
42
+
43
+ /**
44
+ * Spatial graph configuration
45
+ */
46
+ export const SpatialGraphConfigSchema = z.object({
47
+ layout: z
48
+ .object({
49
+ iterations: z.number().int().positive().default(100),
50
+ cooldown: z.number().min(0).max(1).default(0.95),
51
+ repulsionStrength: z.number().positive().default(100),
52
+ attractionStrength: z.number().positive().default(0.1),
53
+ gravityStrength: z.number().nonnegative().default(0.01),
54
+ centerForce: z.boolean().default(true),
55
+ dimensions: z.literal(3).default(3),
56
+ })
57
+ .default({}),
58
+ rendering: z
59
+ .object({
60
+ targetFPS: z.number().int().positive().default(60),
61
+ enableVR: z.boolean().default(false),
62
+ enableAR: z.boolean().default(false),
63
+ nodeSize: z.number().positive().default(1),
64
+ edgeWidth: z.number().positive().default(0.1),
65
+ backgroundColor: z.number().int().default(0x000000),
66
+ })
67
+ .default({}),
68
+ lod: z
69
+ .object({
70
+ enabled: z.boolean().default(true),
71
+ levels: z
72
+ .array(
73
+ z.object({
74
+ distance: z.number().positive(),
75
+ complexity: z.enum(['high', 'medium', 'low']),
76
+ })
77
+ )
78
+ .default([
79
+ { distance: 10, complexity: 'high' },
80
+ { distance: 50, complexity: 'medium' },
81
+ { distance: 100, complexity: 'low' },
82
+ ]),
83
+ })
84
+ .default({}),
85
+ collaboration: z
86
+ .object({
87
+ enabled: z.boolean().default(false),
88
+ serverUrl: z.string().url().optional(),
89
+ username: z.string().optional(),
90
+ })
91
+ .default({}),
92
+ });
93
+
94
+ /**
95
+ * Spatial query options
96
+ */
97
+ export const SpatialQueryOptionsSchema = z.object({
98
+ type: z.enum(['proximity', 'ray', 'knn', 'box', 'sphere']),
99
+ origin: Vector3Schema.optional(),
100
+ direction: Vector3Schema.optional(),
101
+ radius: z.number().positive().optional(),
102
+ k: z.number().int().positive().optional(),
103
+ bounds: z
104
+ .object({
105
+ min: Vector3Schema,
106
+ max: Vector3Schema,
107
+ })
108
+ .optional(),
109
+ });
110
+
111
+ /**
112
+ * Gesture event schema
113
+ */
114
+ export const GestureEventSchema = z.object({
115
+ type: z.enum(['point', 'grab', 'teleport', 'select', 'pinch']),
116
+ controller: z.enum(['left', 'right', 'hand-left', 'hand-right']),
117
+ position: Vector3Schema.optional(),
118
+ target: z.string().optional(),
119
+ timestamp: z.number().int().positive(),
120
+ intensity: z.number().min(0).max(1).optional(),
121
+ });
122
+
123
+ /**
124
+ * Collaboration state schema
125
+ */
126
+ export const CollaborationStateSchema = z.object({
127
+ userId: z.string().min(1),
128
+ position: Vector3Schema,
129
+ rotation: z.object({
130
+ x: z.number().finite(),
131
+ y: z.number().finite(),
132
+ z: z.number().finite(),
133
+ w: z.number().finite(),
134
+ }),
135
+ selectedNode: z.string().optional(),
136
+ timestamp: z.number().int().positive(),
137
+ metadata: z.record(z.unknown()).optional(),
138
+ });
139
+
140
+ /**
141
+ * LOD level schema
142
+ */
143
+ export const LODLevelSchema = z.object({
144
+ nodeId: z.string().min(1),
145
+ currentLevel: z.number().int().min(0).max(2),
146
+ distance: z.number().nonnegative(),
147
+ shouldUpdate: z.boolean(),
148
+ });
149
+
150
+ /**
151
+ * Performance metrics schema
152
+ */
153
+ export const PerformanceMetricsSchema = z.object({
154
+ fps: z.number().nonnegative(),
155
+ frameTime: z.number().nonnegative(),
156
+ nodeCount: z.number().int().nonnegative(),
157
+ edgeCount: z.number().int().nonnegative(),
158
+ renderTime: z.number().nonnegative(),
159
+ layoutTime: z.number().nonnegative(),
160
+ timestamp: z.number().int().positive(),
161
+ });
@@ -0,0 +1,264 @@
1
+ /**
2
+ * @file Spatial Knowledge Graph Engine
3
+ * @module @unrdf/spatial-kg
4
+ * @description Main orchestration engine for spatial knowledge graphs
5
+ */
6
+
7
+ import { Layout3D } from './layout-3d.mjs';
8
+ import { WebXRRenderer } from './webxr-renderer.mjs';
9
+ import { SpatialQueryEngine } from './spatial-query.mjs';
10
+ import { GestureController } from './gesture-controller.mjs';
11
+ import { LODManager } from './lod-manager.mjs';
12
+ import { CollaborationManager } from './collaboration.mjs';
13
+ import { SpatialGraphConfigSchema } from './schemas.mjs';
14
+ import { trace } from '@opentelemetry/api';
15
+
16
+ const tracer = trace.getTracer('@unrdf/spatial-kg');
17
+
18
+ /**
19
+ * Spatial Knowledge Graph Engine
20
+ */
21
+ export class SpatialKGEngine {
22
+ /**
23
+ * @param {Object} config - Engine configuration
24
+ */
25
+ constructor(config = {}) {
26
+ this.config = SpatialGraphConfigSchema.parse(config);
27
+
28
+ // Core components
29
+ this.layout = new Layout3D(this.config.layout);
30
+ this.renderer = new WebXRRenderer(this.config.rendering);
31
+ this.queryEngine = null; // Created after layout
32
+ this.gestureController = new GestureController();
33
+ this.lodManager = new LODManager(this.config.lod);
34
+ this.collaboration = null;
35
+
36
+ // State
37
+ this.running = false;
38
+ this.animationFrame = null;
39
+ this.initialized = false;
40
+ }
41
+
42
+ /**
43
+ * Initialize engine
44
+ * @returns {Promise<void>}
45
+ */
46
+ async initialize() {
47
+ return tracer.startActiveSpan('engine.initialize', async span => {
48
+ try {
49
+ await this.renderer.initialize();
50
+
51
+ // Setup collaboration if enabled
52
+ if (this.config.collaboration.enabled) {
53
+ this.collaboration = new CollaborationManager({
54
+ userId: this.config.collaboration.username || 'user-' + Date.now(),
55
+ serverUrl: this.config.collaboration.serverUrl,
56
+ });
57
+ await this.collaboration.connect();
58
+ }
59
+
60
+ this.initialized = true;
61
+
62
+ span.setAttributes({
63
+ 'engine.initialized': true,
64
+ 'engine.collaboration': this.config.collaboration.enabled,
65
+ });
66
+ } finally {
67
+ span.end();
68
+ }
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Load graph data
74
+ * @param {Object} data - Graph data
75
+ * @param {Array<Object>} data.nodes - Node list
76
+ * @param {Array<Object>} data.edges - Edge list
77
+ * @returns {Promise<void>}
78
+ */
79
+ async loadGraph(data) {
80
+ return tracer.startActiveSpan('engine.load-graph', async span => {
81
+ try {
82
+ // Add nodes to layout
83
+ for (const node of data.nodes) {
84
+ this.layout.addNode(node);
85
+ }
86
+
87
+ // Add edges to layout
88
+ for (const edge of data.edges) {
89
+ this.layout.addEdge(edge);
90
+ }
91
+
92
+ // Run initial layout
93
+ await this.layout.run();
94
+
95
+ // Initialize query engine with node positions
96
+ this.queryEngine = new SpatialQueryEngine(this.layout.nodes);
97
+
98
+ // Update renderer
99
+ this.renderer.updateNodes(this.layout.nodes);
100
+ this.renderer.updateEdges(this.layout.edges, this.layout.nodes);
101
+
102
+ span.setAttributes({
103
+ 'graph.nodes': data.nodes.length,
104
+ 'graph.edges': data.edges.length,
105
+ });
106
+ } finally {
107
+ span.end();
108
+ }
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Start rendering loop
114
+ * @returns {void}
115
+ */
116
+ start() {
117
+ if (!this.initialized) {
118
+ throw new Error('Engine not initialized. Call initialize() first.');
119
+ }
120
+
121
+ this.running = true;
122
+ this._renderLoop();
123
+ }
124
+
125
+ /**
126
+ * Stop rendering loop
127
+ * @returns {void}
128
+ */
129
+ stop() {
130
+ this.running = false;
131
+ if (this.animationFrame && typeof globalThis.cancelAnimationFrame !== 'undefined') {
132
+ globalThis.cancelAnimationFrame(this.animationFrame);
133
+ this.animationFrame = null;
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Main render loop
139
+ * @private
140
+ */
141
+ _renderLoop() {
142
+ if (!this.running) return;
143
+
144
+ tracer.startActiveSpan('engine.render-loop', span => {
145
+ try {
146
+ // Update LOD based on camera
147
+ const cameraPos = this.renderer.getCameraPosition();
148
+ this.lodManager.updateCamera(cameraPos);
149
+ this.lodManager.updateLevels(this.layout.nodes);
150
+
151
+ // Update renderer
152
+ this.renderer.updateNodes(this.layout.nodes);
153
+
154
+ // Render frame
155
+ this.renderer.render();
156
+
157
+ // Update collaboration state
158
+ if (this.collaboration?.isConnected()) {
159
+ this.collaboration.updateLocalState({
160
+ position: cameraPos,
161
+ rotation: { x: 0, y: 0, z: 0, w: 1 }, // Placeholder
162
+ });
163
+ }
164
+
165
+ const metrics = this.renderer.getMetrics();
166
+ span.setAttributes({
167
+ 'engine.fps': metrics.fps,
168
+ 'engine.frameTime': metrics.frameTime,
169
+ });
170
+ } finally {
171
+ span.end();
172
+ }
173
+ });
174
+
175
+ if (typeof globalThis.requestAnimationFrame !== 'undefined') {
176
+ this.animationFrame = globalThis.requestAnimationFrame(() => this._renderLoop());
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Query nodes spatially
182
+ * @param {Object} options - Query options
183
+ * @returns {Array<Object>} Query results
184
+ */
185
+ query(options) {
186
+ if (!this.queryEngine) {
187
+ throw new Error('No graph loaded. Call loadGraph() first.');
188
+ }
189
+
190
+ const { type } = options;
191
+
192
+ switch (type) {
193
+ case 'proximity':
194
+ return this.queryEngine.proximity(options);
195
+ case 'ray':
196
+ return this.queryEngine.rayCast(options);
197
+ case 'knn':
198
+ return this.queryEngine.kNearestNeighbors(options);
199
+ case 'box':
200
+ return this.queryEngine.box(options);
201
+ default:
202
+ throw new Error(`Unknown query type: ${type}`);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Start WebXR session
208
+ * @param {string} mode - 'immersive-vr' or 'immersive-ar'
209
+ * @returns {Promise<void>}
210
+ */
211
+ async startXR(mode) {
212
+ await this.renderer.startXRSession(mode);
213
+ }
214
+
215
+ /**
216
+ * End WebXR session
217
+ * @returns {Promise<void>}
218
+ */
219
+ async endXR() {
220
+ await this.renderer.endXRSession();
221
+ }
222
+
223
+ /**
224
+ * Register gesture listener
225
+ * @param {string} gestureType - Gesture type
226
+ * @param {Function} callback - Event handler
227
+ * @returns {Function} Unsubscribe function
228
+ */
229
+ onGesture(gestureType, callback) {
230
+ return this.gestureController.on(gestureType, callback);
231
+ }
232
+
233
+ /**
234
+ * Get performance metrics
235
+ * @returns {Object} Metrics
236
+ */
237
+ getMetrics() {
238
+ return {
239
+ rendering: this.renderer.getMetrics(),
240
+ lod: this.lodManager.getStats(),
241
+ layout: {
242
+ iteration: this.layout.iteration,
243
+ temperature: this.layout.temperature,
244
+ nodes: this.layout.nodes.size,
245
+ edges: this.layout.edges.length,
246
+ },
247
+ collaboration: this.collaboration?.getStats() || null,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Dispose engine and cleanup resources
253
+ * @returns {void}
254
+ */
255
+ dispose() {
256
+ this.stop();
257
+ this.renderer.dispose();
258
+ this.gestureController.clear();
259
+ this.lodManager.reset();
260
+ if (this.collaboration) {
261
+ this.collaboration.disconnect();
262
+ }
263
+ }
264
+ }