@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.
- package/README.md +292 -0
- package/package.json +79 -0
- package/src/collaboration.mjs +231 -0
- package/src/gesture-controller.mjs +227 -0
- package/src/index.mjs +15 -0
- package/src/layout-3d.mjs +323 -0
- package/src/lod-manager.mjs +209 -0
- package/src/schemas.mjs +161 -0
- package/src/spatial-kg-engine.mjs +264 -0
- package/src/spatial-query.mjs +429 -0
- package/src/webxr-renderer.mjs +291 -0
|
@@ -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
|
+
}
|