@holoscript/engine 6.0.3 → 6.0.4

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.
Files changed (192) hide show
  1. package/dist/AutoMesher-CK47F6AV.js +17 -0
  2. package/dist/GPUBuffers-2LHBCD7X.js +9 -0
  3. package/dist/WebGPUContext-TNEUYU2Y.js +11 -0
  4. package/dist/animation/index.cjs +38 -38
  5. package/dist/animation/index.d.cts +1 -1
  6. package/dist/animation/index.d.ts +1 -1
  7. package/dist/animation/index.js +1 -1
  8. package/dist/audio/index.cjs +16 -6
  9. package/dist/audio/index.d.cts +1 -1
  10. package/dist/audio/index.d.ts +1 -1
  11. package/dist/audio/index.js +1 -1
  12. package/dist/camera/index.cjs +23 -23
  13. package/dist/camera/index.d.cts +1 -1
  14. package/dist/camera/index.d.ts +1 -1
  15. package/dist/camera/index.js +1 -1
  16. package/dist/character/index.cjs +6 -4
  17. package/dist/character/index.js +1 -1
  18. package/dist/choreography/index.cjs +1194 -0
  19. package/dist/choreography/index.d.cts +687 -0
  20. package/dist/choreography/index.d.ts +687 -0
  21. package/dist/choreography/index.js +1156 -0
  22. package/dist/chunk-2CSNRI2N.js +217 -0
  23. package/dist/chunk-33T2WINR.js +266 -0
  24. package/dist/chunk-35R73OFM.js +1257 -0
  25. package/dist/chunk-4MMDSUNP.js +1256 -0
  26. package/dist/chunk-5V6HOU72.js +319 -0
  27. package/dist/chunk-6QOP6PYF.js +1038 -0
  28. package/dist/chunk-7KMJVHIL.js +8944 -0
  29. package/dist/chunk-7VPUC62U.js +1106 -0
  30. package/dist/chunk-A2Y6RCAT.js +1878 -0
  31. package/dist/chunk-AHM42MK6.js +8944 -0
  32. package/dist/chunk-BL7IDTHE.js +218 -0
  33. package/dist/chunk-CITOMSWL.js +10462 -0
  34. package/dist/chunk-CXDPKW2K.js +8944 -0
  35. package/dist/chunk-CXZPLD4S.js +223 -0
  36. package/dist/chunk-CZYJE7IH.js +5169 -0
  37. package/dist/chunk-D2OP7YC7.js +6325 -0
  38. package/dist/chunk-EDRVQHUU.js +1544 -0
  39. package/dist/chunk-EJSLOOW2.js +3589 -0
  40. package/dist/chunk-F53SFGW5.js +1878 -0
  41. package/dist/chunk-HCFPELPY.js +919 -0
  42. package/dist/chunk-HNEE36PY.js +93 -0
  43. package/dist/chunk-HYXNV36F.js +1256 -0
  44. package/dist/chunk-IB7KHVFY.js +821 -0
  45. package/dist/chunk-IBBO7YYG.js +690 -0
  46. package/dist/chunk-ILIBGINU.js +5470 -0
  47. package/dist/chunk-IS4MHLKN.js +5479 -0
  48. package/dist/chunk-JT2PFKWD.js +5479 -0
  49. package/dist/chunk-K4CUB4NY.js +1038 -0
  50. package/dist/chunk-KATDQXRJ.js +10462 -0
  51. package/dist/chunk-KBQE6ZFJ.js +8944 -0
  52. package/dist/chunk-KBVD5K7E.js +560 -0
  53. package/dist/chunk-KCDPVQRY.js +4088 -0
  54. package/dist/chunk-KN4QJPKN.js +8944 -0
  55. package/dist/chunk-KWJ3ROSI.js +8944 -0
  56. package/dist/chunk-L45VF6DD.js +919 -0
  57. package/dist/chunk-LY4T37YK.js +307 -0
  58. package/dist/chunk-MDN5WZXA.js +1544 -0
  59. package/dist/chunk-MGCDP6VU.js +928 -0
  60. package/dist/chunk-NCX7X6G2.js +8681 -0
  61. package/dist/chunk-OF54BPVD.js +913 -0
  62. package/dist/chunk-OWSN2Q3Q.js +690 -0
  63. package/dist/chunk-PRRB5TTA.js +406 -0
  64. package/dist/chunk-PXWVQF76.js +4086 -0
  65. package/dist/chunk-PYCOIDT2.js +812 -0
  66. package/dist/chunk-PZCSADOV.js +928 -0
  67. package/dist/chunk-Q2XBVS2K.js +1038 -0
  68. package/dist/chunk-QDZRXWN5.js +1776 -0
  69. package/dist/chunk-RNWOZ6WQ.js +913 -0
  70. package/dist/chunk-ROLFT4CJ.js +1693 -0
  71. package/dist/chunk-SLTJRZ2N.js +266 -0
  72. package/dist/chunk-SRUS5XSU.js +4088 -0
  73. package/dist/chunk-TKCA3WZ5.js +5409 -0
  74. package/dist/chunk-TNRMXYI2.js +1650 -0
  75. package/dist/chunk-TQB3GJGM.js +9763 -0
  76. package/dist/chunk-TUFGXG6K.js +510 -0
  77. package/dist/chunk-U6KMTGQJ.js +632 -0
  78. package/dist/chunk-VMGJQST6.js +8681 -0
  79. package/dist/chunk-X4F4TCG4.js +5470 -0
  80. package/dist/chunk-ZIFROE75.js +1544 -0
  81. package/dist/chunk-ZIJQYHSQ.js +1204 -0
  82. package/dist/combat/index.cjs +4 -4
  83. package/dist/combat/index.d.cts +1 -1
  84. package/dist/combat/index.d.ts +1 -1
  85. package/dist/combat/index.js +1 -1
  86. package/dist/ecs/index.cjs +1 -1
  87. package/dist/ecs/index.js +1 -1
  88. package/dist/environment/index.cjs +14 -14
  89. package/dist/environment/index.d.cts +1 -1
  90. package/dist/environment/index.d.ts +1 -1
  91. package/dist/environment/index.js +1 -1
  92. package/dist/gpu/index.cjs +4810 -0
  93. package/dist/gpu/index.js +3714 -0
  94. package/dist/hologram/index.cjs +27 -1
  95. package/dist/hologram/index.js +1 -1
  96. package/dist/index-B2PIsAmR.d.cts +2180 -0
  97. package/dist/index-B2PIsAmR.d.ts +2180 -0
  98. package/dist/index-BHySEPX7.d.cts +2921 -0
  99. package/dist/index-BJV21zuy.d.cts +341 -0
  100. package/dist/index-BJV21zuy.d.ts +341 -0
  101. package/dist/index-BQutTphC.d.cts +790 -0
  102. package/dist/index-ByIq2XrS.d.cts +3910 -0
  103. package/dist/index-BysHjDSO.d.cts +224 -0
  104. package/dist/index-BysHjDSO.d.ts +224 -0
  105. package/dist/index-CKwAJGck.d.ts +455 -0
  106. package/dist/index-CUl3QstQ.d.cts +3006 -0
  107. package/dist/index-CUl3QstQ.d.ts +3006 -0
  108. package/dist/index-CmYtNiI-.d.cts +953 -0
  109. package/dist/index-CmYtNiI-.d.ts +953 -0
  110. package/dist/index-CnRzWxi_.d.cts +522 -0
  111. package/dist/index-CnRzWxi_.d.ts +522 -0
  112. package/dist/index-CwRWbSC7.d.ts +2921 -0
  113. package/dist/index-CxKIBstO.d.ts +790 -0
  114. package/dist/index-DJ6-R8vh.d.cts +455 -0
  115. package/dist/index-DQKisbcI.d.cts +4968 -0
  116. package/dist/index-DQKisbcI.d.ts +4968 -0
  117. package/dist/index-DRT2zJez.d.ts +3910 -0
  118. package/dist/index-DfNLiAka.d.cts +192 -0
  119. package/dist/index-DfNLiAka.d.ts +192 -0
  120. package/dist/index-nMvkoRm8.d.cts +405 -0
  121. package/dist/index-nMvkoRm8.d.ts +405 -0
  122. package/dist/index-s9yOFU37.d.cts +604 -0
  123. package/dist/index-s9yOFU37.d.ts +604 -0
  124. package/dist/index.cjs +22966 -6960
  125. package/dist/index.d.cts +864 -20
  126. package/dist/index.d.ts +864 -20
  127. package/dist/index.js +3062 -48
  128. package/dist/input/index.cjs +1 -1
  129. package/dist/input/index.js +1 -1
  130. package/dist/orbital/index.cjs +3 -3
  131. package/dist/orbital/index.d.cts +1 -1
  132. package/dist/orbital/index.d.ts +1 -1
  133. package/dist/orbital/index.js +1 -1
  134. package/dist/particles/index.cjs +16 -16
  135. package/dist/particles/index.d.cts +1 -1
  136. package/dist/particles/index.d.ts +1 -1
  137. package/dist/particles/index.js +1 -1
  138. package/dist/physics/index.cjs +2377 -21
  139. package/dist/physics/index.d.cts +1 -1
  140. package/dist/physics/index.d.ts +1 -1
  141. package/dist/physics/index.js +35 -1
  142. package/dist/postfx/index.cjs +3491 -0
  143. package/dist/postfx/index.js +93 -0
  144. package/dist/procedural/index.cjs +1 -1
  145. package/dist/procedural/index.js +1 -1
  146. package/dist/puppeteer-5VF6KDVO.js +52197 -0
  147. package/dist/puppeteer-IZVZ3SG4.js +52197 -0
  148. package/dist/rendering/index.cjs +33 -32
  149. package/dist/rendering/index.d.cts +1 -1
  150. package/dist/rendering/index.d.ts +1 -1
  151. package/dist/rendering/index.js +8 -6
  152. package/dist/runtime/index.cjs +23 -13
  153. package/dist/runtime/index.d.cts +1 -1
  154. package/dist/runtime/index.d.ts +1 -1
  155. package/dist/runtime/index.js +8 -6
  156. package/dist/runtime/protocols/index.cjs +349 -0
  157. package/dist/runtime/protocols/index.js +15 -0
  158. package/dist/scene/index.cjs +8 -8
  159. package/dist/scene/index.d.cts +1 -1
  160. package/dist/scene/index.d.ts +1 -1
  161. package/dist/scene/index.js +1 -1
  162. package/dist/shader/index.cjs +3087 -0
  163. package/dist/shader/index.js +3044 -0
  164. package/dist/simulation/index.cjs +10680 -0
  165. package/dist/simulation/index.d.cts +3 -0
  166. package/dist/simulation/index.d.ts +3 -0
  167. package/dist/simulation/index.js +307 -0
  168. package/dist/spatial/index.cjs +2443 -0
  169. package/dist/spatial/index.d.cts +1545 -0
  170. package/dist/spatial/index.d.ts +1545 -0
  171. package/dist/spatial/index.js +2400 -0
  172. package/dist/terrain/index.cjs +1 -1
  173. package/dist/terrain/index.d.cts +1 -1
  174. package/dist/terrain/index.d.ts +1 -1
  175. package/dist/terrain/index.js +1 -1
  176. package/dist/transformers.node-4NKAPD5U.js +45620 -0
  177. package/dist/vm/index.cjs +7 -8
  178. package/dist/vm/index.d.cts +1 -1
  179. package/dist/vm/index.d.ts +1 -1
  180. package/dist/vm/index.js +1 -1
  181. package/dist/vm-bridge/index.cjs +2 -2
  182. package/dist/vm-bridge/index.d.cts +2 -2
  183. package/dist/vm-bridge/index.d.ts +2 -2
  184. package/dist/vm-bridge/index.js +1 -1
  185. package/dist/vr/index.cjs +6 -6
  186. package/dist/vr/index.js +1 -1
  187. package/dist/world/index.cjs +3 -3
  188. package/dist/world/index.d.cts +1 -1
  189. package/dist/world/index.d.ts +1 -1
  190. package/dist/world/index.js +1 -1
  191. package/package.json +53 -21
  192. package/LICENSE +0 -21
@@ -0,0 +1,2443 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/spatial/index.ts
21
+ var spatial_exports = {};
22
+ __export(spatial_exports, {
23
+ DEFAULT_SPATIAL_CONFIG: () => DEFAULT_SPATIAL_CONFIG,
24
+ OctreeLODSystem: () => OctreeLODSystem,
25
+ OctreeSystem: () => OctreeSystem,
26
+ SpatialConstraintValidator: () => SpatialConstraintValidator,
27
+ SpatialContextProvider: () => SpatialContextProvider,
28
+ SpatialQueryExecutor: () => SpatialQueryExecutor,
29
+ add: () => add,
30
+ boxesOverlap: () => boxesOverlap,
31
+ cross: () => cross,
32
+ distance: () => distance,
33
+ distanceSquared: () => distanceSquared,
34
+ dot: () => dot,
35
+ getBoxCenter: () => getBoxCenter,
36
+ isPointInBox: () => isPointInBox,
37
+ isPointInSphere: () => isPointInSphere,
38
+ lerp: () => lerp,
39
+ normalize: () => normalize,
40
+ scale: () => scale,
41
+ subtract: () => subtract
42
+ });
43
+ module.exports = __toCommonJS(spatial_exports);
44
+
45
+ // src/spatial/SpatialTypes.ts
46
+ var DEFAULT_SPATIAL_CONFIG = {
47
+ updateRate: 30,
48
+ perceptionRadius: 10,
49
+ visibilityRadius: 50,
50
+ trackRegions: true,
51
+ computeSightLines: true,
52
+ entityTypeFilter: [],
53
+ movementThreshold: 0.01
54
+ };
55
+ function distance(a, b) {
56
+ const dx = b.x - a.x;
57
+ const dy = b.y - a.y;
58
+ const dz = b.z - a.z;
59
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
60
+ }
61
+ function distanceSquared(a, b) {
62
+ const dx = b.x - a.x;
63
+ const dy = b.y - a.y;
64
+ const dz = b.z - a.z;
65
+ return dx * dx + dy * dy + dz * dz;
66
+ }
67
+ function isPointInBox(point, box) {
68
+ return point.x >= box.min.x && point.x <= box.max.x && point.y >= box.min.y && point.y <= box.max.y && point.z >= box.min.z && point.z <= box.max.z;
69
+ }
70
+ function isPointInSphere(point, sphere) {
71
+ return distance(point, sphere.center) <= sphere.radius;
72
+ }
73
+ function getBoxCenter(box) {
74
+ return {
75
+ x: (box.min.x + box.max.x) / 2,
76
+ y: (box.min.y + box.max.y) / 2,
77
+ z: (box.min.z + box.max.z) / 2
78
+ };
79
+ }
80
+ function boxesOverlap(a, b) {
81
+ return a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y && a.min.z <= b.max.z && a.max.z >= b.min.z;
82
+ }
83
+ function normalize(v) {
84
+ const len = Math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
85
+ if (len === 0) return { x: 0, y: 0, z: 0 };
86
+ return { x: v.x / len, y: v.y / len, z: v.z / len };
87
+ }
88
+ function subtract(a, b) {
89
+ return { x: a.x - b.x, y: a.y - b.y, z: a.z - b.z };
90
+ }
91
+ function add(a, b) {
92
+ return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z };
93
+ }
94
+ function scale(v, s) {
95
+ return { x: v.x * s, y: v.y * s, z: v.z * s };
96
+ }
97
+ function dot(a, b) {
98
+ return a.x * b.x + a.y * b.y + a.z * b.z;
99
+ }
100
+ function cross(a, b) {
101
+ return {
102
+ x: a.y * b.z - a.z * b.y,
103
+ y: a.z * b.x - a.x * b.z,
104
+ z: a.x * b.y - a.y * b.x
105
+ };
106
+ }
107
+ function lerp(a, b, t) {
108
+ return {
109
+ x: a.x + (b.x - a.x) * t,
110
+ y: a.y + (b.y - a.y) * t,
111
+ z: a.z + (b.z - a.z) * t
112
+ };
113
+ }
114
+
115
+ // src/spatial/SpatialQuery.ts
116
+ var SpatialQueryExecutor = class {
117
+ entities = /* @__PURE__ */ new Map();
118
+ regions = /* @__PURE__ */ new Map();
119
+ spatialIndex;
120
+ constructor() {
121
+ this.spatialIndex = new SpatialIndex();
122
+ }
123
+ /**
124
+ * Update the entity set
125
+ */
126
+ updateEntities(entities) {
127
+ this.entities.clear();
128
+ for (const entity of entities) {
129
+ this.entities.set(entity.id, entity);
130
+ }
131
+ this.spatialIndex.rebuild(entities);
132
+ }
133
+ /**
134
+ * Update regions
135
+ */
136
+ updateRegions(regions) {
137
+ this.regions.clear();
138
+ for (const region of regions) {
139
+ this.regions.set(region.id, region);
140
+ }
141
+ }
142
+ /**
143
+ * Execute a spatial query
144
+ */
145
+ execute(query) {
146
+ let candidates = Array.from(this.entities.values());
147
+ if (query.entityTypeFilter && query.entityTypeFilter.length > 0) {
148
+ candidates = candidates.filter((e) => query.entityTypeFilter.includes(e.type));
149
+ }
150
+ let results;
151
+ switch (query.type) {
152
+ case "nearest":
153
+ results = this.executeNearest(query, candidates);
154
+ break;
155
+ case "within":
156
+ results = this.executeWithin(query, candidates);
157
+ break;
158
+ case "visible":
159
+ results = this.executeVisible(query, candidates);
160
+ break;
161
+ case "reachable":
162
+ results = this.executeReachable(query, candidates);
163
+ break;
164
+ case "in_region":
165
+ results = this.executeInRegion(query, candidates);
166
+ break;
167
+ case "by_type":
168
+ results = this.executeByType(query, candidates);
169
+ break;
170
+ case "raycast":
171
+ results = this.executeRaycast(query, candidates);
172
+ break;
173
+ default:
174
+ results = [];
175
+ }
176
+ if (query.maxResults && results.length > query.maxResults) {
177
+ results = results.slice(0, query.maxResults);
178
+ }
179
+ return results;
180
+ }
181
+ /**
182
+ * Find nearest entities
183
+ */
184
+ executeNearest(query, candidates) {
185
+ const results = candidates.map((entity) => ({
186
+ entity,
187
+ distance: distance(query.from, entity.position),
188
+ direction: normalize(subtract(entity.position, query.from))
189
+ }));
190
+ results.sort((a, b) => a.distance - b.distance);
191
+ const count = query.count || 1;
192
+ return results.slice(0, count);
193
+ }
194
+ /**
195
+ * Find entities within radius
196
+ */
197
+ executeWithin(query, candidates) {
198
+ const _radiusSq = query.radius * query.radius;
199
+ return candidates.map((entity) => {
200
+ const dist = distance(query.from, entity.position);
201
+ return {
202
+ entity,
203
+ distance: dist,
204
+ direction: normalize(subtract(entity.position, query.from))
205
+ };
206
+ }).filter((r) => {
207
+ if (query.includePartial && r.entity.bounds) {
208
+ return r.distance - this.getEntityRadius(r.entity) <= query.radius;
209
+ }
210
+ return r.distance <= query.radius;
211
+ }).sort((a, b) => a.distance - b.distance);
212
+ }
213
+ /**
214
+ * Find visible entities
215
+ */
216
+ executeVisible(query, candidates) {
217
+ const results = [];
218
+ const maxDist = query.maxDistance || Infinity;
219
+ for (const entity of candidates) {
220
+ const dir = subtract(entity.position, query.from);
221
+ const dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
222
+ if (dist > maxDist) continue;
223
+ if (dist === 0) continue;
224
+ if (query.direction && query.fov !== void 0) {
225
+ const normalizedDir = normalize(dir);
226
+ const dotProduct = dot(normalize(query.direction), normalizedDir);
227
+ const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct))) * (180 / Math.PI);
228
+ if (angle > query.fov / 2) continue;
229
+ }
230
+ const sightLine = this.checkSightLine(query.from, entity.position, candidates, entity.id);
231
+ if (!sightLine.blocked) {
232
+ results.push({
233
+ entity,
234
+ distance: dist,
235
+ direction: normalize(dir),
236
+ sightLine
237
+ });
238
+ }
239
+ }
240
+ return results.sort((a, b) => a.distance - b.distance);
241
+ }
242
+ /**
243
+ * Find reachable entities (simplified - no full pathfinding)
244
+ */
245
+ executeReachable(query, candidates) {
246
+ const maxDist = query.maxDistance || Infinity;
247
+ const obstacles = query.obstacles || [];
248
+ return candidates.map((entity) => ({
249
+ entity,
250
+ distance: distance(query.from, entity.position),
251
+ direction: normalize(subtract(entity.position, query.from))
252
+ })).filter((r) => {
253
+ if (r.distance > maxDist) return false;
254
+ const sight = this.checkSightLine(query.from, r.entity.position, obstacles, r.entity.id);
255
+ return !sight.blocked;
256
+ }).sort((a, b) => a.distance - b.distance);
257
+ }
258
+ /**
259
+ * Find entities in a region
260
+ */
261
+ executeInRegion(query, candidates) {
262
+ return candidates.filter((entity) => this.isInRegion(entity.position, query.region)).map((entity) => ({
263
+ entity,
264
+ distance: distance(query.from, entity.position),
265
+ direction: normalize(subtract(entity.position, query.from))
266
+ })).sort((a, b) => a.distance - b.distance);
267
+ }
268
+ /**
269
+ * Find entities by type
270
+ */
271
+ executeByType(query, candidates) {
272
+ let filtered = candidates.filter((e) => query.entityTypes.includes(e.type));
273
+ if (query.radius !== void 0) {
274
+ filtered = filtered.filter((e) => distance(query.from, e.position) <= query.radius);
275
+ }
276
+ return filtered.map((entity) => ({
277
+ entity,
278
+ distance: distance(query.from, entity.position),
279
+ direction: normalize(subtract(entity.position, query.from))
280
+ })).sort((a, b) => a.distance - b.distance);
281
+ }
282
+ /**
283
+ * Execute raycast
284
+ */
285
+ executeRaycast(query, candidates) {
286
+ const results = [];
287
+ const dir = normalize(query.direction);
288
+ for (const entity of candidates) {
289
+ const hit = this.raycastEntity(query.from, dir, entity, query.maxDistance);
290
+ if (hit) {
291
+ results.push({
292
+ entity,
293
+ distance: hit.distance,
294
+ direction: dir
295
+ });
296
+ }
297
+ }
298
+ results.sort((a, b) => a.distance - b.distance);
299
+ if (query.hitFirst && results.length > 0) {
300
+ return [results[0]];
301
+ }
302
+ return results;
303
+ }
304
+ /**
305
+ * Check line of sight between two points
306
+ */
307
+ checkSightLine(from, to, entities, excludeId) {
308
+ const dir = subtract(to, from);
309
+ const dist = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
310
+ const normalizedDir = dist > 0 ? scale(dir, 1 / dist) : { x: 0, y: 0, z: 0 };
311
+ for (const entity of entities) {
312
+ if (entity.id === excludeId) continue;
313
+ const hit = this.raycastEntity(from, normalizedDir, entity, dist);
314
+ if (hit && hit.distance < dist - 1e-3) {
315
+ return {
316
+ from,
317
+ to,
318
+ blocked: true,
319
+ blockingEntity: entity.id,
320
+ distance: dist
321
+ };
322
+ }
323
+ }
324
+ return { from, to, blocked: false, distance: dist };
325
+ }
326
+ /**
327
+ * Raycast against a single entity
328
+ */
329
+ raycastEntity(origin, direction, entity, maxDistance) {
330
+ const radius = this.getEntityRadius(entity);
331
+ const center = entity.position;
332
+ const oc = subtract(origin, center);
333
+ const a = dot(direction, direction);
334
+ const b = 2 * dot(oc, direction);
335
+ const c = dot(oc, oc) - radius * radius;
336
+ const discriminant = b * b - 4 * a * c;
337
+ if (discriminant < 0) return null;
338
+ const t = (-b - Math.sqrt(discriminant)) / (2 * a);
339
+ if (t < 0 || t > maxDistance) return null;
340
+ const point = add(origin, scale(direction, t));
341
+ return {
342
+ entity,
343
+ point,
344
+ distance: t,
345
+ normal: normalize(subtract(point, center))
346
+ };
347
+ }
348
+ /**
349
+ * Get approximate radius of an entity
350
+ */
351
+ getEntityRadius(entity) {
352
+ if (!entity.bounds) return 0.5;
353
+ if ("radius" in entity.bounds) {
354
+ return entity.bounds.radius;
355
+ }
356
+ const box = entity.bounds;
357
+ const dx = box.max.x - box.min.x;
358
+ const dy = box.max.y - box.min.y;
359
+ const dz = box.max.z - box.min.z;
360
+ return Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
361
+ }
362
+ /**
363
+ * Check if point is in region
364
+ */
365
+ isInRegion(point, region) {
366
+ if ("radius" in region.bounds) {
367
+ return isPointInSphere(point, region.bounds);
368
+ }
369
+ return isPointInBox(point, region.bounds);
370
+ }
371
+ };
372
+ var SpatialIndex = class {
373
+ cellSize = 10;
374
+ cells = /* @__PURE__ */ new Map();
375
+ /**
376
+ * Rebuild the index with new entities
377
+ */
378
+ rebuild(entities) {
379
+ this.cells.clear();
380
+ for (const entity of entities) {
381
+ const cellKey = this.getCellKey(entity.position);
382
+ const cell = this.cells.get(cellKey) || [];
383
+ cell.push(entity);
384
+ this.cells.set(cellKey, cell);
385
+ }
386
+ }
387
+ /**
388
+ * Get entities in neighboring cells
389
+ */
390
+ getNeighbors(position, radius) {
391
+ const results = [];
392
+ const cellRadius = Math.ceil(radius / this.cellSize);
393
+ const cx = Math.floor(position.x / this.cellSize);
394
+ const cy = Math.floor(position.y / this.cellSize);
395
+ const cz = Math.floor(position.z / this.cellSize);
396
+ for (let dx = -cellRadius; dx <= cellRadius; dx++) {
397
+ for (let dy = -cellRadius; dy <= cellRadius; dy++) {
398
+ for (let dz = -cellRadius; dz <= cellRadius; dz++) {
399
+ const key = `${cx + dx},${cy + dy},${cz + dz}`;
400
+ const cell = this.cells.get(key);
401
+ if (cell) {
402
+ results.push(...cell);
403
+ }
404
+ }
405
+ }
406
+ }
407
+ return results;
408
+ }
409
+ getCellKey(position) {
410
+ const cx = Math.floor(position.x / this.cellSize);
411
+ const cy = Math.floor(position.y / this.cellSize);
412
+ const cz = Math.floor(position.z / this.cellSize);
413
+ return `${cx},${cy},${cz}`;
414
+ }
415
+ };
416
+
417
+ // src/spatial/SpatialContextProvider.ts
418
+ var import_events = require("events");
419
+ var SpatialContextProvider = class extends import_events.EventEmitter {
420
+ agents = /* @__PURE__ */ new Map();
421
+ entities = /* @__PURE__ */ new Map();
422
+ regions = /* @__PURE__ */ new Map();
423
+ queryExecutor;
424
+ updateInterval = null;
425
+ isRunning = false;
426
+ constructor() {
427
+ super();
428
+ this.queryExecutor = new SpatialQueryExecutor();
429
+ }
430
+ // ===========================================================================
431
+ // LIFECYCLE
432
+ // ===========================================================================
433
+ /**
434
+ * Start the spatial context provider
435
+ */
436
+ start() {
437
+ if (this.isRunning) return;
438
+ this.isRunning = true;
439
+ const minInterval = this.getMinUpdateInterval();
440
+ if (minInterval > 0) {
441
+ this.updateInterval = setInterval(() => this.update(), minInterval);
442
+ }
443
+ }
444
+ /**
445
+ * Stop the spatial context provider
446
+ */
447
+ stop() {
448
+ if (!this.isRunning) return;
449
+ this.isRunning = false;
450
+ if (this.updateInterval) {
451
+ clearInterval(this.updateInterval);
452
+ this.updateInterval = null;
453
+ }
454
+ }
455
+ /**
456
+ * Manual update (for testing or custom update loops)
457
+ */
458
+ update() {
459
+ const now = Date.now();
460
+ for (const [agentId, state] of this.agents) {
461
+ this.updateAgentContext(agentId, state, now);
462
+ }
463
+ }
464
+ // ===========================================================================
465
+ // AGENT REGISTRATION
466
+ // ===========================================================================
467
+ /**
468
+ * Register an agent for spatial awareness
469
+ */
470
+ registerAgent(agentId, position, config = {}) {
471
+ const fullConfig = { ...DEFAULT_SPATIAL_CONFIG, ...config };
472
+ const state = {
473
+ id: agentId,
474
+ position,
475
+ config: fullConfig,
476
+ lastContext: null,
477
+ trackedEntities: /* @__PURE__ */ new Map(),
478
+ currentRegions: /* @__PURE__ */ new Set(),
479
+ subscriptions: /* @__PURE__ */ new Map()
480
+ };
481
+ this.agents.set(agentId, state);
482
+ if (this.isRunning) {
483
+ this.stop();
484
+ this.start();
485
+ }
486
+ }
487
+ /**
488
+ * Unregister an agent
489
+ */
490
+ unregisterAgent(agentId) {
491
+ this.agents.delete(agentId);
492
+ }
493
+ /**
494
+ * Update agent position
495
+ */
496
+ updateAgentPosition(agentId, position, velocity) {
497
+ const state = this.agents.get(agentId);
498
+ if (!state) return;
499
+ state.position = position;
500
+ state.velocity = velocity;
501
+ }
502
+ /**
503
+ * Get agent's current spatial context
504
+ */
505
+ getContext(agentId) {
506
+ const state = this.agents.get(agentId);
507
+ return state?.lastContext || null;
508
+ }
509
+ // ===========================================================================
510
+ // ENTITY MANAGEMENT
511
+ // ===========================================================================
512
+ /**
513
+ * Add or update an entity
514
+ */
515
+ setEntity(entity) {
516
+ this.entities.set(entity.id, entity);
517
+ this.rebuildQueryIndex();
518
+ }
519
+ /**
520
+ * Remove an entity
521
+ */
522
+ removeEntity(entityId) {
523
+ this.entities.delete(entityId);
524
+ this.rebuildQueryIndex();
525
+ }
526
+ /**
527
+ * Batch update entities
528
+ */
529
+ setEntities(entities) {
530
+ this.entities.clear();
531
+ for (const entity of entities) {
532
+ this.entities.set(entity.id, entity);
533
+ }
534
+ this.rebuildQueryIndex();
535
+ }
536
+ /**
537
+ * Get all entities
538
+ */
539
+ getEntities() {
540
+ return Array.from(this.entities.values());
541
+ }
542
+ // ===========================================================================
543
+ // REGION MANAGEMENT
544
+ // ===========================================================================
545
+ /**
546
+ * Add or update a region
547
+ */
548
+ setRegion(region) {
549
+ this.regions.set(region.id, region);
550
+ this.queryExecutor.updateRegions(Array.from(this.regions.values()));
551
+ }
552
+ /**
553
+ * Remove a region
554
+ */
555
+ removeRegion(regionId) {
556
+ this.regions.delete(regionId);
557
+ this.queryExecutor.updateRegions(Array.from(this.regions.values()));
558
+ }
559
+ /**
560
+ * Subscribe to region events for an agent
561
+ */
562
+ subscribeToRegion(agentId, regionId, callback) {
563
+ const state = this.agents.get(agentId);
564
+ if (!state) return;
565
+ state.subscriptions.set(regionId, { regionId, callback });
566
+ }
567
+ /**
568
+ * Unsubscribe from region events
569
+ */
570
+ unsubscribeFromRegion(agentId, regionId) {
571
+ const state = this.agents.get(agentId);
572
+ if (!state) return;
573
+ state.subscriptions.delete(regionId);
574
+ }
575
+ // ===========================================================================
576
+ // QUERIES
577
+ // ===========================================================================
578
+ /**
579
+ * Execute a spatial query
580
+ */
581
+ query(query) {
582
+ return this.queryExecutor.execute(query);
583
+ }
584
+ /**
585
+ * Find nearest entities to position
586
+ */
587
+ findNearest(from, count = 1, typeFilter) {
588
+ return this.query({
589
+ type: "nearest",
590
+ from,
591
+ count,
592
+ entityTypeFilter: typeFilter
593
+ });
594
+ }
595
+ /**
596
+ * Find entities within radius
597
+ */
598
+ findWithin(from, radius, typeFilter) {
599
+ return this.query({
600
+ type: "within",
601
+ from,
602
+ radius,
603
+ entityTypeFilter: typeFilter
604
+ });
605
+ }
606
+ /**
607
+ * Find visible entities from position
608
+ */
609
+ findVisible(from, direction, fov, maxDistance) {
610
+ return this.query({
611
+ type: "visible",
612
+ from,
613
+ direction,
614
+ fov,
615
+ maxDistance
616
+ });
617
+ }
618
+ // ===========================================================================
619
+ // PRIVATE METHODS
620
+ // ===========================================================================
621
+ /**
622
+ * Update context for a single agent
623
+ */
624
+ updateAgentContext(agentId, state, now) {
625
+ const { config, position } = state;
626
+ const nearbyEntities = this.findEntitiesInRadius(position, config.perceptionRadius);
627
+ const filteredEntities = config.entityTypeFilter.length > 0 ? nearbyEntities.filter((e) => config.entityTypeFilter.includes(e.type)) : nearbyEntities;
628
+ this.checkEntityEvents(agentId, state, filteredEntities, now);
629
+ const currentRegions = this.findRegionsContaining(position);
630
+ this.checkRegionEvents(agentId, state, currentRegions, now);
631
+ const sightLines = config.computeSightLines ? this.computeSightLines(position, filteredEntities) : [];
632
+ const context = {
633
+ agentPosition: position,
634
+ agentVelocity: state.velocity,
635
+ nearbyEntities: filteredEntities,
636
+ currentRegions,
637
+ allRegions: Array.from(this.regions.values()),
638
+ sightLines,
639
+ timestamp: now,
640
+ updateRate: config.updateRate
641
+ };
642
+ state.lastContext = context;
643
+ this.emit("context:updated", agentId, context);
644
+ }
645
+ /**
646
+ * Find entities within radius of position
647
+ */
648
+ findEntitiesInRadius(position, radius) {
649
+ const radiusSq = radius * radius;
650
+ return Array.from(this.entities.values()).filter(
651
+ (e) => distanceSquared(position, e.position) <= radiusSq
652
+ );
653
+ }
654
+ /**
655
+ * Find regions containing position
656
+ */
657
+ findRegionsContaining(position) {
658
+ return Array.from(this.regions.values()).filter((r) => this.isInRegion(position, r));
659
+ }
660
+ /**
661
+ * Check if position is in region
662
+ */
663
+ isInRegion(position, region) {
664
+ if ("radius" in region.bounds) {
665
+ return isPointInSphere(position, region.bounds);
666
+ }
667
+ return isPointInBox(position, region.bounds);
668
+ }
669
+ /**
670
+ * Check for entity enter/exit events
671
+ */
672
+ checkEntityEvents(agentId, state, currentEntities, now) {
673
+ const currentIds = new Set(currentEntities.map((e) => e.id));
674
+ for (const entity of currentEntities) {
675
+ if (!state.trackedEntities.has(entity.id)) {
676
+ const event = {
677
+ type: "entity_entered",
678
+ entity,
679
+ distance: distance(state.position, entity.position),
680
+ timestamp: now
681
+ };
682
+ this.emit("entity:entered", agentId, event);
683
+ }
684
+ }
685
+ for (const [entityId, cachedEntity] of state.trackedEntities) {
686
+ if (!currentIds.has(entityId)) {
687
+ const event = {
688
+ type: "entity_exited",
689
+ entity: cachedEntity,
690
+ timestamp: now
691
+ };
692
+ this.emit("entity:exited", agentId, event);
693
+ }
694
+ }
695
+ const newTrackedEntities = /* @__PURE__ */ new Map();
696
+ for (const entity of currentEntities) {
697
+ newTrackedEntities.set(entity.id, entity);
698
+ }
699
+ state.trackedEntities = newTrackedEntities;
700
+ }
701
+ /**
702
+ * Check for region enter/exit events
703
+ */
704
+ checkRegionEvents(agentId, state, currentRegions, now) {
705
+ const currentIds = new Set(currentRegions.map((r) => r.id));
706
+ for (const region of currentRegions) {
707
+ if (!state.currentRegions.has(region.id)) {
708
+ const event = {
709
+ type: "region_entered",
710
+ region,
711
+ timestamp: now
712
+ };
713
+ this.emit("region:entered", agentId, event);
714
+ const sub = state.subscriptions.get(region.id);
715
+ if (sub) {
716
+ sub.callback(event);
717
+ }
718
+ }
719
+ }
720
+ for (const regionId of state.currentRegions) {
721
+ if (!currentIds.has(regionId)) {
722
+ const region = this.regions.get(regionId);
723
+ if (region) {
724
+ const event = {
725
+ type: "region_exited",
726
+ region,
727
+ timestamp: now
728
+ };
729
+ this.emit("region:exited", agentId, event);
730
+ const sub = state.subscriptions.get(regionId);
731
+ if (sub) {
732
+ sub.callback(event);
733
+ }
734
+ }
735
+ }
736
+ }
737
+ state.currentRegions = currentIds;
738
+ }
739
+ /**
740
+ * Compute sight lines to entities
741
+ */
742
+ computeSightLines(from, entities) {
743
+ const allEntities = Array.from(this.entities.values());
744
+ return entities.map((target) => {
745
+ const dist = distance(from, target.position);
746
+ const blocked = this.isLineBlocked(from, target.position, allEntities, target.id);
747
+ return {
748
+ from,
749
+ to: target.position,
750
+ blocked: blocked.blocked,
751
+ blockingEntity: blocked.blockerId,
752
+ distance: dist
753
+ };
754
+ });
755
+ }
756
+ /**
757
+ * Check if line between two points is blocked
758
+ */
759
+ isLineBlocked(from, to, entities, excludeId) {
760
+ const dir = normalize(subtract(to, from));
761
+ const maxDist = distance(from, to);
762
+ for (const entity of entities) {
763
+ if (entity.id === excludeId) continue;
764
+ const radius = this.getEntityRadius(entity);
765
+ const toEntity = subtract(entity.position, from);
766
+ const projection = this.dot3(toEntity, dir);
767
+ if (projection < 0 || projection > maxDist) continue;
768
+ const closest = {
769
+ x: from.x + dir.x * projection,
770
+ y: from.y + dir.y * projection,
771
+ z: from.z + dir.z * projection
772
+ };
773
+ const distToLine = distance(closest, entity.position);
774
+ if (distToLine < radius) {
775
+ return { blocked: true, blockerId: entity.id };
776
+ }
777
+ }
778
+ return { blocked: false };
779
+ }
780
+ /**
781
+ * Get entity radius
782
+ */
783
+ getEntityRadius(entity) {
784
+ if (!entity.bounds) return 0.5;
785
+ if ("radius" in entity.bounds) {
786
+ return entity.bounds.radius;
787
+ }
788
+ const box = entity.bounds;
789
+ const dx = box.max.x - box.min.x;
790
+ const dy = box.max.y - box.min.y;
791
+ const dz = box.max.z - box.min.z;
792
+ return Math.sqrt(dx * dx + dy * dy + dz * dz) / 2;
793
+ }
794
+ /**
795
+ * Dot product
796
+ */
797
+ dot3(a, b) {
798
+ return a.x * b.x + a.y * b.y + a.z * b.z;
799
+ }
800
+ /**
801
+ * Rebuild the query executor index
802
+ */
803
+ rebuildQueryIndex() {
804
+ this.queryExecutor.updateEntities(Array.from(this.entities.values()));
805
+ }
806
+ /**
807
+ * Get minimum update interval
808
+ */
809
+ getMinUpdateInterval() {
810
+ let minRate = 0;
811
+ for (const state of this.agents.values()) {
812
+ if (state.config.updateRate > minRate) {
813
+ minRate = state.config.updateRate;
814
+ }
815
+ }
816
+ return minRate > 0 ? 1e3 / minRate : 0;
817
+ }
818
+ };
819
+
820
+ // src/spatial/SpatialConstraintValidator.ts
821
+ var SpatialConstraintValidator = class {
822
+ declarations = /* @__PURE__ */ new Map();
823
+ diagnostics = [];
824
+ constraintMap = /* @__PURE__ */ new Map();
825
+ // -------------------------------------------------------------------------
826
+ // Public API
827
+ // -------------------------------------------------------------------------
828
+ /**
829
+ * Validate all spatial constraints across a set of declarations.
830
+ */
831
+ validate(declarations) {
832
+ this.reset();
833
+ for (const decl of declarations) {
834
+ this.declarations.set(decl.entityId, decl);
835
+ this.constraintMap.set(decl.entityId, [...decl.constraints]);
836
+ }
837
+ this.resolveReferences();
838
+ this.detectCircularReferences();
839
+ for (const decl of declarations) {
840
+ for (const constraint of decl.constraints) {
841
+ switch (constraint.kind) {
842
+ case "spatial_adjacent":
843
+ this.validateAdjacent(decl, constraint);
844
+ break;
845
+ case "spatial_contains":
846
+ this.validateContains(decl, constraint);
847
+ break;
848
+ case "spatial_reachable":
849
+ this.validateReachable(decl, constraint);
850
+ break;
851
+ case "spatial_temporal_adjacent":
852
+ this.validateTemporalAdjacent(decl, constraint);
853
+ break;
854
+ case "spatial_temporal_reachable":
855
+ this.validateTemporalReachable(decl, constraint);
856
+ break;
857
+ case "spatial_trajectory":
858
+ this.validateTrajectory(decl, constraint);
859
+ break;
860
+ }
861
+ }
862
+ }
863
+ this.validateCrossConstraintConsistency();
864
+ const errors = this.diagnostics.filter((d) => d.severity === "error");
865
+ const warnings = this.diagnostics.filter((d) => d.severity === "warning");
866
+ const allConstraints = declarations.flatMap((d) => d.constraints);
867
+ return {
868
+ valid: errors.length === 0,
869
+ diagnostics: [...this.diagnostics],
870
+ constraintMap: new Map(this.constraintMap),
871
+ stats: {
872
+ totalConstraints: allConstraints.length,
873
+ adjacentCount: allConstraints.filter((c) => c.kind === "spatial_adjacent").length,
874
+ containsCount: allConstraints.filter((c) => c.kind === "spatial_contains").length,
875
+ reachableCount: allConstraints.filter((c) => c.kind === "spatial_reachable").length,
876
+ temporalAdjacentCount: allConstraints.filter((c) => c.kind === "spatial_temporal_adjacent").length,
877
+ temporalReachableCount: allConstraints.filter(
878
+ (c) => c.kind === "spatial_temporal_reachable"
879
+ ).length,
880
+ trajectoryCount: allConstraints.filter((c) => c.kind === "spatial_trajectory").length,
881
+ errorsCount: errors.length,
882
+ warningsCount: warnings.length
883
+ }
884
+ };
885
+ }
886
+ /**
887
+ * Validate a single constraint in isolation (useful for incremental checks).
888
+ */
889
+ validateSingle(source, constraint, allDeclarations) {
890
+ this.reset();
891
+ for (const decl of allDeclarations) {
892
+ this.declarations.set(decl.entityId, decl);
893
+ }
894
+ switch (constraint.kind) {
895
+ case "spatial_adjacent":
896
+ this.validateAdjacent(source, constraint);
897
+ break;
898
+ case "spatial_contains":
899
+ this.validateContains(source, constraint);
900
+ break;
901
+ case "spatial_reachable":
902
+ this.validateReachable(source, constraint);
903
+ break;
904
+ case "spatial_temporal_adjacent":
905
+ this.validateTemporalAdjacent(source, constraint);
906
+ break;
907
+ case "spatial_temporal_reachable":
908
+ this.validateTemporalReachable(source, constraint);
909
+ break;
910
+ case "spatial_trajectory":
911
+ this.validateTrajectory(source, constraint);
912
+ break;
913
+ }
914
+ return [...this.diagnostics];
915
+ }
916
+ // -------------------------------------------------------------------------
917
+ // Pass 1: Reference resolution
918
+ // -------------------------------------------------------------------------
919
+ resolveReferences() {
920
+ for (const [entityId, decl] of this.declarations) {
921
+ for (const constraint of decl.constraints) {
922
+ const targetId = this.getTargetId(constraint);
923
+ if (!this.declarations.has(targetId)) {
924
+ const typeMatches = Array.from(this.declarations.values()).filter(
925
+ (d) => d.entityType === targetId
926
+ );
927
+ if (typeMatches.length === 0) {
928
+ this.addDiagnostic(
929
+ "error",
930
+ "HSP033",
931
+ `Spatial constraint on '${entityId}' references target '${targetId}' which is not declared. Ensure the target entity exists in the composition.`,
932
+ decl.line ?? 0,
933
+ decl.column ?? 0,
934
+ constraint.kind,
935
+ entityId,
936
+ targetId,
937
+ [`Declare entity '${targetId}' or fix the target reference.`]
938
+ );
939
+ }
940
+ }
941
+ }
942
+ }
943
+ }
944
+ // -------------------------------------------------------------------------
945
+ // Pass 2: Circular reference detection
946
+ // -------------------------------------------------------------------------
947
+ detectCircularReferences() {
948
+ const visited = /* @__PURE__ */ new Set();
949
+ const stack = /* @__PURE__ */ new Set();
950
+ const visit = (entityId, path) => {
951
+ if (stack.has(entityId)) {
952
+ const cycle = [...path, entityId].join(" -> ");
953
+ const decl2 = this.declarations.get(entityId);
954
+ this.addDiagnostic(
955
+ "error",
956
+ "HSP034",
957
+ `Circular spatial constraint reference detected: ${cycle}. Spatial constraints must form a directed acyclic graph.`,
958
+ decl2?.line ?? 0,
959
+ decl2?.column ?? 0,
960
+ "spatial_contains",
961
+ // containment is the most common source of cycles
962
+ entityId,
963
+ path[path.length - 1] || entityId,
964
+ ["Remove one of the constraints in the cycle to break the circular dependency."]
965
+ );
966
+ return;
967
+ }
968
+ if (visited.has(entityId)) return;
969
+ visited.add(entityId);
970
+ stack.add(entityId);
971
+ const decl = this.declarations.get(entityId);
972
+ if (decl) {
973
+ for (const constraint of decl.constraints) {
974
+ if (constraint.kind === "spatial_contains") {
975
+ visit(constraint.containedId, [...path, entityId]);
976
+ }
977
+ }
978
+ }
979
+ stack.delete(entityId);
980
+ };
981
+ for (const entityId of this.declarations.keys()) {
982
+ visit(entityId, []);
983
+ }
984
+ }
985
+ // -------------------------------------------------------------------------
986
+ // Pass 3a: Adjacency validation
987
+ // -------------------------------------------------------------------------
988
+ validateAdjacent(source, constraint) {
989
+ const target = this.resolveTarget(constraint.targetId);
990
+ if (!target) return;
991
+ if (!source.position || !target.position) {
992
+ this.addDiagnostic(
993
+ "info",
994
+ "HSP030",
995
+ `spatial_adjacent constraint between '${source.entityId}' and '${constraint.targetId}': positions not fully known at compile time, deferring to runtime verification.`,
996
+ source.line ?? 0,
997
+ source.column ?? 0,
998
+ "spatial_adjacent",
999
+ source.entityId,
1000
+ constraint.targetId
1001
+ );
1002
+ return;
1003
+ }
1004
+ const dist = this.computeAxisDistance(
1005
+ source.position,
1006
+ target.position,
1007
+ constraint.axis ?? "xyz"
1008
+ );
1009
+ if (dist > constraint.maxDistance) {
1010
+ this.addDiagnostic(
1011
+ "error",
1012
+ "HSP030",
1013
+ `spatial_adjacent violation: '${source.entityId}' is ${dist.toFixed(2)}m from '${constraint.targetId}' but must be within ${constraint.maxDistance}m` + (constraint.axis && constraint.axis !== "xyz" ? ` (on ${constraint.axis} axis)` : "") + `.${constraint.label ? ` (${constraint.label})` : ""}`,
1014
+ source.line ?? 0,
1015
+ source.column ?? 0,
1016
+ "spatial_adjacent",
1017
+ source.entityId,
1018
+ constraint.targetId,
1019
+ [
1020
+ `Move '${source.entityId}' closer to '${constraint.targetId}' (currently ${dist.toFixed(2)}m, max ${constraint.maxDistance}m).`
1021
+ ]
1022
+ );
1023
+ }
1024
+ if (constraint.minDistance !== void 0 && dist < constraint.minDistance) {
1025
+ this.addDiagnostic(
1026
+ "error",
1027
+ "HSP030",
1028
+ `spatial_adjacent violation: '${source.entityId}' is ${dist.toFixed(2)}m from '${constraint.targetId}' but must be at least ${constraint.minDistance}m apart.${constraint.label ? ` (${constraint.label})` : ""}`,
1029
+ source.line ?? 0,
1030
+ source.column ?? 0,
1031
+ "spatial_adjacent",
1032
+ source.entityId,
1033
+ constraint.targetId,
1034
+ [
1035
+ `Move '${source.entityId}' further from '${constraint.targetId}' (currently ${dist.toFixed(2)}m, min ${constraint.minDistance}m).`
1036
+ ]
1037
+ );
1038
+ }
1039
+ }
1040
+ // -------------------------------------------------------------------------
1041
+ // Pass 3b: Containment validation
1042
+ // -------------------------------------------------------------------------
1043
+ validateContains(source, constraint) {
1044
+ const contained = this.resolveTarget(constraint.containedId);
1045
+ if (!contained) return;
1046
+ if (!source.bounds) {
1047
+ this.addDiagnostic(
1048
+ "warning",
1049
+ "HSP031",
1050
+ `spatial_contains constraint on '${source.entityId}': container has no declared bounds. Containment cannot be verified at compile time.`,
1051
+ source.line ?? 0,
1052
+ source.column ?? 0,
1053
+ "spatial_contains",
1054
+ source.entityId,
1055
+ constraint.containedId,
1056
+ [`Add bounds to '${source.entityId}' for compile-time containment checking.`]
1057
+ );
1058
+ return;
1059
+ }
1060
+ if (!contained.position) {
1061
+ this.addDiagnostic(
1062
+ "info",
1063
+ "HSP031",
1064
+ `spatial_contains constraint: '${constraint.containedId}' position not known at compile time, deferring to runtime.`,
1065
+ source.line ?? 0,
1066
+ source.column ?? 0,
1067
+ "spatial_contains",
1068
+ source.entityId,
1069
+ constraint.containedId
1070
+ );
1071
+ return;
1072
+ }
1073
+ const margin = constraint.margin ?? 0;
1074
+ if (constraint.strict && contained.bounds) {
1075
+ this.validateStrictContainment(source, constraint, contained, margin);
1076
+ } else {
1077
+ this.validatePointContainment(source, constraint, contained.position, margin);
1078
+ }
1079
+ if (constraint.recursive) {
1080
+ this.validateRecursiveContainment(source, constraint);
1081
+ }
1082
+ }
1083
+ /**
1084
+ * Check that a point is inside the container bounds (with margin).
1085
+ */
1086
+ validatePointContainment(container, constraint, point, margin) {
1087
+ const bounds = container.bounds;
1088
+ let isInside;
1089
+ if ("radius" in bounds) {
1090
+ const sphere = bounds;
1091
+ const dist = distance(point, sphere.center);
1092
+ isInside = dist <= sphere.radius - margin;
1093
+ } else {
1094
+ const box = bounds;
1095
+ const shrunk = {
1096
+ min: {
1097
+ x: box.min.x + margin,
1098
+ y: box.min.y + margin,
1099
+ z: box.min.z + margin
1100
+ },
1101
+ max: {
1102
+ x: box.max.x - margin,
1103
+ y: box.max.y - margin,
1104
+ z: box.max.z - margin
1105
+ }
1106
+ };
1107
+ isInside = isPointInBox(point, shrunk);
1108
+ }
1109
+ if (!isInside) {
1110
+ this.addDiagnostic(
1111
+ "error",
1112
+ "HSP031",
1113
+ `spatial_contains violation: '${constraint.containedId}' (at [${point.x}, ${point.y}, ${point.z}]) is outside container '${container.entityId}'` + (margin > 0 ? ` (with margin ${margin}m)` : "") + `.${constraint.label ? ` (${constraint.label})` : ""}`,
1114
+ container.line ?? 0,
1115
+ container.column ?? 0,
1116
+ "spatial_contains",
1117
+ container.entityId,
1118
+ constraint.containedId,
1119
+ [
1120
+ `Move '${constraint.containedId}' inside '${container.entityId}' bounds.`,
1121
+ `Alternatively, increase the container's bounds to enclose the target.`
1122
+ ]
1123
+ );
1124
+ }
1125
+ }
1126
+ /**
1127
+ * Strict containment: contained entity's full bounds must be inside container.
1128
+ */
1129
+ validateStrictContainment(container, constraint, contained, margin) {
1130
+ if (!contained.bounds || !container.bounds) return;
1131
+ const containerBox = this.toBoundingBox(container.bounds);
1132
+ const containedBox = this.toBoundingBox(contained.bounds, contained.position);
1133
+ const shrunk = {
1134
+ min: {
1135
+ x: containerBox.min.x + margin,
1136
+ y: containerBox.min.y + margin,
1137
+ z: containerBox.min.z + margin
1138
+ },
1139
+ max: {
1140
+ x: containerBox.max.x - margin,
1141
+ y: containerBox.max.y - margin,
1142
+ z: containerBox.max.z - margin
1143
+ }
1144
+ };
1145
+ const fullyInside = containedBox.min.x >= shrunk.min.x && containedBox.min.y >= shrunk.min.y && containedBox.min.z >= shrunk.min.z && containedBox.max.x <= shrunk.max.x && containedBox.max.y <= shrunk.max.y && containedBox.max.z <= shrunk.max.z;
1146
+ if (!fullyInside) {
1147
+ this.addDiagnostic(
1148
+ "error",
1149
+ "HSP031",
1150
+ `spatial_contains (strict) violation: '${constraint.containedId}' bounds extend outside container '${container.entityId}'` + (margin > 0 ? ` (with margin ${margin}m)` : "") + `.${constraint.label ? ` (${constraint.label})` : ""}`,
1151
+ container.line ?? 0,
1152
+ container.column ?? 0,
1153
+ "spatial_contains",
1154
+ container.entityId,
1155
+ constraint.containedId,
1156
+ [
1157
+ `Resize '${constraint.containedId}' to fit within '${container.entityId}'.`,
1158
+ `Alternatively, enlarge '${container.entityId}' bounds.`
1159
+ ]
1160
+ );
1161
+ }
1162
+ }
1163
+ /**
1164
+ * Recursively check containment for child entities.
1165
+ */
1166
+ validateRecursiveContainment(container, constraint) {
1167
+ const contained = this.declarations.get(constraint.containedId);
1168
+ if (!contained) return;
1169
+ for (const [, decl] of this.declarations) {
1170
+ if (decl.parentId === constraint.containedId && decl.position && container.bounds) {
1171
+ this.validatePointContainment(
1172
+ container,
1173
+ {
1174
+ ...constraint,
1175
+ containedId: decl.entityId,
1176
+ label: `recursive child of '${constraint.containedId}'`
1177
+ },
1178
+ decl.position,
1179
+ constraint.margin ?? 0
1180
+ );
1181
+ }
1182
+ }
1183
+ }
1184
+ // -------------------------------------------------------------------------
1185
+ // Pass 3c: Reachability validation
1186
+ // -------------------------------------------------------------------------
1187
+ validateReachable(source, constraint) {
1188
+ const target = this.resolveTarget(constraint.targetId);
1189
+ if (!target) return;
1190
+ if (!source.position || !target.position) {
1191
+ this.addDiagnostic(
1192
+ "info",
1193
+ "HSP032",
1194
+ `spatial_reachable constraint between '${source.entityId}' and '${constraint.targetId}': positions not known at compile time, deferring to runtime.`,
1195
+ source.line ?? 0,
1196
+ source.column ?? 0,
1197
+ "spatial_reachable",
1198
+ source.entityId,
1199
+ constraint.targetId
1200
+ );
1201
+ return;
1202
+ }
1203
+ const straightDist = distance(source.position, target.position);
1204
+ if (constraint.maxPathLength !== void 0 && straightDist > constraint.maxPathLength) {
1205
+ this.addDiagnostic(
1206
+ "error",
1207
+ "HSP032",
1208
+ `spatial_reachable violation: straight-line distance from '${source.entityId}' to '${constraint.targetId}' is ${straightDist.toFixed(2)}m, exceeding maxPathLength of ${constraint.maxPathLength}m. No valid path can exist.`,
1209
+ source.line ?? 0,
1210
+ source.column ?? 0,
1211
+ "spatial_reachable",
1212
+ source.entityId,
1213
+ constraint.targetId,
1214
+ [
1215
+ `Move '${source.entityId}' closer to '${constraint.targetId}' or increase maxPathLength.`
1216
+ ]
1217
+ );
1218
+ return;
1219
+ }
1220
+ if (constraint.obstacleTypes && constraint.obstacleTypes.length > 0) {
1221
+ const blockingObstacle = this.checkLineOfSight(
1222
+ source.position,
1223
+ target.position,
1224
+ constraint.obstacleTypes,
1225
+ source.entityId,
1226
+ constraint.targetId
1227
+ );
1228
+ if (blockingObstacle) {
1229
+ const severity = constraint.algorithm === "line_of_sight" ? "error" : "warning";
1230
+ this.addDiagnostic(
1231
+ severity,
1232
+ "HSP032",
1233
+ `spatial_reachable: line-of-sight from '${source.entityId}' to '${constraint.targetId}' is blocked by '${blockingObstacle}'` + (constraint.algorithm && constraint.algorithm !== "line_of_sight" ? `. A ${constraint.algorithm} path may still exist (runtime check required).` : ".") + (constraint.label ? ` (${constraint.label})` : ""),
1234
+ source.line ?? 0,
1235
+ source.column ?? 0,
1236
+ "spatial_reachable",
1237
+ source.entityId,
1238
+ constraint.targetId,
1239
+ [
1240
+ `Remove or reposition obstacle '${blockingObstacle}' to clear the path.`,
1241
+ `Alternatively, use algorithm: "navmesh" or "astar" for path-around-obstacles.`
1242
+ ]
1243
+ );
1244
+ }
1245
+ }
1246
+ }
1247
+ /**
1248
+ * Check line-of-sight between two points through known obstacles.
1249
+ * Returns the ID of the first blocking obstacle, or null if clear.
1250
+ */
1251
+ checkLineOfSight(from, to, obstacleTypes, excludeA, excludeB) {
1252
+ const dir = subtract(to, from);
1253
+ const len = Math.sqrt(dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
1254
+ if (len === 0) return null;
1255
+ const normalizedDir = normalize(dir);
1256
+ for (const [entityId, decl] of this.declarations) {
1257
+ if (entityId === excludeA || entityId === excludeB) continue;
1258
+ if (!obstacleTypes.includes(decl.entityType)) continue;
1259
+ if (!decl.bounds) continue;
1260
+ const box = this.toBoundingBox(decl.bounds, decl.position);
1261
+ if (this.rayIntersectsBox(from, normalizedDir, box, len)) {
1262
+ return entityId;
1263
+ }
1264
+ }
1265
+ return null;
1266
+ }
1267
+ // -------------------------------------------------------------------------
1268
+ // Pass 3d: Temporal adjacency validation
1269
+ // -------------------------------------------------------------------------
1270
+ validateTemporalAdjacent(source, constraint) {
1271
+ const target = this.resolveTarget(constraint.targetId);
1272
+ if (!target) return;
1273
+ if (constraint.minDuration < 0) {
1274
+ this.addDiagnostic(
1275
+ "error",
1276
+ "HSP036",
1277
+ `spatial_temporal_adjacent on '${source.entityId}': minDuration must be >= 0 (got ${constraint.minDuration}s).`,
1278
+ source.line ?? 0,
1279
+ source.column ?? 0,
1280
+ "spatial_temporal_adjacent",
1281
+ source.entityId,
1282
+ constraint.targetId,
1283
+ ["Set minDuration to a non-negative value."]
1284
+ );
1285
+ return;
1286
+ }
1287
+ if (constraint.gracePeriod !== void 0 && constraint.gracePeriod < 0) {
1288
+ this.addDiagnostic(
1289
+ "error",
1290
+ "HSP036",
1291
+ `spatial_temporal_adjacent on '${source.entityId}': gracePeriod must be >= 0 (got ${constraint.gracePeriod}s).`,
1292
+ source.line ?? 0,
1293
+ source.column ?? 0,
1294
+ "spatial_temporal_adjacent",
1295
+ source.entityId,
1296
+ constraint.targetId,
1297
+ ["Set gracePeriod to a non-negative value."]
1298
+ );
1299
+ return;
1300
+ }
1301
+ if (constraint.gracePeriod !== void 0 && constraint.gracePeriod > constraint.minDuration && constraint.minDuration > 0) {
1302
+ this.addDiagnostic(
1303
+ "warning",
1304
+ "HSP036",
1305
+ `spatial_temporal_adjacent on '${source.entityId}': gracePeriod (${constraint.gracePeriod}s) exceeds minDuration (${constraint.minDuration}s). The grace period allows the entity to remain out of range longer than the required adjacency duration.`,
1306
+ source.line ?? 0,
1307
+ source.column ?? 0,
1308
+ "spatial_temporal_adjacent",
1309
+ source.entityId,
1310
+ constraint.targetId,
1311
+ ["Consider reducing gracePeriod or increasing minDuration."]
1312
+ );
1313
+ }
1314
+ if (source.position && target.position) {
1315
+ const dist = this.computeAxisDistance(
1316
+ source.position,
1317
+ target.position,
1318
+ constraint.axis ?? "xyz"
1319
+ );
1320
+ if (dist > constraint.maxDistance) {
1321
+ this.addDiagnostic(
1322
+ "error",
1323
+ "HSP036",
1324
+ `spatial_temporal_adjacent violation: '${source.entityId}' is ${dist.toFixed(2)}m from '${constraint.targetId}' (max: ${constraint.maxDistance}m). The duration constraint of ${constraint.minDuration}s cannot be satisfied if entities start out of range` + (constraint.axis && constraint.axis !== "xyz" ? ` (on ${constraint.axis} axis)` : "") + `.${constraint.label ? ` (${constraint.label})` : ""}`,
1325
+ source.line ?? 0,
1326
+ source.column ?? 0,
1327
+ "spatial_temporal_adjacent",
1328
+ source.entityId,
1329
+ constraint.targetId,
1330
+ [
1331
+ `Move '${source.entityId}' within ${constraint.maxDistance}m of '${constraint.targetId}'.`,
1332
+ `Or ensure runtime movement brings entities into range.`
1333
+ ]
1334
+ );
1335
+ }
1336
+ } else {
1337
+ this.addDiagnostic(
1338
+ "info",
1339
+ "HSP036",
1340
+ `spatial_temporal_adjacent between '${source.entityId}' and '${constraint.targetId}': positions not fully known at compile time. Duration constraint (${constraint.minDuration}s hold, ${constraint.gracePeriod ?? 0}s grace) will be verified at runtime.`,
1341
+ source.line ?? 0,
1342
+ source.column ?? 0,
1343
+ "spatial_temporal_adjacent",
1344
+ source.entityId,
1345
+ constraint.targetId
1346
+ );
1347
+ }
1348
+ }
1349
+ // -------------------------------------------------------------------------
1350
+ // Pass 3e: Temporal reachability validation
1351
+ // -------------------------------------------------------------------------
1352
+ validateTemporalReachable(source, constraint) {
1353
+ const target = this.resolveTarget(constraint.targetId);
1354
+ if (!target) return;
1355
+ if (constraint.predictionHorizon <= 0) {
1356
+ this.addDiagnostic(
1357
+ "error",
1358
+ "HSP037",
1359
+ `spatial_temporal_reachable on '${source.entityId}': predictionHorizon must be > 0 (got ${constraint.predictionHorizon}s).`,
1360
+ source.line ?? 0,
1361
+ source.column ?? 0,
1362
+ "spatial_temporal_reachable",
1363
+ source.entityId,
1364
+ constraint.targetId,
1365
+ ["Set predictionHorizon to a positive value."]
1366
+ );
1367
+ return;
1368
+ }
1369
+ if (constraint.safetyMargin !== void 0 && constraint.safetyMargin < 0) {
1370
+ this.addDiagnostic(
1371
+ "error",
1372
+ "HSP037",
1373
+ `spatial_temporal_reachable on '${source.entityId}': safetyMargin must be >= 0 (got ${constraint.safetyMargin}m).`,
1374
+ source.line ?? 0,
1375
+ source.column ?? 0,
1376
+ "spatial_temporal_reachable",
1377
+ source.entityId,
1378
+ constraint.targetId,
1379
+ ["Set safetyMargin to a non-negative value."]
1380
+ );
1381
+ return;
1382
+ }
1383
+ if ((!constraint.movingObstacles || constraint.movingObstacles.length === 0) && (!constraint.staticObstacles || constraint.staticObstacles.length === 0)) {
1384
+ this.addDiagnostic(
1385
+ "warning",
1386
+ "HSP037",
1387
+ `spatial_temporal_reachable on '${source.entityId}': no moving or static obstacles specified. Consider using spatial_reachable instead if velocity prediction is not needed.`,
1388
+ source.line ?? 0,
1389
+ source.column ?? 0,
1390
+ "spatial_temporal_reachable",
1391
+ source.entityId,
1392
+ constraint.targetId,
1393
+ [
1394
+ "Add movingObstacles or staticObstacles to the constraint.",
1395
+ "Or use @spatial_reachable if velocity prediction is not needed."
1396
+ ]
1397
+ );
1398
+ }
1399
+ if (source.position && target.position) {
1400
+ const straightDist = distance(source.position, target.position);
1401
+ if (constraint.maxPathLength !== void 0 && straightDist > constraint.maxPathLength) {
1402
+ this.addDiagnostic(
1403
+ "error",
1404
+ "HSP037",
1405
+ `spatial_temporal_reachable violation: straight-line distance from '${source.entityId}' to '${constraint.targetId}' is ${straightDist.toFixed(2)}m, exceeding maxPathLength of ${constraint.maxPathLength}m. No valid path can exist (prediction horizon: ${constraint.predictionHorizon}s).`,
1406
+ source.line ?? 0,
1407
+ source.column ?? 0,
1408
+ "spatial_temporal_reachable",
1409
+ source.entityId,
1410
+ constraint.targetId,
1411
+ [
1412
+ `Move '${source.entityId}' closer to '${constraint.targetId}' or increase maxPathLength.`
1413
+ ]
1414
+ );
1415
+ }
1416
+ } else {
1417
+ this.addDiagnostic(
1418
+ "info",
1419
+ "HSP037",
1420
+ `spatial_temporal_reachable between '${source.entityId}' and '${constraint.targetId}': positions not known at compile time. Velocity-predicted reachability (horizon: ${constraint.predictionHorizon}s) will be verified at runtime.`,
1421
+ source.line ?? 0,
1422
+ source.column ?? 0,
1423
+ "spatial_temporal_reachable",
1424
+ source.entityId,
1425
+ constraint.targetId
1426
+ );
1427
+ }
1428
+ }
1429
+ // -------------------------------------------------------------------------
1430
+ // Pass 3f: Trajectory validation
1431
+ // -------------------------------------------------------------------------
1432
+ validateTrajectory(source, constraint) {
1433
+ if (constraint.horizon <= 0) {
1434
+ this.addDiagnostic(
1435
+ "error",
1436
+ "HSP038",
1437
+ `spatial_trajectory on '${source.entityId}': horizon must be > 0 (got ${constraint.horizon}s).`,
1438
+ source.line ?? 0,
1439
+ source.column ?? 0,
1440
+ "spatial_trajectory",
1441
+ source.entityId,
1442
+ constraint.regionId ?? source.entityId,
1443
+ ["Set horizon to a positive value."]
1444
+ );
1445
+ return;
1446
+ }
1447
+ if (constraint.sampleCount !== void 0 && constraint.sampleCount < 1) {
1448
+ this.addDiagnostic(
1449
+ "error",
1450
+ "HSP038",
1451
+ `spatial_trajectory on '${source.entityId}': sampleCount must be >= 1 (got ${constraint.sampleCount}).`,
1452
+ source.line ?? 0,
1453
+ source.column ?? 0,
1454
+ "spatial_trajectory",
1455
+ source.entityId,
1456
+ constraint.regionId ?? source.entityId,
1457
+ ["Set sampleCount to at least 1."]
1458
+ );
1459
+ return;
1460
+ }
1461
+ switch (constraint.mode) {
1462
+ case "keep_in":
1463
+ case "keep_out": {
1464
+ if (!constraint.regionId) {
1465
+ this.addDiagnostic(
1466
+ "error",
1467
+ "HSP038",
1468
+ `spatial_trajectory (${constraint.mode}) on '${source.entityId}': regionId is required for '${constraint.mode}' mode.`,
1469
+ source.line ?? 0,
1470
+ source.column ?? 0,
1471
+ "spatial_trajectory",
1472
+ source.entityId,
1473
+ source.entityId,
1474
+ [`Add regionId to the @spatial_trajectory constraint.`]
1475
+ );
1476
+ } else {
1477
+ const region = this.resolveTarget(constraint.regionId);
1478
+ if (region && !region.bounds) {
1479
+ this.addDiagnostic(
1480
+ "warning",
1481
+ "HSP038",
1482
+ `spatial_trajectory (${constraint.mode}) on '${source.entityId}': region '${constraint.regionId}' has no declared bounds. Trajectory containment cannot be verified at compile time.`,
1483
+ source.line ?? 0,
1484
+ source.column ?? 0,
1485
+ "spatial_trajectory",
1486
+ source.entityId,
1487
+ constraint.regionId,
1488
+ [`Add bounds to '${constraint.regionId}' for compile-time trajectory checking.`]
1489
+ );
1490
+ }
1491
+ }
1492
+ break;
1493
+ }
1494
+ case "follow": {
1495
+ if (!constraint.referencePath || constraint.referencePath.length < 2) {
1496
+ this.addDiagnostic(
1497
+ "error",
1498
+ "HSP038",
1499
+ `spatial_trajectory (follow) on '${source.entityId}': referencePath must contain at least 2 points for 'follow' mode (got ${constraint.referencePath?.length ?? 0}).`,
1500
+ source.line ?? 0,
1501
+ source.column ?? 0,
1502
+ "spatial_trajectory",
1503
+ source.entityId,
1504
+ constraint.regionId ?? source.entityId,
1505
+ ["Provide a referencePath with at least 2 waypoints."]
1506
+ );
1507
+ }
1508
+ if (constraint.maxDeviation !== void 0 && constraint.maxDeviation <= 0) {
1509
+ this.addDiagnostic(
1510
+ "error",
1511
+ "HSP038",
1512
+ `spatial_trajectory (follow) on '${source.entityId}': maxDeviation must be > 0 (got ${constraint.maxDeviation}m).`,
1513
+ source.line ?? 0,
1514
+ source.column ?? 0,
1515
+ "spatial_trajectory",
1516
+ source.entityId,
1517
+ constraint.regionId ?? source.entityId,
1518
+ ["Set maxDeviation to a positive value."]
1519
+ );
1520
+ }
1521
+ if (source.position && constraint.referencePath && constraint.referencePath.length >= 2) {
1522
+ const distToStart = distance(source.position, constraint.referencePath[0]);
1523
+ const maxDev = constraint.maxDeviation ?? 1;
1524
+ if (distToStart > maxDev * 3) {
1525
+ this.addDiagnostic(
1526
+ "warning",
1527
+ "HSP038",
1528
+ `spatial_trajectory (follow) on '${source.entityId}': entity starts ${distToStart.toFixed(2)}m from the reference path origin (maxDeviation: ${maxDev}m). The entity may immediately violate the constraint.`,
1529
+ source.line ?? 0,
1530
+ source.column ?? 0,
1531
+ "spatial_trajectory",
1532
+ source.entityId,
1533
+ constraint.regionId ?? source.entityId,
1534
+ [`Move '${source.entityId}' closer to the reference path start point.`]
1535
+ );
1536
+ }
1537
+ }
1538
+ break;
1539
+ }
1540
+ case "waypoint": {
1541
+ if (!constraint.waypoints || constraint.waypoints.length === 0) {
1542
+ this.addDiagnostic(
1543
+ "error",
1544
+ "HSP038",
1545
+ `spatial_trajectory (waypoint) on '${source.entityId}': at least one waypoint is required for 'waypoint' mode.`,
1546
+ source.line ?? 0,
1547
+ source.column ?? 0,
1548
+ "spatial_trajectory",
1549
+ source.entityId,
1550
+ constraint.regionId ?? source.entityId,
1551
+ ["Add waypoints with position and radius to the constraint."]
1552
+ );
1553
+ } else {
1554
+ for (let i = 0; i < constraint.waypoints.length; i++) {
1555
+ const wp = constraint.waypoints[i];
1556
+ if (wp.radius <= 0) {
1557
+ this.addDiagnostic(
1558
+ "error",
1559
+ "HSP038",
1560
+ `spatial_trajectory (waypoint) on '${source.entityId}': waypoint ${i} (${wp.label ?? "unnamed"}) has invalid radius ${wp.radius}m. Radius must be > 0.`,
1561
+ source.line ?? 0,
1562
+ source.column ?? 0,
1563
+ "spatial_trajectory",
1564
+ source.entityId,
1565
+ constraint.regionId ?? source.entityId,
1566
+ [`Set waypoint ${i} radius to a positive value.`]
1567
+ );
1568
+ }
1569
+ }
1570
+ }
1571
+ break;
1572
+ }
1573
+ }
1574
+ }
1575
+ // -------------------------------------------------------------------------
1576
+ // Pass 4: Cross-constraint consistency
1577
+ // -------------------------------------------------------------------------
1578
+ validateCrossConstraintConsistency() {
1579
+ for (const [entityId, decl] of this.declarations) {
1580
+ const adjacentTargets = decl.constraints.filter((c) => c.kind === "spatial_adjacent").map((c) => c.targetId);
1581
+ const containedIn = this.findContainers(entityId);
1582
+ for (const adjacentTarget of adjacentTargets) {
1583
+ for (const containerId of containedIn) {
1584
+ const container = this.declarations.get(containerId);
1585
+ const target = this.resolveTarget(adjacentTarget);
1586
+ if (container?.bounds && target?.position) {
1587
+ const isTargetReachable = this.isPointNearBounds(
1588
+ target.position,
1589
+ container.bounds,
1590
+ // Use the max distance from the adjacency constraint
1591
+ decl.constraints.filter(
1592
+ (c) => c.kind === "spatial_adjacent" && c.targetId === adjacentTarget
1593
+ ).map((c) => c.maxDistance)[0] ?? Infinity
1594
+ );
1595
+ if (!isTargetReachable) {
1596
+ this.addDiagnostic(
1597
+ "warning",
1598
+ "HSP035",
1599
+ `Potential inconsistency: '${entityId}' must be adjacent to '${adjacentTarget}' but is contained in '${containerId}'. '${adjacentTarget}' appears to be outside the container's reachable range.`,
1600
+ decl.line ?? 0,
1601
+ decl.column ?? 0,
1602
+ "spatial_adjacent",
1603
+ entityId,
1604
+ adjacentTarget,
1605
+ [
1606
+ `Ensure '${adjacentTarget}' is placed near '${containerId}' bounds.`,
1607
+ `Or adjust the adjacency maxDistance to account for container size.`
1608
+ ]
1609
+ );
1610
+ }
1611
+ }
1612
+ }
1613
+ }
1614
+ }
1615
+ }
1616
+ // -------------------------------------------------------------------------
1617
+ // Utility methods
1618
+ // -------------------------------------------------------------------------
1619
+ /**
1620
+ * Reset all internal state for a new validation run.
1621
+ */
1622
+ reset() {
1623
+ this.declarations.clear();
1624
+ this.diagnostics = [];
1625
+ this.constraintMap.clear();
1626
+ }
1627
+ /**
1628
+ * Add a diagnostic to the list.
1629
+ */
1630
+ addDiagnostic(severity, code, message, line, column, constraintKind, sourceId, targetId, suggestions) {
1631
+ this.diagnostics.push({
1632
+ severity,
1633
+ code,
1634
+ message,
1635
+ line,
1636
+ column,
1637
+ constraintKind,
1638
+ sourceId,
1639
+ targetId,
1640
+ suggestions
1641
+ });
1642
+ }
1643
+ /**
1644
+ * Resolve a target ID to a declaration, including type-based matching.
1645
+ */
1646
+ resolveTarget(targetId) {
1647
+ const direct = this.declarations.get(targetId);
1648
+ if (direct) return direct;
1649
+ for (const [, decl] of this.declarations) {
1650
+ if (decl.entityType === targetId) return decl;
1651
+ }
1652
+ return null;
1653
+ }
1654
+ /**
1655
+ * Get the target ID from any spatial constraint.
1656
+ */
1657
+ getTargetId(constraint) {
1658
+ switch (constraint.kind) {
1659
+ case "spatial_adjacent":
1660
+ return constraint.targetId;
1661
+ case "spatial_contains":
1662
+ return constraint.containedId;
1663
+ case "spatial_reachable":
1664
+ return constraint.targetId;
1665
+ case "spatial_temporal_adjacent":
1666
+ return constraint.targetId;
1667
+ case "spatial_temporal_reachable":
1668
+ return constraint.targetId;
1669
+ case "spatial_trajectory":
1670
+ return constraint.regionId ?? constraint.sourceId;
1671
+ }
1672
+ }
1673
+ /**
1674
+ * Compute distance along a specific axis filter.
1675
+ */
1676
+ computeAxisDistance(a, b, axis) {
1677
+ switch (axis) {
1678
+ case "x":
1679
+ return Math.abs(b.x - a.x);
1680
+ case "y":
1681
+ return Math.abs(b.y - a.y);
1682
+ case "z":
1683
+ return Math.abs(b.z - a.z);
1684
+ case "xy": {
1685
+ const dx = b.x - a.x;
1686
+ const dy = b.y - a.y;
1687
+ return Math.sqrt(dx * dx + dy * dy);
1688
+ }
1689
+ case "xz": {
1690
+ const dx = b.x - a.x;
1691
+ const dz = b.z - a.z;
1692
+ return Math.sqrt(dx * dx + dz * dz);
1693
+ }
1694
+ case "xyz":
1695
+ default:
1696
+ return distance(a, b);
1697
+ }
1698
+ }
1699
+ /**
1700
+ * Convert any bounds type to a BoundingBox, optionally offset by position.
1701
+ */
1702
+ toBoundingBox(bounds, offset) {
1703
+ const ox = offset?.x ?? 0;
1704
+ const oy = offset?.y ?? 0;
1705
+ const oz = offset?.z ?? 0;
1706
+ if ("radius" in bounds) {
1707
+ const sphere = bounds;
1708
+ return {
1709
+ min: {
1710
+ x: sphere.center.x + ox - sphere.radius,
1711
+ y: sphere.center.y + oy - sphere.radius,
1712
+ z: sphere.center.z + oz - sphere.radius
1713
+ },
1714
+ max: {
1715
+ x: sphere.center.x + ox + sphere.radius,
1716
+ y: sphere.center.y + oy + sphere.radius,
1717
+ z: sphere.center.z + oz + sphere.radius
1718
+ }
1719
+ };
1720
+ }
1721
+ const box = bounds;
1722
+ return {
1723
+ min: { x: box.min.x + ox, y: box.min.y + oy, z: box.min.z + oz },
1724
+ max: { x: box.max.x + ox, y: box.max.y + oy, z: box.max.z + oz }
1725
+ };
1726
+ }
1727
+ /**
1728
+ * Ray-AABB intersection test.
1729
+ */
1730
+ rayIntersectsBox(origin, direction, box, maxDist) {
1731
+ let tmin = -Infinity;
1732
+ let tmax = Infinity;
1733
+ const axes = ["x", "y", "z"];
1734
+ for (const axis of axes) {
1735
+ const d = direction[axis];
1736
+ const o = origin[axis];
1737
+ const bmin = box.min[axis];
1738
+ const bmax = box.max[axis];
1739
+ if (Math.abs(d) < 1e-10) {
1740
+ if (o < bmin || o > bmax) return false;
1741
+ } else {
1742
+ let t1 = (bmin - o) / d;
1743
+ let t2 = (bmax - o) / d;
1744
+ if (t1 > t2) {
1745
+ const tmp = t1;
1746
+ t1 = t2;
1747
+ t2 = tmp;
1748
+ }
1749
+ tmin = Math.max(tmin, t1);
1750
+ tmax = Math.min(tmax, t2);
1751
+ if (tmin > tmax) return false;
1752
+ }
1753
+ }
1754
+ return tmin >= 0 && tmin <= maxDist;
1755
+ }
1756
+ /**
1757
+ * Check if a point is within `extraRadius` of a bounding volume.
1758
+ */
1759
+ isPointNearBounds(point, bounds, extraRadius) {
1760
+ if ("radius" in bounds) {
1761
+ const sphere = bounds;
1762
+ return distance(point, sphere.center) <= sphere.radius + extraRadius;
1763
+ }
1764
+ const box = bounds;
1765
+ const expanded = {
1766
+ min: {
1767
+ x: box.min.x - extraRadius,
1768
+ y: box.min.y - extraRadius,
1769
+ z: box.min.z - extraRadius
1770
+ },
1771
+ max: {
1772
+ x: box.max.x + extraRadius,
1773
+ y: box.max.y + extraRadius,
1774
+ z: box.max.z + extraRadius
1775
+ }
1776
+ };
1777
+ return isPointInBox(point, expanded);
1778
+ }
1779
+ /**
1780
+ * Find all containers that declare spatial_contains with a given entity.
1781
+ */
1782
+ findContainers(entityId) {
1783
+ const containers = [];
1784
+ for (const [containerId, decl] of this.declarations) {
1785
+ for (const constraint of decl.constraints) {
1786
+ if (constraint.kind === "spatial_contains" && constraint.containedId === entityId) {
1787
+ containers.push(containerId);
1788
+ }
1789
+ }
1790
+ }
1791
+ return containers;
1792
+ }
1793
+ };
1794
+
1795
+ // src/spatial/OctreeSystem.ts
1796
+ var OctreeSystem = class {
1797
+ root;
1798
+ maxEntriesPerNode = 8;
1799
+ maxDepth = 8;
1800
+ entryCount = 0;
1801
+ constructor(centerX, centerY, centerZ, halfSize) {
1802
+ this.root = {
1803
+ cx: centerX,
1804
+ cy: centerY,
1805
+ cz: centerZ,
1806
+ halfSize,
1807
+ entries: [],
1808
+ children: null,
1809
+ depth: 0
1810
+ };
1811
+ }
1812
+ // ---------------------------------------------------------------------------
1813
+ // Insert / Remove
1814
+ // ---------------------------------------------------------------------------
1815
+ insert(entry) {
1816
+ const inserted = this.insertIntoNode(this.root, entry);
1817
+ if (inserted) this.entryCount++;
1818
+ return inserted;
1819
+ }
1820
+ insertIntoNode(node, entry) {
1821
+ if (!this.containsPoint(node, entry.x, entry.y, entry.z)) return false;
1822
+ if (node.children === null) {
1823
+ node.entries.push(entry);
1824
+ if (node.entries.length > this.maxEntriesPerNode && node.depth < this.maxDepth) {
1825
+ this.subdivide(node);
1826
+ }
1827
+ return true;
1828
+ }
1829
+ for (const child of node.children) {
1830
+ if (this.insertIntoNode(child, entry)) return true;
1831
+ }
1832
+ node.entries.push(entry);
1833
+ return true;
1834
+ }
1835
+ remove(id) {
1836
+ const removed = this.removeFromNode(this.root, id);
1837
+ if (removed) this.entryCount--;
1838
+ return removed;
1839
+ }
1840
+ removeFromNode(node, id) {
1841
+ const idx = node.entries.findIndex((e) => e.id === id);
1842
+ if (idx >= 0) {
1843
+ node.entries.splice(idx, 1);
1844
+ return true;
1845
+ }
1846
+ if (node.children) {
1847
+ for (const child of node.children) {
1848
+ if (this.removeFromNode(child, id)) return true;
1849
+ }
1850
+ }
1851
+ return false;
1852
+ }
1853
+ // ---------------------------------------------------------------------------
1854
+ // Queries
1855
+ // ---------------------------------------------------------------------------
1856
+ queryRadius(x, y, z, radius) {
1857
+ const results = [];
1858
+ this.queryRadiusNode(this.root, x, y, z, radius, results);
1859
+ return results;
1860
+ }
1861
+ queryRadiusNode(node, x, y, z, radius, results) {
1862
+ if (!this.sphereOverlapsNode(node, x, y, z, radius)) return;
1863
+ for (const entry of node.entries) {
1864
+ const dx = entry.x - x, dy = entry.y - y, dz = entry.z - z;
1865
+ if (Math.sqrt(dx * dx + dy * dy + dz * dz) <= radius + entry.radius) {
1866
+ results.push(entry);
1867
+ }
1868
+ }
1869
+ if (node.children) {
1870
+ for (const child of node.children) {
1871
+ this.queryRadiusNode(child, x, y, z, radius, results);
1872
+ }
1873
+ }
1874
+ }
1875
+ sphereOverlapsNode(node, x, y, z, radius) {
1876
+ const dx = Math.max(0, Math.abs(x - node.cx) - node.halfSize);
1877
+ const dy = Math.max(0, Math.abs(y - node.cy) - node.halfSize);
1878
+ const dz = Math.max(0, Math.abs(z - node.cz) - node.halfSize);
1879
+ return dx * dx + dy * dy + dz * dz <= radius * radius;
1880
+ }
1881
+ // ---------------------------------------------------------------------------
1882
+ // Subdivision
1883
+ // ---------------------------------------------------------------------------
1884
+ subdivide(node) {
1885
+ const hs = node.halfSize / 2;
1886
+ node.children = [];
1887
+ for (let x = -1; x <= 1; x += 2) {
1888
+ for (let y = -1; y <= 1; y += 2) {
1889
+ for (let z = -1; z <= 1; z += 2) {
1890
+ node.children.push({
1891
+ cx: node.cx + x * hs,
1892
+ cy: node.cy + y * hs,
1893
+ cz: node.cz + z * hs,
1894
+ halfSize: hs,
1895
+ entries: [],
1896
+ children: null,
1897
+ depth: node.depth + 1
1898
+ });
1899
+ }
1900
+ }
1901
+ }
1902
+ const entries = [...node.entries];
1903
+ node.entries = [];
1904
+ for (const entry of entries) {
1905
+ let placed = false;
1906
+ for (const child of node.children) {
1907
+ if (this.containsPoint(child, entry.x, entry.y, entry.z)) {
1908
+ child.entries.push(entry);
1909
+ placed = true;
1910
+ break;
1911
+ }
1912
+ }
1913
+ if (!placed) node.entries.push(entry);
1914
+ }
1915
+ }
1916
+ // ---------------------------------------------------------------------------
1917
+ // Helpers
1918
+ // ---------------------------------------------------------------------------
1919
+ containsPoint(node, x, y, z) {
1920
+ return Math.abs(x - node.cx) <= node.halfSize && Math.abs(y - node.cy) <= node.halfSize && Math.abs(z - node.cz) <= node.halfSize;
1921
+ }
1922
+ getEntryCount() {
1923
+ return this.entryCount;
1924
+ }
1925
+ clear() {
1926
+ this.root.entries = [];
1927
+ this.root.children = null;
1928
+ this.entryCount = 0;
1929
+ }
1930
+ };
1931
+
1932
+ // src/spatial/OctreeLODSystem.ts
1933
+ var DEFAULT_CONFIG = {
1934
+ maxDepth: 6,
1935
+ powerLawExponent: 1.5,
1936
+ baseDistance: 2,
1937
+ maxDistance: 200,
1938
+ vrMode: false,
1939
+ gaussianBudget: 0,
1940
+ perAvatarReservation: 0,
1941
+ maxAvatars: 3,
1942
+ maxAnchorsPerNode: 16
1943
+ };
1944
+ var OctreeLODSystem = class {
1945
+ root = null;
1946
+ config;
1947
+ thresholds = [];
1948
+ anchorCount = 0;
1949
+ totalGaussianCount = 0;
1950
+ activeAvatars = 0;
1951
+ nodeCount = 0;
1952
+ /** Scene center for distance calculations */
1953
+ sceneCX = 0;
1954
+ sceneCY = 0;
1955
+ sceneCZ = 0;
1956
+ constructor(config) {
1957
+ this.config = { ...DEFAULT_CONFIG, ...config };
1958
+ this.computeThresholds();
1959
+ }
1960
+ // ---------------------------------------------------------------------------
1961
+ // Initialization
1962
+ // ---------------------------------------------------------------------------
1963
+ /**
1964
+ * Initialize the octree with scene bounds.
1965
+ * Must be called before inserting anchors.
1966
+ */
1967
+ initialize(centerX, centerY, centerZ, halfSize) {
1968
+ this.sceneCX = centerX;
1969
+ this.sceneCY = centerY;
1970
+ this.sceneCZ = centerZ;
1971
+ this.root = this.createNode(centerX, centerY, centerZ, halfSize, 0);
1972
+ this.anchorCount = 0;
1973
+ this.totalGaussianCount = 0;
1974
+ this.nodeCount = 1;
1975
+ }
1976
+ /**
1977
+ * Initialize from a bounding box (min/max corners).
1978
+ */
1979
+ initializeFromBounds(minX, minY, minZ, maxX, maxY, maxZ) {
1980
+ const cx = (minX + maxX) / 2;
1981
+ const cy = (minY + maxY) / 2;
1982
+ const cz = (minZ + maxZ) / 2;
1983
+ const halfSize = Math.max(maxX - cx, maxY - cy, maxZ - cz);
1984
+ this.initialize(cx, cy, cz, halfSize);
1985
+ }
1986
+ // ---------------------------------------------------------------------------
1987
+ // Threshold Computation
1988
+ // ---------------------------------------------------------------------------
1989
+ /**
1990
+ * Compute power-law transition thresholds for LOD level selection.
1991
+ *
1992
+ * Power-law spacing (Levy flight-inspired, W.030):
1993
+ * threshold[i] = baseDistance * ((i + 1) / maxDepth) ^ exponent * (maxDistance / baseDistance)
1994
+ *
1995
+ * This produces thresholds that are tightly spaced near the camera
1996
+ * (where detail matters most) and widely spaced at far distances
1997
+ * (where coarse LOD suffices).
1998
+ */
1999
+ computeThresholds() {
2000
+ const { maxDepth, powerLawExponent, baseDistance, maxDistance } = this.config;
2001
+ this.thresholds = [];
2002
+ for (let i = 0; i < maxDepth; i++) {
2003
+ const t = (i + 1) / maxDepth;
2004
+ const threshold = baseDistance + (maxDistance - baseDistance) * Math.pow(t, powerLawExponent);
2005
+ this.thresholds.push(threshold);
2006
+ }
2007
+ }
2008
+ /**
2009
+ * Get the computed transition thresholds (read-only).
2010
+ */
2011
+ getThresholds() {
2012
+ return this.thresholds;
2013
+ }
2014
+ // ---------------------------------------------------------------------------
2015
+ // Anchor Insertion / Removal
2016
+ // ---------------------------------------------------------------------------
2017
+ /**
2018
+ * Insert an anchor Gaussian into the octree at its designated LOD level.
2019
+ * Returns true if successfully inserted.
2020
+ */
2021
+ insertAnchor(anchor) {
2022
+ if (!this.root) return false;
2023
+ if (anchor.lodLevel < 0 || anchor.lodLevel >= this.config.maxDepth) return false;
2024
+ const inserted = this.insertIntoNode(this.root, anchor);
2025
+ if (inserted) {
2026
+ this.anchorCount++;
2027
+ this.totalGaussianCount += anchor.gaussianCount;
2028
+ }
2029
+ return inserted;
2030
+ }
2031
+ insertIntoNode(node, anchor) {
2032
+ if (!this.containsPoint(node, anchor.x, anchor.y, anchor.z)) return false;
2033
+ if (node.depth === anchor.lodLevel) {
2034
+ node.anchors.push(anchor);
2035
+ node.gaussianCount += anchor.gaussianCount;
2036
+ if (node.anchors.length > this.config.maxAnchorsPerNode && node.depth < this.config.maxDepth - 1 && node.children === null) {
2037
+ this.subdivideNode(node);
2038
+ }
2039
+ return true;
2040
+ }
2041
+ if (node.children === null && node.depth < this.config.maxDepth - 1) {
2042
+ this.subdivideNode(node);
2043
+ }
2044
+ if (node.children) {
2045
+ for (const child of node.children) {
2046
+ if (this.insertIntoNode(child, anchor)) return true;
2047
+ }
2048
+ }
2049
+ if (node.depth < anchor.lodLevel) {
2050
+ node.anchors.push(anchor);
2051
+ node.gaussianCount += anchor.gaussianCount;
2052
+ return true;
2053
+ }
2054
+ return false;
2055
+ }
2056
+ /**
2057
+ * Remove an anchor by ID.
2058
+ */
2059
+ removeAnchor(id) {
2060
+ if (!this.root) return false;
2061
+ const result = this.removeFromNode(this.root, id);
2062
+ if (result.removed) {
2063
+ this.anchorCount--;
2064
+ this.totalGaussianCount -= result.gaussianCount;
2065
+ }
2066
+ return result.removed;
2067
+ }
2068
+ removeFromNode(node, id) {
2069
+ const idx = node.anchors.findIndex((a) => a.id === id);
2070
+ if (idx >= 0) {
2071
+ const count = node.anchors[idx].gaussianCount;
2072
+ node.anchors.splice(idx, 1);
2073
+ node.gaussianCount -= count;
2074
+ return { removed: true, gaussianCount: count };
2075
+ }
2076
+ if (node.children) {
2077
+ for (const child of node.children) {
2078
+ const result = this.removeFromNode(child, id);
2079
+ if (result.removed) return result;
2080
+ }
2081
+ }
2082
+ return { removed: false, gaussianCount: 0 };
2083
+ }
2084
+ /**
2085
+ * Bulk-insert anchors (more efficient than individual inserts for large scenes).
2086
+ * Anchors are sorted by LOD level for efficient tree traversal.
2087
+ */
2088
+ bulkInsert(anchors) {
2089
+ if (!this.root) return 0;
2090
+ const sorted = [...anchors].sort((a, b) => a.lodLevel - b.lodLevel);
2091
+ let inserted = 0;
2092
+ for (const anchor of sorted) {
2093
+ if (this.insertAnchor(anchor)) {
2094
+ inserted++;
2095
+ }
2096
+ }
2097
+ return inserted;
2098
+ }
2099
+ // ---------------------------------------------------------------------------
2100
+ // LOD Selection (Core Algorithm)
2101
+ // ---------------------------------------------------------------------------
2102
+ /**
2103
+ * Select LOD levels and anchors to render based on camera position.
2104
+ *
2105
+ * Algorithm:
2106
+ * 1. Compute camera distance to scene center
2107
+ * 2. Walk thresholds to find the deepest (finest) LOD level visible
2108
+ * 3. Select all levels from 0 (coarsest) through the deepest visible level
2109
+ * 4. Collect anchors from selected levels
2110
+ * 5. If budget mode, drop deepest levels until under budget
2111
+ * 6. In VR mode, subtract avatar reservations from available budget
2112
+ */
2113
+ selectLOD(cameraX, cameraY, cameraZ, avatarCount) {
2114
+ if (!this.root) {
2115
+ return {
2116
+ selectedLevels: [],
2117
+ totalGaussians: 0,
2118
+ budgetCapped: false,
2119
+ levelsDropped: 0,
2120
+ anchors: [],
2121
+ cameraDistance: 0,
2122
+ thresholds: [...this.thresholds],
2123
+ availableBudget: this.config.gaussianBudget
2124
+ };
2125
+ }
2126
+ const dx = cameraX - this.sceneCX;
2127
+ const dy = cameraY - this.sceneCY;
2128
+ const dz = cameraZ - this.sceneCZ;
2129
+ const cameraDistance = Math.sqrt(dx * dx + dy * dy + dz * dz);
2130
+ let deepestVisibleLevel = 0;
2131
+ for (let i = 0; i < this.thresholds.length; i++) {
2132
+ if (cameraDistance < this.thresholds[i]) {
2133
+ deepestVisibleLevel = i + 1;
2134
+ } else {
2135
+ break;
2136
+ }
2137
+ }
2138
+ deepestVisibleLevel = Math.min(deepestVisibleLevel, this.config.maxDepth - 1);
2139
+ const selectedLevels = [];
2140
+ for (let l = 0; l <= deepestVisibleLevel; l++) {
2141
+ selectedLevels.push(l);
2142
+ }
2143
+ const anchorsByLevel = /* @__PURE__ */ new Map();
2144
+ const gaussiansByLevel = /* @__PURE__ */ new Map();
2145
+ for (const level of selectedLevels) {
2146
+ anchorsByLevel.set(level, []);
2147
+ gaussiansByLevel.set(level, 0);
2148
+ }
2149
+ this.collectAnchors(this.root, selectedLevels, anchorsByLevel, gaussiansByLevel);
2150
+ let availableBudget = this.config.gaussianBudget;
2151
+ const actualAvatars = avatarCount ?? this.activeAvatars;
2152
+ if (this.config.vrMode && this.config.perAvatarReservation > 0 && availableBudget > 0) {
2153
+ const clampedAvatars = Math.min(actualAvatars, this.config.maxAvatars);
2154
+ const reserved = clampedAvatars * this.config.perAvatarReservation;
2155
+ availableBudget = Math.max(0, availableBudget - reserved);
2156
+ }
2157
+ let budgetCapped = false;
2158
+ let levelsDropped = 0;
2159
+ let totalGaussians = 0;
2160
+ for (const level of selectedLevels) {
2161
+ totalGaussians += gaussiansByLevel.get(level) ?? 0;
2162
+ }
2163
+ if (availableBudget > 0 && totalGaussians > availableBudget) {
2164
+ budgetCapped = true;
2165
+ const levelUtility = (level) => {
2166
+ const anchorsAtLevel = anchorsByLevel.get(level) || [];
2167
+ const gaussians = gaussiansByLevel.get(level) || 1;
2168
+ const baseUtility = Math.pow(0.7, level);
2169
+ const avgImportance = anchorsAtLevel.length > 0 ? anchorsAtLevel.reduce((sum, a) => sum + (a.importance ?? 0.5), 0) / anchorsAtLevel.length : 0.5;
2170
+ return baseUtility * avgImportance / gaussians;
2171
+ };
2172
+ while (selectedLevels.length > 1 && totalGaussians > availableBudget) {
2173
+ let worstIdx = selectedLevels.length - 1;
2174
+ let worstRatio = Infinity;
2175
+ for (let i = 1; i < selectedLevels.length; i++) {
2176
+ const ratio = levelUtility(selectedLevels[i]);
2177
+ if (ratio < worstRatio) {
2178
+ worstRatio = ratio;
2179
+ worstIdx = i;
2180
+ }
2181
+ }
2182
+ const droppedLevel = selectedLevels.splice(worstIdx, 1)[0];
2183
+ totalGaussians -= gaussiansByLevel.get(droppedLevel) ?? 0;
2184
+ anchorsByLevel.delete(droppedLevel);
2185
+ levelsDropped++;
2186
+ }
2187
+ }
2188
+ const anchors = [];
2189
+ for (const level of selectedLevels) {
2190
+ const levelAnchors = anchorsByLevel.get(level);
2191
+ if (levelAnchors) {
2192
+ anchors.push(...levelAnchors);
2193
+ }
2194
+ }
2195
+ return {
2196
+ selectedLevels,
2197
+ totalGaussians,
2198
+ budgetCapped,
2199
+ levelsDropped,
2200
+ anchors,
2201
+ cameraDistance,
2202
+ thresholds: [...this.thresholds],
2203
+ availableBudget
2204
+ };
2205
+ }
2206
+ /**
2207
+ * Recursively collect anchors from the octree that belong to the selected levels.
2208
+ */
2209
+ collectAnchors(node, levels, anchorsByLevel, gaussiansByLevel) {
2210
+ if (anchorsByLevel.has(node.depth)) {
2211
+ for (const anchor of node.anchors) {
2212
+ if (levels.includes(anchor.lodLevel)) {
2213
+ anchorsByLevel.get(anchor.lodLevel).push(anchor);
2214
+ gaussiansByLevel.set(
2215
+ anchor.lodLevel,
2216
+ (gaussiansByLevel.get(anchor.lodLevel) ?? 0) + anchor.gaussianCount
2217
+ );
2218
+ }
2219
+ }
2220
+ }
2221
+ if (node.children) {
2222
+ for (const child of node.children) {
2223
+ this.collectAnchors(child, levels, anchorsByLevel, gaussiansByLevel);
2224
+ }
2225
+ }
2226
+ }
2227
+ // ---------------------------------------------------------------------------
2228
+ // Avatar Reservation Management
2229
+ // ---------------------------------------------------------------------------
2230
+ /**
2231
+ * Set the number of active avatars (for VR budget reservation).
2232
+ */
2233
+ setActiveAvatars(count) {
2234
+ this.activeAvatars = Math.min(Math.max(0, count), this.config.maxAvatars);
2235
+ }
2236
+ /**
2237
+ * Get the number of active avatar reservations.
2238
+ */
2239
+ getActiveAvatars() {
2240
+ return this.activeAvatars;
2241
+ }
2242
+ /**
2243
+ * Get the scene-available Gaussian budget after avatar reservations.
2244
+ */
2245
+ getAvailableSceneBudget() {
2246
+ if (this.config.gaussianBudget <= 0) return Infinity;
2247
+ const reserved = this.activeAvatars * this.config.perAvatarReservation;
2248
+ return Math.max(0, this.config.gaussianBudget - reserved);
2249
+ }
2250
+ // ---------------------------------------------------------------------------
2251
+ // Auto-Assignment: Assign LOD Level from Scale
2252
+ // ---------------------------------------------------------------------------
2253
+ /**
2254
+ * Compute the appropriate LOD level for a Gaussian based on its scale.
2255
+ *
2256
+ * Larger Gaussians (coarse detail) -> lower LOD levels (shallow octree nodes)
2257
+ * Smaller Gaussians (fine detail) -> higher LOD levels (deep octree nodes)
2258
+ *
2259
+ * Uses logarithmic mapping: level = floor(log2(maxScale / scale))
2260
+ * Clamped to [0, maxDepth-1].
2261
+ */
2262
+ computeLODLevelFromScale(scale2, maxScaleInScene) {
2263
+ if (scale2 <= 0 || maxScaleInScene <= 0) return this.config.maxDepth - 1;
2264
+ if (scale2 >= maxScaleInScene) return 0;
2265
+ const ratio = maxScaleInScene / scale2;
2266
+ const level = Math.floor(Math.log2(ratio));
2267
+ return Math.min(Math.max(0, level), this.config.maxDepth - 1);
2268
+ }
2269
+ // ---------------------------------------------------------------------------
2270
+ // Octree Internals
2271
+ // ---------------------------------------------------------------------------
2272
+ createNode(cx, cy, cz, halfSize, depth) {
2273
+ return {
2274
+ cx,
2275
+ cy,
2276
+ cz,
2277
+ halfSize,
2278
+ depth,
2279
+ anchors: [],
2280
+ gaussianCount: 0,
2281
+ children: null
2282
+ };
2283
+ }
2284
+ subdivideNode(node) {
2285
+ const hs = node.halfSize / 2;
2286
+ node.children = [];
2287
+ for (let x = -1; x <= 1; x += 2) {
2288
+ for (let y = -1; y <= 1; y += 2) {
2289
+ for (let z = -1; z <= 1; z += 2) {
2290
+ const child = this.createNode(
2291
+ node.cx + x * hs,
2292
+ node.cy + y * hs,
2293
+ node.cz + z * hs,
2294
+ hs,
2295
+ node.depth + 1
2296
+ );
2297
+ node.children.push(child);
2298
+ this.nodeCount++;
2299
+ }
2300
+ }
2301
+ }
2302
+ const remaining = [];
2303
+ for (const anchor of node.anchors) {
2304
+ let placed = false;
2305
+ if (anchor.lodLevel > node.depth) {
2306
+ for (const child of node.children) {
2307
+ if (this.containsPoint(child, anchor.x, anchor.y, anchor.z)) {
2308
+ if (anchor.lodLevel === child.depth) {
2309
+ child.anchors.push(anchor);
2310
+ child.gaussianCount += anchor.gaussianCount;
2311
+ } else {
2312
+ this.insertIntoNode(child, anchor);
2313
+ }
2314
+ placed = true;
2315
+ break;
2316
+ }
2317
+ }
2318
+ }
2319
+ if (!placed) {
2320
+ remaining.push(anchor);
2321
+ }
2322
+ }
2323
+ node.anchors = remaining;
2324
+ node.gaussianCount = remaining.reduce((sum, a) => sum + a.gaussianCount, 0);
2325
+ }
2326
+ containsPoint(node, x, y, z) {
2327
+ return Math.abs(x - node.cx) <= node.halfSize && Math.abs(y - node.cy) <= node.halfSize && Math.abs(z - node.cz) <= node.halfSize;
2328
+ }
2329
+ // ---------------------------------------------------------------------------
2330
+ // Metrics & Diagnostics
2331
+ // ---------------------------------------------------------------------------
2332
+ /**
2333
+ * Get comprehensive metrics about the octree state.
2334
+ */
2335
+ getMetrics() {
2336
+ const levelStats = /* @__PURE__ */ new Map();
2337
+ for (let i = 0; i < this.config.maxDepth; i++) {
2338
+ levelStats.set(i, { level: i, anchorCount: 0, gaussianCount: 0, nodeCount: 0 });
2339
+ }
2340
+ if (this.root) {
2341
+ this.collectMetrics(this.root, levelStats);
2342
+ }
2343
+ let actualDepth = 0;
2344
+ for (const [level, stats] of levelStats) {
2345
+ if (stats.anchorCount > 0 && level > actualDepth) {
2346
+ actualDepth = level;
2347
+ }
2348
+ }
2349
+ return {
2350
+ totalAnchors: this.anchorCount,
2351
+ totalGaussians: this.totalGaussianCount,
2352
+ levels: Array.from(levelStats.values()),
2353
+ actualDepth,
2354
+ activeAvatarReservations: this.activeAvatars,
2355
+ totalNodes: this.nodeCount
2356
+ };
2357
+ }
2358
+ collectMetrics(node, stats) {
2359
+ const ls = stats.get(node.depth);
2360
+ if (ls) {
2361
+ ls.nodeCount++;
2362
+ for (const anchor of node.anchors) {
2363
+ const anchorStats = stats.get(anchor.lodLevel);
2364
+ if (anchorStats) {
2365
+ anchorStats.anchorCount++;
2366
+ anchorStats.gaussianCount += anchor.gaussianCount;
2367
+ }
2368
+ }
2369
+ }
2370
+ if (node.children) {
2371
+ for (const child of node.children) {
2372
+ this.collectMetrics(child, stats);
2373
+ }
2374
+ }
2375
+ }
2376
+ /**
2377
+ * Get the total number of anchors.
2378
+ */
2379
+ getAnchorCount() {
2380
+ return this.anchorCount;
2381
+ }
2382
+ /**
2383
+ * Get the total Gaussian count across all anchors.
2384
+ */
2385
+ getTotalGaussianCount() {
2386
+ return this.totalGaussianCount;
2387
+ }
2388
+ /**
2389
+ * Get the current configuration.
2390
+ */
2391
+ getConfig() {
2392
+ return { ...this.config };
2393
+ }
2394
+ /**
2395
+ * Update configuration (recomputes thresholds).
2396
+ */
2397
+ updateConfig(config) {
2398
+ this.config = { ...this.config, ...config };
2399
+ this.computeThresholds();
2400
+ }
2401
+ /**
2402
+ * Clear all anchors and reset the octree.
2403
+ * Preserves configuration.
2404
+ */
2405
+ clear() {
2406
+ if (this.root) {
2407
+ this.root.anchors = [];
2408
+ this.root.gaussianCount = 0;
2409
+ this.root.children = null;
2410
+ }
2411
+ this.anchorCount = 0;
2412
+ this.totalGaussianCount = 0;
2413
+ this.nodeCount = this.root ? 1 : 0;
2414
+ }
2415
+ /**
2416
+ * Check if the system has been initialized.
2417
+ */
2418
+ isInitialized() {
2419
+ return this.root !== null;
2420
+ }
2421
+ };
2422
+ // Annotate the CommonJS export names for ESM import in node:
2423
+ 0 && (module.exports = {
2424
+ DEFAULT_SPATIAL_CONFIG,
2425
+ OctreeLODSystem,
2426
+ OctreeSystem,
2427
+ SpatialConstraintValidator,
2428
+ SpatialContextProvider,
2429
+ SpatialQueryExecutor,
2430
+ add,
2431
+ boxesOverlap,
2432
+ cross,
2433
+ distance,
2434
+ distanceSquared,
2435
+ dot,
2436
+ getBoxCenter,
2437
+ isPointInBox,
2438
+ isPointInSphere,
2439
+ lerp,
2440
+ normalize,
2441
+ scale,
2442
+ subtract
2443
+ });