@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,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
|
+
}
|
package/src/schemas.mjs
ADDED
|
@@ -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
|
+
}
|