@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,429 @@
1
+ /**
2
+ * @file Spatial Query Engine
3
+ * @module @unrdf/spatial-kg/spatial-query
4
+ * @description Spatial queries for 3D knowledge graphs (proximity, ray casting, k-NN)
5
+ */
6
+
7
+ import { SpatialQueryOptionsSchema } from './schemas.mjs';
8
+ import { trace } from '@opentelemetry/api';
9
+
10
+ const tracer = trace.getTracer('@unrdf/spatial-kg/spatial-query');
11
+
12
+ /**
13
+ * Octree node for spatial indexing
14
+ * @private
15
+ */
16
+ class OctreeNode {
17
+ constructor(bounds, capacity = 8) {
18
+ this.bounds = bounds;
19
+ this.capacity = capacity;
20
+ this.nodes = [];
21
+ this.divided = false;
22
+ this.children = null;
23
+ }
24
+
25
+ /**
26
+ * Check if point is in bounds
27
+ */
28
+ contains(point) {
29
+ return (
30
+ point.x >= this.bounds.min.x &&
31
+ point.x <= this.bounds.max.x &&
32
+ point.y >= this.bounds.min.y &&
33
+ point.y <= this.bounds.max.y &&
34
+ point.z >= this.bounds.min.z &&
35
+ point.z <= this.bounds.max.z
36
+ );
37
+ }
38
+
39
+ /**
40
+ * Insert node into octree
41
+ */
42
+ insert(node) {
43
+ if (!this.contains(node.position)) return false;
44
+
45
+ if (this.nodes.length < this.capacity) {
46
+ this.nodes.push(node);
47
+ return true;
48
+ }
49
+
50
+ if (!this.divided) {
51
+ this._subdivide();
52
+ }
53
+
54
+ for (const child of this.children) {
55
+ if (child.insert(node)) return true;
56
+ }
57
+
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Subdivide into 8 octants
63
+ * @private
64
+ */
65
+ _subdivide() {
66
+ const { min, max } = this.bounds;
67
+ const midX = (min.x + max.x) / 2;
68
+ const midY = (min.y + max.y) / 2;
69
+ const midZ = (min.z + max.z) / 2;
70
+
71
+ this.children = [
72
+ // Front quadrants
73
+ new OctreeNode(
74
+ { min: { x: min.x, y: min.y, z: min.z }, max: { x: midX, y: midY, z: midZ } },
75
+ this.capacity
76
+ ),
77
+ new OctreeNode(
78
+ { min: { x: midX, y: min.y, z: min.z }, max: { x: max.x, y: midY, z: midZ } },
79
+ this.capacity
80
+ ),
81
+ new OctreeNode(
82
+ { min: { x: min.x, y: midY, z: min.z }, max: { x: midX, y: max.y, z: midZ } },
83
+ this.capacity
84
+ ),
85
+ new OctreeNode(
86
+ { min: { x: midX, y: midY, z: min.z }, max: { x: max.x, y: max.y, z: midZ } },
87
+ this.capacity
88
+ ),
89
+ // Back quadrants
90
+ new OctreeNode(
91
+ { min: { x: min.x, y: min.y, z: midZ }, max: { x: midX, y: midY, z: max.z } },
92
+ this.capacity
93
+ ),
94
+ new OctreeNode(
95
+ { min: { x: midX, y: min.y, z: midZ }, max: { x: max.x, y: midY, z: max.z } },
96
+ this.capacity
97
+ ),
98
+ new OctreeNode(
99
+ { min: { x: min.x, y: midY, z: midZ }, max: { x: midX, y: max.y, z: max.z } },
100
+ this.capacity
101
+ ),
102
+ new OctreeNode(
103
+ { min: { x: midX, y: midY, z: midZ }, max: { x: max.x, y: max.y, z: max.z } },
104
+ this.capacity
105
+ ),
106
+ ];
107
+
108
+ this.divided = true;
109
+
110
+ // Redistribute existing nodes
111
+ for (const existingNode of this.nodes) {
112
+ for (const child of this.children) {
113
+ if (child.insert(existingNode)) break;
114
+ }
115
+ }
116
+ this.nodes = [];
117
+ }
118
+
119
+ /**
120
+ * Query nodes in range
121
+ */
122
+ query(bounds, found = []) {
123
+ if (!this._intersects(bounds)) return found;
124
+
125
+ for (const node of this.nodes) {
126
+ if (this._pointInBounds(node.position, bounds)) {
127
+ found.push(node);
128
+ }
129
+ }
130
+
131
+ if (this.divided) {
132
+ for (const child of this.children) {
133
+ child.query(bounds, found);
134
+ }
135
+ }
136
+
137
+ return found;
138
+ }
139
+
140
+ /**
141
+ * Check if bounds intersect
142
+ * @private
143
+ */
144
+ _intersects(bounds) {
145
+ return !(
146
+ bounds.max.x < this.bounds.min.x ||
147
+ bounds.min.x > this.bounds.max.x ||
148
+ bounds.max.y < this.bounds.min.y ||
149
+ bounds.min.y > this.bounds.max.y ||
150
+ bounds.max.z < this.bounds.min.z ||
151
+ bounds.min.z > this.bounds.max.z
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Check if point in bounds
157
+ * @private
158
+ */
159
+ _pointInBounds(point, bounds) {
160
+ return (
161
+ point.x >= bounds.min.x &&
162
+ point.x <= bounds.max.x &&
163
+ point.y >= bounds.min.y &&
164
+ point.y <= bounds.max.y &&
165
+ point.z >= bounds.min.z &&
166
+ point.z <= bounds.max.z
167
+ );
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Spatial Query Engine
173
+ */
174
+ export class SpatialQueryEngine {
175
+ /**
176
+ * @param {Map<string, Object>} nodes - Graph nodes with positions
177
+ */
178
+ constructor(nodes = new Map()) {
179
+ this.nodes = nodes;
180
+ this.octree = null;
181
+ this._buildOctree();
182
+ }
183
+
184
+ /**
185
+ * Build octree spatial index
186
+ * @private
187
+ */
188
+ _buildOctree() {
189
+ if (this.nodes.size === 0) {
190
+ this.octree = null;
191
+ return;
192
+ }
193
+
194
+ // Calculate bounds
195
+ let minX = Infinity,
196
+ minY = Infinity,
197
+ minZ = Infinity;
198
+ let maxX = -Infinity,
199
+ maxY = -Infinity,
200
+ maxZ = -Infinity;
201
+
202
+ for (const node of this.nodes.values()) {
203
+ minX = Math.min(minX, node.position.x);
204
+ minY = Math.min(minY, node.position.y);
205
+ minZ = Math.min(minZ, node.position.z);
206
+ maxX = Math.max(maxX, node.position.x);
207
+ maxY = Math.max(maxY, node.position.y);
208
+ maxZ = Math.max(maxZ, node.position.z);
209
+ }
210
+
211
+ // Add padding
212
+ const padding = 10;
213
+ const bounds = {
214
+ min: { x: minX - padding, y: minY - padding, z: minZ - padding },
215
+ max: { x: maxX + padding, y: maxY + padding, z: maxZ + padding },
216
+ };
217
+
218
+ this.octree = new OctreeNode(bounds);
219
+
220
+ // Insert all nodes
221
+ for (const node of this.nodes.values()) {
222
+ this.octree.insert(node);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Proximity query - find nodes within radius
228
+ * @param {Object} options - Query options
229
+ * @param {Object} options.origin - Center point
230
+ * @param {number} options.radius - Search radius
231
+ * @returns {Array<Object>} Nodes within radius
232
+ */
233
+ proximity(options) {
234
+ return tracer.startActiveSpan('spatial-query.proximity', span => {
235
+ try {
236
+ const { origin, radius } = SpatialQueryOptionsSchema.parse({
237
+ type: 'proximity',
238
+ ...options,
239
+ });
240
+
241
+ if (!this.octree) return [];
242
+
243
+ // Query octree with bounding box
244
+ const bounds = {
245
+ min: {
246
+ x: origin.x - radius,
247
+ y: origin.y - radius,
248
+ z: origin.z - radius,
249
+ },
250
+ max: {
251
+ x: origin.x + radius,
252
+ y: origin.y + radius,
253
+ z: origin.z + radius,
254
+ },
255
+ };
256
+
257
+ const candidates = this.octree.query(bounds);
258
+
259
+ // Filter by actual distance
260
+ const results = candidates.filter(node => {
261
+ const dx = node.position.x - origin.x;
262
+ const dy = node.position.y - origin.y;
263
+ const dz = node.position.z - origin.z;
264
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
265
+ return dist <= radius;
266
+ });
267
+
268
+ span.setAttributes({
269
+ 'query.type': 'proximity',
270
+ 'query.candidates': candidates.length,
271
+ 'query.results': results.length,
272
+ 'query.radius': radius,
273
+ });
274
+
275
+ return results;
276
+ } finally {
277
+ span.end();
278
+ }
279
+ });
280
+ }
281
+
282
+ /**
283
+ * Ray casting query - find nodes along ray
284
+ * @param {Object} options - Query options
285
+ * @param {Object} options.origin - Ray origin
286
+ * @param {Object} options.direction - Ray direction (normalized)
287
+ * @param {number} [options.radius=1] - Ray thickness
288
+ * @returns {Array<Object>} Nodes intersecting ray
289
+ */
290
+ rayCast(options) {
291
+ return tracer.startActiveSpan('spatial-query.raycast', span => {
292
+ try {
293
+ const {
294
+ origin,
295
+ direction,
296
+ radius = 1,
297
+ } = SpatialQueryOptionsSchema.parse({
298
+ type: 'ray',
299
+ ...options,
300
+ });
301
+
302
+ const results = [];
303
+
304
+ for (const node of this.nodes.values()) {
305
+ // Vector from origin to node
306
+ const toNode = {
307
+ x: node.position.x - origin.x,
308
+ y: node.position.y - origin.y,
309
+ z: node.position.z - origin.z,
310
+ };
311
+
312
+ // Project onto ray direction
313
+ const projection =
314
+ toNode.x * direction.x + toNode.y * direction.y + toNode.z * direction.z;
315
+
316
+ if (projection < 0) continue; // Behind ray
317
+
318
+ // Closest point on ray
319
+ const closest = {
320
+ x: origin.x + direction.x * projection,
321
+ y: origin.y + direction.y * projection,
322
+ z: origin.z + direction.z * projection,
323
+ };
324
+
325
+ // Distance from node to ray
326
+ const dx = node.position.x - closest.x;
327
+ const dy = node.position.y - closest.y;
328
+ const dz = node.position.z - closest.z;
329
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
330
+
331
+ if (dist <= radius) {
332
+ results.push({ node, distance: projection });
333
+ }
334
+ }
335
+
336
+ // Sort by distance along ray
337
+ results.sort((a, b) => a.distance - b.distance);
338
+
339
+ span.setAttributes({
340
+ 'query.type': 'raycast',
341
+ 'query.results': results.length,
342
+ });
343
+
344
+ return results.map(r => r.node);
345
+ } finally {
346
+ span.end();
347
+ }
348
+ });
349
+ }
350
+
351
+ /**
352
+ * K-nearest neighbors query
353
+ * @param {Object} options - Query options
354
+ * @param {Object} options.origin - Query point
355
+ * @param {number} options.k - Number of neighbors
356
+ * @returns {Array<Object>} K nearest nodes
357
+ */
358
+ kNearestNeighbors(options) {
359
+ return tracer.startActiveSpan('spatial-query.knn', span => {
360
+ try {
361
+ const { origin, k } = SpatialQueryOptionsSchema.parse({
362
+ type: 'knn',
363
+ ...options,
364
+ });
365
+
366
+ const distances = [];
367
+
368
+ for (const node of this.nodes.values()) {
369
+ const dx = node.position.x - origin.x;
370
+ const dy = node.position.y - origin.y;
371
+ const dz = node.position.z - origin.z;
372
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
373
+ distances.push({ node, distance: dist });
374
+ }
375
+
376
+ distances.sort((a, b) => a.distance - b.distance);
377
+ const results = distances.slice(0, k).map(d => d.node);
378
+
379
+ span.setAttributes({
380
+ 'query.type': 'knn',
381
+ 'query.k': k,
382
+ 'query.results': results.length,
383
+ });
384
+
385
+ return results;
386
+ } finally {
387
+ span.end();
388
+ }
389
+ });
390
+ }
391
+
392
+ /**
393
+ * Box query - find nodes in bounding box
394
+ * @param {Object} options - Query options
395
+ * @param {Object} options.bounds - Bounding box {min, max}
396
+ * @returns {Array<Object>} Nodes in box
397
+ */
398
+ box(options) {
399
+ return tracer.startActiveSpan('spatial-query.box', span => {
400
+ try {
401
+ const { bounds } = SpatialQueryOptionsSchema.parse({
402
+ type: 'box',
403
+ ...options,
404
+ });
405
+
406
+ if (!this.octree) return [];
407
+
408
+ const results = this.octree.query(bounds);
409
+
410
+ span.setAttributes({
411
+ 'query.type': 'box',
412
+ 'query.results': results.length,
413
+ });
414
+
415
+ return results;
416
+ } finally {
417
+ span.end();
418
+ }
419
+ });
420
+ }
421
+
422
+ /**
423
+ * Rebuild spatial index
424
+ * @returns {void}
425
+ */
426
+ rebuild() {
427
+ this._buildOctree();
428
+ }
429
+ }
@@ -0,0 +1,291 @@
1
+ /**
2
+ * @file WebXR Renderer
3
+ * @module @unrdf/spatial-kg/webxr-renderer
4
+ * @description WebXR-enabled 3D rendering for spatial knowledge graphs
5
+ */
6
+
7
+ import { SpatialGraphConfigSchema } from './schemas.mjs';
8
+ import { trace } from '@opentelemetry/api';
9
+
10
+ const tracer = trace.getTracer('@unrdf/spatial-kg/webxr-renderer');
11
+
12
+ /**
13
+ * Mock Three.js scene (for testing without actual Three.js)
14
+ * In production, import from 'three'
15
+ */
16
+ class MockScene {
17
+ constructor() {
18
+ this.children = [];
19
+ }
20
+ add(object) {
21
+ this.children.push(object);
22
+ }
23
+ remove(object) {
24
+ const index = this.children.indexOf(object);
25
+ if (index > -1) this.children.splice(index, 1);
26
+ }
27
+ }
28
+
29
+ class MockMesh {
30
+ constructor(geometry, material) {
31
+ this.geometry = geometry;
32
+ this.material = material;
33
+ this.position = { x: 0, y: 0, z: 0 };
34
+ this.userData = {};
35
+ }
36
+ }
37
+
38
+ /**
39
+ * WebXR Renderer
40
+ */
41
+ export class WebXRRenderer {
42
+ /**
43
+ * @param {Object} config - Renderer configuration
44
+ * @param {HTMLCanvasElement} [config.canvas] - Canvas element
45
+ */
46
+ constructor(config = {}) {
47
+ const validated = SpatialGraphConfigSchema.parse({ rendering: config });
48
+ this.config = validated.rendering;
49
+ this.canvas = config.canvas;
50
+
51
+ // Scene setup (using mock for testing)
52
+ this.scene = new MockScene();
53
+ this.camera = null;
54
+ this.renderer = null;
55
+
56
+ // Node/edge meshes
57
+ this.nodeMeshes = new Map();
58
+ this.edgeMeshes = new Map();
59
+
60
+ // XR state
61
+ this.xrSession = null;
62
+ this.isVR = false;
63
+ this.isAR = false;
64
+
65
+ // Performance
66
+ this.frameTime = 0;
67
+ this.lastFrameTime = 0;
68
+ this.fps = 60;
69
+ }
70
+
71
+ /**
72
+ * Initialize renderer
73
+ * @returns {Promise<void>}
74
+ */
75
+ async initialize() {
76
+ return tracer.startActiveSpan('renderer.initialize', async span => {
77
+ try {
78
+ // In real implementation, create Three.js renderer, camera, etc.
79
+ // For testing, just simulate initialization
80
+
81
+ this.camera = {
82
+ position: { x: 0, y: 10, z: 50 },
83
+ rotation: { x: 0, y: 0, z: 0 },
84
+ fov: 75,
85
+ };
86
+
87
+ span.setAttributes({
88
+ 'renderer.initialized': true,
89
+ 'renderer.targetFPS': this.config.targetFPS,
90
+ });
91
+ } finally {
92
+ span.end();
93
+ }
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Start WebXR session
99
+ * @param {string} mode - 'immersive-vr' or 'immersive-ar'
100
+ * @returns {Promise<void>}
101
+ */
102
+ async startXRSession(mode) {
103
+ return tracer.startActiveSpan('renderer.start-xr', async span => {
104
+ try {
105
+ // In real implementation, request XR session from WebXR API
106
+ // For testing, simulate session start
107
+
108
+ this.xrSession = { mode };
109
+ this.isVR = mode === 'immersive-vr';
110
+ this.isAR = mode === 'immersive-ar';
111
+
112
+ span.setAttributes({
113
+ 'xr.mode': mode,
114
+ 'xr.active': true,
115
+ });
116
+ } finally {
117
+ span.end();
118
+ }
119
+ });
120
+ }
121
+
122
+ /**
123
+ * End WebXR session
124
+ * @returns {Promise<void>}
125
+ */
126
+ async endXRSession() {
127
+ if (this.xrSession) {
128
+ this.xrSession = null;
129
+ this.isVR = false;
130
+ this.isAR = false;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Update node positions
136
+ * @param {Map<string, Object>} nodes - Nodes with positions
137
+ * @returns {void}
138
+ */
139
+ updateNodes(nodes) {
140
+ return tracer.startActiveSpan('renderer.update-nodes', span => {
141
+ try {
142
+ // Update existing nodes
143
+ for (const [id, node] of nodes) {
144
+ let mesh = this.nodeMeshes.get(id);
145
+
146
+ if (!mesh) {
147
+ // Create new mesh
148
+ mesh = new MockMesh({}, {});
149
+ mesh.userData.nodeId = id;
150
+ this.nodeMeshes.set(id, mesh);
151
+ this.scene.add(mesh);
152
+ }
153
+
154
+ // Update position
155
+ mesh.position.x = node.position.x;
156
+ mesh.position.y = node.position.y;
157
+ mesh.position.z = node.position.z;
158
+ }
159
+
160
+ // Remove deleted nodes
161
+ for (const [id, mesh] of this.nodeMeshes) {
162
+ if (!nodes.has(id)) {
163
+ this.scene.remove(mesh);
164
+ this.nodeMeshes.delete(id);
165
+ }
166
+ }
167
+
168
+ span.setAttributes({
169
+ 'renderer.nodes': nodes.size,
170
+ 'renderer.meshes': this.nodeMeshes.size,
171
+ });
172
+ } finally {
173
+ span.end();
174
+ }
175
+ });
176
+ }
177
+
178
+ /**
179
+ * Update edges
180
+ * @param {Array<Object>} edges - Edge list
181
+ * @param {Map<string, Object>} nodes - Node positions
182
+ * @returns {void}
183
+ */
184
+ updateEdges(edges, nodes) {
185
+ return tracer.startActiveSpan('renderer.update-edges', span => {
186
+ try {
187
+ // Clear existing edges (simple approach)
188
+ for (const mesh of this.edgeMeshes.values()) {
189
+ this.scene.remove(mesh);
190
+ }
191
+ this.edgeMeshes.clear();
192
+
193
+ // Create edge meshes
194
+ for (const edge of edges) {
195
+ const source = nodes.get(edge.source);
196
+ const target = nodes.get(edge.target);
197
+
198
+ if (source && target) {
199
+ const mesh = new MockMesh({}, {});
200
+ mesh.userData.edgeId = edge.id;
201
+ mesh.userData.source = edge.source;
202
+ mesh.userData.target = edge.target;
203
+
204
+ this.edgeMeshes.set(edge.id, mesh);
205
+ this.scene.add(mesh);
206
+ }
207
+ }
208
+
209
+ span.setAttributes({
210
+ 'renderer.edges': edges.length,
211
+ });
212
+ } finally {
213
+ span.end();
214
+ }
215
+ });
216
+ }
217
+
218
+ /**
219
+ * Render frame
220
+ * @returns {void}
221
+ */
222
+ render() {
223
+ return tracer.startActiveSpan('renderer.render', span => {
224
+ try {
225
+ const now = performance.now();
226
+ this.frameTime = now - this.lastFrameTime;
227
+ this.lastFrameTime = now;
228
+
229
+ // Calculate FPS
230
+ if (this.frameTime > 0) {
231
+ this.fps = 1000 / this.frameTime;
232
+ }
233
+
234
+ // In real implementation, render Three.js scene
235
+ // For testing, just track metrics
236
+
237
+ span.setAttributes({
238
+ 'renderer.fps': Math.round(this.fps),
239
+ 'renderer.frameTime': this.frameTime,
240
+ });
241
+ } finally {
242
+ span.end();
243
+ }
244
+ });
245
+ }
246
+
247
+ /**
248
+ * Get camera position
249
+ * @returns {Object} Camera position
250
+ */
251
+ getCameraPosition() {
252
+ return { ...this.camera.position };
253
+ }
254
+
255
+ /**
256
+ * Set camera position
257
+ * @param {Object} position - New position
258
+ * @returns {void}
259
+ */
260
+ setCameraPosition(position) {
261
+ this.camera.position.x = position.x;
262
+ this.camera.position.y = position.y;
263
+ this.camera.position.z = position.z;
264
+ }
265
+
266
+ /**
267
+ * Get performance metrics
268
+ * @returns {Object} Metrics
269
+ */
270
+ getMetrics() {
271
+ return {
272
+ fps: Math.round(this.fps),
273
+ frameTime: this.frameTime,
274
+ nodeCount: this.nodeMeshes.size,
275
+ edgeCount: this.edgeMeshes.size,
276
+ xrActive: this.xrSession !== null,
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Dispose renderer
282
+ * @returns {void}
283
+ */
284
+ dispose() {
285
+ this.nodeMeshes.clear();
286
+ this.edgeMeshes.clear();
287
+ if (this.xrSession) {
288
+ this.endXRSession();
289
+ }
290
+ }
291
+ }