worldorbit 2.5.17 → 3.0.0

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 (203) hide show
  1. package/README.md +91 -18
  2. package/dist/browser/core/dist/atlas-edit.d.ts +11 -0
  3. package/dist/browser/core/dist/atlas-edit.js +347 -0
  4. package/dist/browser/core/dist/atlas-utils.d.ts +22 -0
  5. package/dist/browser/core/dist/atlas-utils.js +189 -0
  6. package/dist/browser/core/dist/atlas-validate.d.ts +2 -0
  7. package/dist/browser/core/dist/atlas-validate.js +488 -0
  8. package/dist/browser/core/dist/diagnostics.d.ts +10 -0
  9. package/dist/browser/core/dist/diagnostics.js +109 -0
  10. package/dist/browser/core/dist/draft-parse.d.ts +3 -0
  11. package/dist/browser/core/dist/draft-parse.js +1654 -0
  12. package/dist/browser/core/dist/draft.d.ts +21 -0
  13. package/dist/browser/core/dist/draft.js +482 -0
  14. package/dist/browser/core/dist/errors.d.ts +7 -0
  15. package/dist/browser/core/dist/errors.js +16 -0
  16. package/dist/browser/core/dist/format.d.ts +4 -0
  17. package/dist/browser/core/dist/format.js +613 -0
  18. package/dist/browser/core/dist/index.d.ts +29 -0
  19. package/dist/browser/core/dist/index.js +35 -6101
  20. package/dist/browser/core/dist/load.d.ts +4 -0
  21. package/dist/browser/core/dist/load.js +182 -0
  22. package/dist/browser/core/dist/markdown.d.ts +2 -0
  23. package/dist/browser/core/dist/markdown.js +37 -0
  24. package/dist/browser/core/dist/normalize.d.ts +2 -0
  25. package/dist/browser/core/dist/normalize.js +312 -0
  26. package/dist/browser/core/dist/parse.d.ts +2 -0
  27. package/dist/browser/core/dist/parse.js +133 -0
  28. package/dist/browser/core/dist/scene.d.ts +3 -0
  29. package/dist/browser/core/dist/scene.js +1901 -0
  30. package/dist/browser/core/dist/schema.d.ts +8 -0
  31. package/dist/browser/core/dist/schema.js +298 -0
  32. package/dist/browser/core/dist/spatial-scene.d.ts +3 -0
  33. package/dist/browser/core/dist/spatial-scene.js +420 -0
  34. package/dist/browser/core/dist/tokenize.d.ts +4 -0
  35. package/dist/browser/core/dist/tokenize.js +68 -0
  36. package/dist/browser/core/dist/types.d.ts +637 -0
  37. package/dist/browser/core/dist/types.js +1 -0
  38. package/dist/browser/core/dist/validate.d.ts +2 -0
  39. package/dist/browser/core/dist/validate.js +56 -0
  40. package/dist/browser/editor/dist/editor.d.ts +2 -0
  41. package/dist/browser/editor/dist/editor.js +3700 -0
  42. package/dist/browser/editor/dist/index.d.ts +2 -0
  43. package/dist/browser/editor/dist/index.js +1 -11702
  44. package/dist/browser/editor/dist/types.d.ts +59 -0
  45. package/dist/browser/editor/dist/types.js +1 -0
  46. package/dist/browser/markdown/dist/html.d.ts +3 -0
  47. package/dist/browser/markdown/dist/html.js +64 -0
  48. package/dist/browser/markdown/dist/index.d.ts +4 -0
  49. package/dist/browser/markdown/dist/index.js +3 -5766
  50. package/dist/browser/markdown/dist/rehype.d.ts +10 -0
  51. package/dist/browser/markdown/dist/rehype.js +49 -0
  52. package/dist/browser/markdown/dist/remark.d.ts +9 -0
  53. package/dist/browser/markdown/dist/remark.js +28 -0
  54. package/dist/browser/markdown/dist/types.d.ts +11 -0
  55. package/dist/browser/markdown/dist/types.js +1 -0
  56. package/dist/browser/viewer/dist/atlas-state.d.ts +12 -0
  57. package/dist/browser/viewer/dist/atlas-state.js +269 -0
  58. package/dist/browser/viewer/dist/atlas-viewer.d.ts +2 -0
  59. package/dist/browser/viewer/dist/atlas-viewer.js +495 -0
  60. package/dist/browser/viewer/dist/custom-element.d.ts +1 -0
  61. package/dist/browser/viewer/dist/custom-element.js +78 -0
  62. package/dist/browser/viewer/dist/embed.d.ts +24 -0
  63. package/dist/browser/viewer/dist/embed.js +172 -0
  64. package/dist/browser/viewer/dist/errors.d.ts +6 -0
  65. package/dist/browser/viewer/dist/errors.js +12 -0
  66. package/dist/browser/viewer/dist/index.d.ts +10 -0
  67. package/dist/browser/viewer/dist/index.js +9 -7901
  68. package/dist/browser/viewer/dist/minimap.d.ts +3 -0
  69. package/dist/browser/viewer/dist/minimap.js +63 -0
  70. package/dist/browser/viewer/dist/render.d.ts +6 -0
  71. package/dist/browser/viewer/dist/render.js +670 -0
  72. package/dist/browser/viewer/dist/runtime-3d.d.ts +19 -0
  73. package/dist/browser/viewer/dist/runtime-3d.js +494 -0
  74. package/dist/browser/viewer/dist/theme.d.ts +4 -0
  75. package/dist/browser/viewer/dist/theme.js +103 -0
  76. package/dist/browser/viewer/dist/tooltip.d.ts +3 -0
  77. package/dist/browser/viewer/dist/tooltip.js +198 -0
  78. package/dist/browser/viewer/dist/types.d.ts +292 -0
  79. package/dist/browser/viewer/dist/types.js +1 -0
  80. package/dist/browser/viewer/dist/vendor/three.module.js +53032 -0
  81. package/dist/browser/viewer/dist/viewer-state.d.ts +19 -0
  82. package/dist/browser/viewer/dist/viewer-state.js +162 -0
  83. package/dist/browser/viewer/dist/viewer.d.ts +2 -0
  84. package/dist/browser/viewer/dist/viewer.js +1662 -0
  85. package/dist/unpkg/core/dist/atlas-edit.d.ts +11 -0
  86. package/dist/unpkg/core/dist/atlas-edit.js +347 -0
  87. package/dist/unpkg/core/dist/atlas-utils.d.ts +22 -0
  88. package/dist/unpkg/core/dist/atlas-utils.js +189 -0
  89. package/dist/unpkg/core/dist/atlas-validate.d.ts +2 -0
  90. package/dist/unpkg/core/dist/atlas-validate.js +488 -0
  91. package/dist/unpkg/core/dist/diagnostics.d.ts +10 -0
  92. package/dist/unpkg/core/dist/diagnostics.js +109 -0
  93. package/dist/unpkg/core/dist/draft-parse.d.ts +3 -0
  94. package/dist/unpkg/core/dist/draft-parse.js +1654 -0
  95. package/dist/unpkg/core/dist/draft.d.ts +21 -0
  96. package/dist/unpkg/core/dist/draft.js +482 -0
  97. package/dist/unpkg/core/dist/errors.d.ts +7 -0
  98. package/dist/unpkg/core/dist/errors.js +16 -0
  99. package/dist/unpkg/core/dist/format.d.ts +4 -0
  100. package/dist/unpkg/core/dist/format.js +613 -0
  101. package/dist/unpkg/core/dist/index.d.ts +29 -0
  102. package/dist/unpkg/core/dist/index.js +35 -6173
  103. package/dist/unpkg/core/dist/load.d.ts +4 -0
  104. package/dist/unpkg/core/dist/load.js +182 -0
  105. package/dist/unpkg/core/dist/markdown.d.ts +2 -0
  106. package/dist/unpkg/core/dist/markdown.js +37 -0
  107. package/dist/unpkg/core/dist/normalize.d.ts +2 -0
  108. package/dist/unpkg/core/dist/normalize.js +312 -0
  109. package/dist/unpkg/core/dist/parse.d.ts +2 -0
  110. package/dist/unpkg/core/dist/parse.js +133 -0
  111. package/dist/unpkg/core/dist/scene.d.ts +3 -0
  112. package/dist/unpkg/core/dist/scene.js +1901 -0
  113. package/dist/unpkg/core/dist/schema.d.ts +8 -0
  114. package/dist/unpkg/core/dist/schema.js +298 -0
  115. package/dist/unpkg/core/dist/spatial-scene.d.ts +3 -0
  116. package/dist/unpkg/core/dist/spatial-scene.js +420 -0
  117. package/dist/unpkg/core/dist/tokenize.d.ts +4 -0
  118. package/dist/unpkg/core/dist/tokenize.js +68 -0
  119. package/dist/unpkg/core/dist/types.d.ts +637 -0
  120. package/dist/unpkg/core/dist/types.js +1 -0
  121. package/dist/unpkg/core/dist/validate.d.ts +2 -0
  122. package/dist/unpkg/core/dist/validate.js +56 -0
  123. package/dist/unpkg/editor/dist/editor.d.ts +2 -0
  124. package/dist/unpkg/editor/dist/editor.js +3700 -0
  125. package/dist/unpkg/editor/dist/index.d.ts +2 -0
  126. package/dist/unpkg/editor/dist/index.js +1 -11727
  127. package/dist/unpkg/editor/dist/types.d.ts +59 -0
  128. package/dist/unpkg/editor/dist/types.js +1 -0
  129. package/dist/unpkg/markdown/dist/html.d.ts +3 -0
  130. package/dist/unpkg/markdown/dist/html.js +64 -0
  131. package/dist/unpkg/markdown/dist/index.d.ts +4 -0
  132. package/dist/unpkg/markdown/dist/index.js +3 -5794
  133. package/dist/unpkg/markdown/dist/rehype.d.ts +10 -0
  134. package/dist/unpkg/markdown/dist/rehype.js +49 -0
  135. package/dist/unpkg/markdown/dist/remark.d.ts +9 -0
  136. package/dist/unpkg/markdown/dist/remark.js +28 -0
  137. package/dist/unpkg/markdown/dist/types.d.ts +11 -0
  138. package/dist/unpkg/markdown/dist/types.js +1 -0
  139. package/dist/unpkg/viewer/dist/atlas-state.d.ts +12 -0
  140. package/dist/unpkg/viewer/dist/atlas-state.js +269 -0
  141. package/dist/unpkg/viewer/dist/atlas-viewer.d.ts +2 -0
  142. package/dist/unpkg/viewer/dist/atlas-viewer.js +495 -0
  143. package/dist/unpkg/viewer/dist/custom-element.d.ts +1 -0
  144. package/dist/unpkg/viewer/dist/custom-element.js +78 -0
  145. package/dist/unpkg/viewer/dist/embed.d.ts +24 -0
  146. package/dist/unpkg/viewer/dist/embed.js +172 -0
  147. package/dist/unpkg/viewer/dist/errors.d.ts +6 -0
  148. package/dist/unpkg/viewer/dist/errors.js +12 -0
  149. package/dist/unpkg/viewer/dist/index.d.ts +10 -0
  150. package/dist/unpkg/viewer/dist/index.js +9 -7958
  151. package/dist/unpkg/viewer/dist/minimap.d.ts +3 -0
  152. package/dist/unpkg/viewer/dist/minimap.js +63 -0
  153. package/dist/unpkg/viewer/dist/render.d.ts +6 -0
  154. package/dist/unpkg/viewer/dist/render.js +670 -0
  155. package/dist/unpkg/viewer/dist/runtime-3d.d.ts +19 -0
  156. package/dist/unpkg/viewer/dist/runtime-3d.js +494 -0
  157. package/dist/unpkg/viewer/dist/theme.d.ts +4 -0
  158. package/dist/unpkg/viewer/dist/theme.js +103 -0
  159. package/dist/unpkg/viewer/dist/tooltip.d.ts +3 -0
  160. package/dist/unpkg/viewer/dist/tooltip.js +198 -0
  161. package/dist/unpkg/viewer/dist/types.d.ts +292 -0
  162. package/dist/unpkg/viewer/dist/types.js +1 -0
  163. package/dist/unpkg/viewer/dist/vendor/three.module.js +53032 -0
  164. package/dist/unpkg/viewer/dist/viewer-state.d.ts +19 -0
  165. package/dist/unpkg/viewer/dist/viewer-state.js +162 -0
  166. package/dist/unpkg/viewer/dist/viewer.d.ts +2 -0
  167. package/dist/unpkg/viewer/dist/viewer.js +1662 -0
  168. package/dist/unpkg/worldorbit-core.min.js +1 -12
  169. package/dist/unpkg/worldorbit-editor.min.js +1 -894
  170. package/dist/unpkg/worldorbit-markdown.min.js +1 -103
  171. package/dist/unpkg/worldorbit-viewer.min.js +1 -259
  172. package/dist/unpkg/worldorbit.js +2 -9243
  173. package/dist/unpkg/worldorbit.min.js +2 -263
  174. package/package.json +1 -1
  175. package/packages/core/dist/atlas-edit.js +1 -1
  176. package/packages/core/dist/atlas-validate.js +99 -10
  177. package/packages/core/dist/draft-parse.js +190 -15
  178. package/packages/core/dist/draft.js +50 -11
  179. package/packages/core/dist/format.js +36 -5
  180. package/packages/core/dist/index.d.ts +1 -0
  181. package/packages/core/dist/index.js +1 -0
  182. package/packages/core/dist/load.js +9 -2
  183. package/packages/core/dist/scene.js +158 -24
  184. package/packages/core/dist/spatial-scene.d.ts +3 -0
  185. package/packages/core/dist/spatial-scene.js +420 -0
  186. package/packages/core/dist/types.d.ts +124 -2
  187. package/packages/editor/dist/editor.js +130 -8
  188. package/packages/editor/dist/types.d.ts +4 -0
  189. package/packages/markdown/dist/html.js +10 -3
  190. package/packages/viewer/dist/atlas-state.js +8 -2
  191. package/packages/viewer/dist/atlas-viewer.js +20 -8
  192. package/packages/viewer/dist/custom-element.js +18 -4
  193. package/packages/viewer/dist/embed.d.ts +5 -1
  194. package/packages/viewer/dist/embed.js +58 -24
  195. package/packages/viewer/dist/errors.d.ts +6 -0
  196. package/packages/viewer/dist/errors.js +12 -0
  197. package/packages/viewer/dist/index.d.ts +1 -0
  198. package/packages/viewer/dist/index.js +1 -0
  199. package/packages/viewer/dist/runtime-3d.d.ts +19 -0
  200. package/packages/viewer/dist/runtime-3d.js +494 -0
  201. package/packages/viewer/dist/types.d.ts +25 -3
  202. package/packages/viewer/dist/vendor/three.module.js +53032 -0
  203. package/packages/viewer/dist/viewer.js +517 -41
@@ -1,7 +1,8 @@
1
- import { loadWorldOrbitSource, renderDocumentToScene, rotatePoint, } from "@worldorbit/core";
1
+ import { loadWorldOrbitSource, renderDocumentToScene, renderDocumentToSpatialScene, rotatePoint, } from "@worldorbit/core";
2
2
  import { computeVisibleObjectIds, createAtlasStateSnapshot, createViewerBookmark, deserializeViewerAtlasState, normalizeViewerFilter, sceneViewpointToLayerOptions, searchSceneObjects, serializeViewerAtlasState, viewpointToViewerFilter, } from "./atlas-state.js";
3
3
  import { renderViewerMinimap } from "./minimap.js";
4
4
  import { renderSceneToSvg } from "./render.js";
5
+ import { createViewer3DRuntime, } from "./runtime-3d.js";
5
6
  import { buildViewerTooltipDetails, renderDefaultTooltipContent, } from "./tooltip.js";
6
7
  import { DEFAULT_VIEWER_STATE, composeViewerTransform, fitViewerState, focusViewerState, invertViewerPoint, panViewerState, rotateViewerState, zoomViewerStateAt, } from "./viewer-state.js";
7
8
  const DEFAULT_VIEWER_LIMITS = {
@@ -43,6 +44,8 @@ export function createInteractiveViewer(container, options) {
43
44
  padding: options.padding,
44
45
  preset: options.preset,
45
46
  projection: options.projection,
47
+ viewMode: options.viewMode ?? "2d",
48
+ camera: options.camera ? { ...options.camera } : null,
46
49
  scaleModel: options.scaleModel ? { ...options.scaleModel } : undefined,
47
50
  theme: options.theme,
48
51
  layers: options.layers,
@@ -54,9 +57,14 @@ export function createInteractiveViewer(container, options) {
54
57
  const previousPosition = container.style.position;
55
58
  let currentInput = resolveInitialInput(options);
56
59
  let scene = renderSceneFromInput(currentInput, renderOptions);
60
+ let providedSpatialScene = options.spatialScene ?? null;
61
+ let spatialScene = renderOptions.viewMode === "3d"
62
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, providedSpatialScene)
63
+ : null;
57
64
  let state = { ...DEFAULT_VIEWER_STATE };
58
65
  let svgElement = null;
59
66
  let cameraRoot = null;
67
+ let runtime3d = null;
60
68
  let minimapRoot = null;
61
69
  let tooltipRoot = null;
62
70
  let suppressClick = false;
@@ -71,6 +79,14 @@ export function createInteractiveViewer(container, options) {
71
79
  let activeTooltipObjectId = null;
72
80
  let activeTooltipDetails = null;
73
81
  let activeViewpointId = null;
82
+ let animationFrameId = null;
83
+ let lastAnimationTimestamp = null;
84
+ let animationState = {
85
+ playing: false,
86
+ speed: 1,
87
+ timeSeconds: 0,
88
+ frozenByEvent: spatialScene?.timeFrozen ?? false,
89
+ };
74
90
  if (previousTabIndex === null) {
75
91
  container.tabIndex = 0;
76
92
  }
@@ -80,12 +96,18 @@ export function createInteractiveViewer(container, options) {
80
96
  if (!container.style.position) {
81
97
  container.style.position = "relative";
82
98
  }
99
+ syncAnimationFrozenState();
83
100
  const handleWheel = (event) => {
84
101
  if (!behavior.pointer || destroyed) {
85
102
  return;
86
103
  }
87
104
  event.preventDefault();
88
105
  container.focus();
106
+ if (is3DView()) {
107
+ const factor = clampValue(Math.exp(-event.deltaY * 0.002), 0.6, 1.6);
108
+ api.zoomBy(factor);
109
+ return;
110
+ }
89
111
  const anchor = getWorldPointFromClient(event.clientX, event.clientY);
90
112
  const factor = clampValue(Math.exp(-event.deltaY * 0.002), 0.6, 1.6);
91
113
  updateState(zoomViewerStateAt(scene, state, factor, anchor, constraints));
@@ -98,7 +120,7 @@ export function createInteractiveViewer(container, options) {
98
120
  if ((isTouch && !behavior.touch) || (!isTouch && !behavior.pointer)) {
99
121
  return;
100
122
  }
101
- if (!isTouch && event.button !== 0) {
123
+ if (!isTouch && event.button !== 0 && !is3DView()) {
102
124
  return;
103
125
  }
104
126
  container.focus();
@@ -126,6 +148,9 @@ export function createInteractiveViewer(container, options) {
126
148
  }
127
149
  const isTouch = event.pointerType === "touch";
128
150
  if (isTouch) {
151
+ if (is3DView()) {
152
+ return;
153
+ }
129
154
  if (!behavior.touch || !touchPoints.has(event.pointerId)) {
130
155
  return;
131
156
  }
@@ -154,6 +179,29 @@ export function createInteractiveViewer(container, options) {
154
179
  }
155
180
  return;
156
181
  }
182
+ if (is3DView() && behavior.pointer && activePointerId === null) {
183
+ applyHover(runtime3d?.hitTest(event.clientX, event.clientY) ?? null);
184
+ return;
185
+ }
186
+ if (is3DView() && behavior.pointer && activePointerId === event.pointerId && lastPointerPoint) {
187
+ const nextPoint = getViewportPointFromClient(event.clientX, event.clientY);
188
+ const deltaX = nextPoint.x - lastPointerPoint.x;
189
+ const deltaY = nextPoint.y - lastPointerPoint.y;
190
+ dragDistance += Math.abs(deltaX) + Math.abs(deltaY);
191
+ lastPointerPoint = nextPoint;
192
+ if (dragDistance > 2) {
193
+ suppressClick = true;
194
+ }
195
+ if (event.shiftKey || event.buttons === 2) {
196
+ api.panBy(deltaX, deltaY);
197
+ }
198
+ else {
199
+ api.rotateBy(deltaX * 0.35);
200
+ api.panBy(0, deltaY * 0.35);
201
+ }
202
+ applyHover(runtime3d?.hitTest(event.clientX, event.clientY) ?? null);
203
+ return;
204
+ }
157
205
  if (!behavior.pointer || activePointerId !== event.pointerId || !lastPointerPoint) {
158
206
  return;
159
207
  }
@@ -188,7 +236,9 @@ export function createInteractiveViewer(container, options) {
188
236
  suppressClick = false;
189
237
  return;
190
238
  }
191
- const objectId = getClosestObjectId(event.target);
239
+ const objectId = is3DView()
240
+ ? runtime3d?.hitTest(event.clientX, event.clientY) ?? null
241
+ : getClosestObjectId(event.target);
192
242
  applySelection(objectId);
193
243
  if (behavior.tooltipMode === "pinned") {
194
244
  pinnedTooltipObjectId = objectId;
@@ -196,6 +246,9 @@ export function createInteractiveViewer(container, options) {
196
246
  }
197
247
  };
198
248
  const handleMouseOver = (event) => {
249
+ if (is3DView()) {
250
+ return;
251
+ }
199
252
  const objectId = getClosestObjectId(event.target);
200
253
  applyHover(objectId);
201
254
  };
@@ -203,6 +256,9 @@ export function createInteractiveViewer(container, options) {
203
256
  applyHover(null);
204
257
  };
205
258
  const handleFocusIn = (event) => {
259
+ if (is3DView()) {
260
+ return;
261
+ }
206
262
  const objectId = getClosestObjectId(event.target);
207
263
  if (!objectId) {
208
264
  return;
@@ -216,7 +272,9 @@ export function createInteractiveViewer(container, options) {
216
272
  if (!behavior.keyboard || destroyed) {
217
273
  return;
218
274
  }
219
- const objectId = getClosestObjectId(event.target);
275
+ const objectId = is3DView()
276
+ ? state.selectedObjectId
277
+ : getClosestObjectId(event.target);
220
278
  if ((event.key === "Enter" || event.key === " ") && objectId) {
221
279
  event.preventDefault();
222
280
  applySelection(objectId);
@@ -293,18 +351,36 @@ export function createInteractiveViewer(container, options) {
293
351
  setSource(source) {
294
352
  currentInput = { kind: "source", value: source };
295
353
  scene = renderSceneFromInput(currentInput, renderOptions);
354
+ providedSpatialScene = null;
355
+ spatialScene =
356
+ renderOptions.viewMode === "3d"
357
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
358
+ : null;
359
+ syncAnimationFrozenState();
296
360
  activeViewpointId = null;
297
361
  rerenderScene(true);
298
362
  },
299
363
  setDocument(document) {
300
364
  currentInput = { kind: "document", value: document };
301
365
  scene = renderSceneFromInput(currentInput, renderOptions);
366
+ providedSpatialScene = null;
367
+ spatialScene =
368
+ renderOptions.viewMode === "3d"
369
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
370
+ : null;
371
+ syncAnimationFrozenState();
302
372
  activeViewpointId = null;
303
373
  rerenderScene(true);
304
374
  },
305
375
  setScene(nextScene) {
306
376
  currentInput = { kind: "scene", value: nextScene };
307
377
  scene = nextScene;
378
+ providedSpatialScene = null;
379
+ spatialScene =
380
+ renderOptions.viewMode === "3d"
381
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
382
+ : null;
383
+ syncAnimationFrozenState();
308
384
  activeViewpointId = null;
309
385
  rerenderScene(true);
310
386
  },
@@ -314,6 +390,30 @@ export function createInteractiveViewer(container, options) {
314
390
  getRenderOptions() {
315
391
  return cloneRenderOptions(renderOptions);
316
392
  },
393
+ getViewMode() {
394
+ return renderOptions.viewMode ?? "2d";
395
+ },
396
+ setViewMode(mode) {
397
+ const previousRenderOptions = renderOptions;
398
+ const previousSpatialScene = spatialScene;
399
+ const nextRenderOptions = mergeRenderOptions(renderOptions, { viewMode: mode });
400
+ const nextSpatialScene = mode === "3d"
401
+ ? renderSpatialSceneFromInput(currentInput, nextRenderOptions, providedSpatialScene)
402
+ : null;
403
+ renderOptions = nextRenderOptions;
404
+ spatialScene = nextSpatialScene;
405
+ syncAnimationFrozenState();
406
+ try {
407
+ rerenderScene(false);
408
+ }
409
+ catch (error) {
410
+ renderOptions = previousRenderOptions;
411
+ spatialScene = previousSpatialScene;
412
+ syncAnimationFrozenState();
413
+ rerenderScene(false);
414
+ throw error;
415
+ }
416
+ },
317
417
  listViewpoints() {
318
418
  return scene.viewpoints.slice();
319
419
  },
@@ -333,6 +433,12 @@ export function createInteractiveViewer(container, options) {
333
433
  if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
334
434
  nextRenderOptions.projection = viewpoint.projection;
335
435
  }
436
+ if (viewpoint.camera) {
437
+ nextRenderOptions.camera = { ...viewpoint.camera };
438
+ }
439
+ else if (renderOptions.camera) {
440
+ nextRenderOptions.camera = null;
441
+ }
336
442
  if (viewpointLayers) {
337
443
  nextRenderOptions.layers = viewpointLayers;
338
444
  }
@@ -361,6 +467,53 @@ export function createInteractiveViewer(container, options) {
361
467
  setActiveEvent(id) {
362
468
  api.setRenderOptions({ activeEventId: id });
363
469
  },
470
+ playAnimation() {
471
+ if (!is3DView()) {
472
+ animationState = {
473
+ ...animationState,
474
+ playing: false,
475
+ };
476
+ stopAnimationLoop();
477
+ return;
478
+ }
479
+ if (animationState.frozenByEvent) {
480
+ animationState = {
481
+ ...animationState,
482
+ playing: false,
483
+ };
484
+ return;
485
+ }
486
+ animationState = {
487
+ ...animationState,
488
+ playing: true,
489
+ };
490
+ ensureAnimationFrame();
491
+ },
492
+ pauseAnimation() {
493
+ animationState = {
494
+ ...animationState,
495
+ playing: false,
496
+ };
497
+ stopAnimationLoop();
498
+ },
499
+ resetAnimation() {
500
+ animationState = {
501
+ ...animationState,
502
+ playing: false,
503
+ timeSeconds: 0,
504
+ };
505
+ stopAnimationLoop();
506
+ syncRuntimePresentation();
507
+ },
508
+ setAnimationSpeed(multiplier) {
509
+ animationState = {
510
+ ...animationState,
511
+ speed: clampValue(multiplier, 0.1, 64),
512
+ };
513
+ },
514
+ getAnimationState() {
515
+ return { ...animationState };
516
+ },
364
517
  search(query, limit = 12) {
365
518
  return searchSceneObjects(scene, query, limit);
366
519
  },
@@ -420,11 +573,32 @@ export function createInteractiveViewer(container, options) {
420
573
  },
421
574
  setRenderOptions(options) {
422
575
  const sceneAffecting = hasSceneAffectingRenderOptions(options);
423
- renderOptions = mergeRenderOptions(renderOptions, options);
576
+ const previousRenderOptions = renderOptions;
577
+ const previousScene = scene;
578
+ const previousSpatialScene = spatialScene;
579
+ const nextRenderOptions = mergeRenderOptions(renderOptions, options);
580
+ let nextScene = scene;
424
581
  if (currentInput.kind !== "scene" && sceneAffecting) {
425
- scene = renderSceneFromInput(currentInput, renderOptions);
582
+ nextScene = renderSceneFromInput(currentInput, nextRenderOptions);
583
+ }
584
+ const nextSpatialScene = nextRenderOptions.viewMode === "3d"
585
+ ? renderSpatialSceneFromInput(currentInput, nextRenderOptions, providedSpatialScene)
586
+ : null;
587
+ renderOptions = nextRenderOptions;
588
+ scene = nextScene;
589
+ spatialScene = nextSpatialScene;
590
+ syncAnimationFrozenState();
591
+ try {
592
+ rerenderScene(sceneAffecting);
593
+ }
594
+ catch (error) {
595
+ renderOptions = previousRenderOptions;
596
+ scene = previousScene;
597
+ spatialScene = previousSpatialScene;
598
+ syncAnimationFrozenState();
599
+ rerenderScene(sceneAffecting);
600
+ throw error;
426
601
  }
427
- rerenderScene(sceneAffecting);
428
602
  },
429
603
  getState() {
430
604
  return { ...state };
@@ -442,11 +616,15 @@ export function createInteractiveViewer(container, options) {
442
616
  updateState(rotateViewerState(state, deg));
443
617
  },
444
618
  fitToSystem() {
445
- updateState(fitViewerState(scene, state, constraints));
619
+ updateState(is3DView()
620
+ ? { ...DEFAULT_VIEWER_STATE, selectedObjectId: state.selectedObjectId }
621
+ : fitViewerState(scene, state, constraints));
446
622
  },
447
623
  focusObject(id) {
448
624
  activeViewpointId = null;
449
- updateState(focusViewerState(scene, state, id, constraints));
625
+ updateState(is3DView()
626
+ ? create3DFocusState(id)
627
+ : focusViewerState(scene, state, id, constraints));
450
628
  applySelection(id);
451
629
  if (behavior.tooltipMode === "pinned") {
452
630
  pinnedTooltipObjectId = getObjectById(id)?.objectId ?? null;
@@ -458,7 +636,9 @@ export function createInteractiveViewer(container, options) {
458
636
  updateTooltip();
459
637
  },
460
638
  resetView() {
461
- const resetState = fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints);
639
+ const resetState = is3DView()
640
+ ? { ...DEFAULT_VIEWER_STATE }
641
+ : fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints);
462
642
  activeViewpointId = null;
463
643
  updateState(resetState);
464
644
  applySelection(null);
@@ -488,6 +668,9 @@ export function createInteractiveViewer(container, options) {
488
668
  container.removeEventListener("focusin", handleFocusIn);
489
669
  container.removeEventListener("focusout", handleFocusOut);
490
670
  container.removeEventListener("keydown", handleKeyDown);
671
+ stopAnimationLoop();
672
+ runtime3d?.destroy();
673
+ runtime3d = null;
491
674
  tooltipRoot?.remove();
492
675
  tooltipRoot = null;
493
676
  minimapRoot?.remove();
@@ -515,15 +698,26 @@ export function createInteractiveViewer(container, options) {
515
698
  }
516
699
  return api;
517
700
  function rerenderScene(resetView) {
518
- container.innerHTML = renderSceneToSvg(scene, {
519
- ...renderOptions,
520
- filter: renderOptions.filter ?? null,
521
- selectedObjectId: state.selectedObjectId,
522
- });
523
- svgElement = container.querySelector('[data-worldorbit-svg="true"]');
524
- cameraRoot = container.querySelector("#worldorbit-camera-root");
701
+ runtime3d?.destroy();
702
+ runtime3d = null;
703
+ container.innerHTML = "";
704
+ svgElement = null;
705
+ cameraRoot = null;
525
706
  minimapRoot = null;
526
707
  tooltipRoot = null;
708
+ if (is3DView()) {
709
+ spatialScene = spatialScene ?? renderSpatialSceneFromInput(currentInput, renderOptions, providedSpatialScene);
710
+ runtime3d = createViewer3DRuntime(container);
711
+ }
712
+ else {
713
+ container.innerHTML = renderSceneToSvg(scene, {
714
+ ...renderOptions,
715
+ filter: renderOptions.filter ?? null,
716
+ selectedObjectId: state.selectedObjectId,
717
+ });
718
+ svgElement = container.querySelector('[data-worldorbit-svg="true"]');
719
+ cameraRoot = container.querySelector("#worldorbit-camera-root");
720
+ }
527
721
  if (behavior.minimap) {
528
722
  minimapRoot = document.createElement("div");
529
723
  minimapRoot.dataset.worldorbitMinimapRoot = "true";
@@ -537,11 +731,13 @@ export function createInteractiveViewer(container, options) {
537
731
  tooltipRoot.addEventListener("click", handleTooltipClick);
538
732
  container.append(tooltipRoot);
539
733
  }
540
- if (!svgElement || !cameraRoot) {
734
+ if (!is3DView() && (!svgElement || !cameraRoot)) {
541
735
  throw new Error("Interactive viewer could not locate the rendered SVG camera root.");
542
736
  }
543
737
  state = resetView
544
- ? fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints)
738
+ ? is3DView()
739
+ ? { ...DEFAULT_VIEWER_STATE }
740
+ : fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints)
545
741
  : sanitizeState(state);
546
742
  applySelection(state.selectedObjectId &&
547
743
  getObjectById(state.selectedObjectId)
@@ -555,7 +751,7 @@ export function createInteractiveViewer(container, options) {
555
751
  pinnedTooltipObjectId && getObjectById(pinnedTooltipObjectId)
556
752
  ? pinnedTooltipObjectId
557
753
  : null;
558
- updateCameraTransform();
754
+ syncRuntimePresentation();
559
755
  notifyFilterChange();
560
756
  notifyViewpointChange();
561
757
  options.onViewChange?.({ ...state });
@@ -563,7 +759,7 @@ export function createInteractiveViewer(container, options) {
563
759
  }
564
760
  function updateState(nextState) {
565
761
  state = sanitizeState(nextState);
566
- updateCameraTransform();
762
+ syncRuntimePresentation();
567
763
  options.onViewChange?.({ ...state });
568
764
  emitAtlasStateChange();
569
765
  }
@@ -579,6 +775,10 @@ export function createInteractiveViewer(container, options) {
579
775
  };
580
776
  }
581
777
  function updateCameraTransform() {
778
+ if (is3DView()) {
779
+ sync3DView();
780
+ return;
781
+ }
582
782
  if (!cameraRoot) {
583
783
  return;
584
784
  }
@@ -587,7 +787,7 @@ export function createInteractiveViewer(container, options) {
587
787
  updateTooltip();
588
788
  }
589
789
  function applySelection(objectId, emitCallback = true) {
590
- if (state.selectedObjectId) {
790
+ if (!is3DView() && state.selectedObjectId) {
591
791
  container
592
792
  .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
593
793
  ?.classList.remove("wo-object-selected");
@@ -598,7 +798,7 @@ export function createInteractiveViewer(container, options) {
598
798
  ? objectId
599
799
  : null,
600
800
  };
601
- if (state.selectedObjectId) {
801
+ if (!is3DView() && state.selectedObjectId) {
602
802
  container
603
803
  .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
604
804
  ?.classList.add("wo-object-selected");
@@ -671,6 +871,10 @@ export function createInteractiveViewer(container, options) {
671
871
  };
672
872
  }
673
873
  function syncAtlasHighlights() {
874
+ if (is3DView()) {
875
+ sync3DView();
876
+ return;
877
+ }
674
878
  for (const element of container.querySelectorAll(".wo-chain-selected, .wo-chain-hover, .wo-ancestor-selected, .wo-ancestor-hover, .wo-orbit-related-selected, .wo-orbit-related-hover")) {
675
879
  element.classList.remove("wo-chain-selected", "wo-chain-hover", "wo-ancestor-selected", "wo-ancestor-hover", "wo-orbit-related-selected", "wo-orbit-related-hover");
676
880
  }
@@ -712,6 +916,19 @@ export function createInteractiveViewer(container, options) {
712
916
  }
713
917
  }
714
918
  function getViewportPointFromClient(clientX, clientY) {
919
+ if (is3DView()) {
920
+ const rect = container.getBoundingClientRect();
921
+ if (!rect.width || !rect.height) {
922
+ return {
923
+ x: scene.width / 2,
924
+ y: scene.height / 2,
925
+ };
926
+ }
927
+ return {
928
+ x: clientX - rect.left,
929
+ y: clientY - rect.top,
930
+ };
931
+ }
715
932
  if (!svgElement) {
716
933
  return {
717
934
  x: scene.width / 2,
@@ -757,6 +974,19 @@ export function createInteractiveViewer(container, options) {
757
974
  const scale = viewpoint.scale !== null && viewpoint.scale !== undefined
758
975
  ? clampValue(viewpoint.scale, constraints.minScale, constraints.maxScale)
759
976
  : null;
977
+ if (is3DView()) {
978
+ const focusId = viewpoint.objectId ?? viewpoint.selectedObjectId ?? null;
979
+ const target = focusId
980
+ ? spatialScene?.focusTargets.find((entry) => entry.objectId === focusId)
981
+ : null;
982
+ return {
983
+ scale: scale ?? 1.6,
984
+ rotationDeg,
985
+ translateX: target ? -target.center.x : 0,
986
+ translateY: target ? -target.center.z : 0,
987
+ selectedObjectId: viewpoint.selectedObjectId ?? viewpoint.objectId ?? null,
988
+ };
989
+ }
760
990
  const targetObject = viewpoint.objectId &&
761
991
  scene.objects.find((object) => object.objectId === viewpoint.objectId && !object.hidden);
762
992
  if (targetObject) {
@@ -901,32 +1131,24 @@ export function createInteractiveViewer(container, options) {
901
1131
  }
902
1132
  }
903
1133
  function positionTooltip(element, renderObject) {
904
- if (!svgElement) {
1134
+ const point = is3DView()
1135
+ ? runtime3d?.projectObjectToContainer(renderObject.objectId) ?? null
1136
+ : project2DTooltipPoint(renderObject);
1137
+ if (!point) {
905
1138
  return;
906
1139
  }
907
- const anchor = {
908
- x: renderObject.anchorX ?? renderObject.x,
909
- y: renderObject.anchorY ??
910
- renderObject.y - Math.max(renderObject.visualRadius, renderObject.radius),
911
- };
912
- const viewportPoint = projectWorldPoint(anchor);
913
- const svgRect = svgElement.getBoundingClientRect();
914
- const containerRect = container.getBoundingClientRect();
915
- const pointX = svgRect.left -
916
- containerRect.left +
917
- (viewportPoint.x / Math.max(scene.width, 1)) * svgRect.width;
918
- const pointY = svgRect.top -
919
- containerRect.top +
920
- (viewportPoint.y / Math.max(scene.height, 1)) * svgRect.height;
921
1140
  const maxLeft = Math.max(container.clientWidth - element.offsetWidth - 12, 12);
922
1141
  const maxTop = Math.max(container.clientHeight - element.offsetHeight - 12, 12);
923
- const preferAbove = pointY > container.clientHeight * 0.48;
924
- const nextLeft = clampValue(pointX + 18, 12, maxLeft);
925
- const nextTop = clampValue(preferAbove ? pointY - element.offsetHeight - 18 : pointY + 18, 12, maxTop);
1142
+ const preferAbove = point.y > container.clientHeight * 0.48;
1143
+ const nextLeft = clampValue(point.x + 18, 12, maxLeft);
1144
+ const nextTop = clampValue(preferAbove ? point.y - element.offsetHeight - 18 : point.y + 18, 12, maxTop);
926
1145
  element.style.left = `${nextLeft}px`;
927
1146
  element.style.top = `${nextTop}px`;
928
1147
  }
929
1148
  function projectWorldPoint(point) {
1149
+ if (is3DView()) {
1150
+ return point;
1151
+ }
930
1152
  const center = {
931
1153
  x: scene.width / 2,
932
1154
  y: scene.height / 2,
@@ -937,6 +1159,27 @@ export function createInteractiveViewer(container, options) {
937
1159
  y: center.y + (rotated.y - center.y) * state.scale + state.translateY,
938
1160
  };
939
1161
  }
1162
+ function project2DTooltipPoint(renderObject) {
1163
+ if (!svgElement) {
1164
+ return null;
1165
+ }
1166
+ const anchor = {
1167
+ x: renderObject.anchorX ?? renderObject.x,
1168
+ y: renderObject.anchorY ??
1169
+ renderObject.y - Math.max(renderObject.visualRadius, renderObject.radius),
1170
+ };
1171
+ const viewportPoint = projectWorldPoint(anchor);
1172
+ const svgRect = svgElement.getBoundingClientRect();
1173
+ const containerRect = container.getBoundingClientRect();
1174
+ return {
1175
+ x: svgRect.left -
1176
+ containerRect.left +
1177
+ (viewportPoint.x / Math.max(scene.width, 1)) * svgRect.width,
1178
+ y: svgRect.top -
1179
+ containerRect.top +
1180
+ (viewportPoint.y / Math.max(scene.height, 1)) * svgRect.height,
1181
+ };
1182
+ }
940
1183
  function handleTooltipClick(event) {
941
1184
  const target = event.target?.closest("[data-tooltip-action]");
942
1185
  if (!target) {
@@ -964,6 +1207,92 @@ export function createInteractiveViewer(container, options) {
964
1207
  options.onTooltipChange?.(details);
965
1208
  }
966
1209
  }
1210
+ function is3DView() {
1211
+ return renderOptions.viewMode === "3d";
1212
+ }
1213
+ function syncAnimationFrozenState() {
1214
+ animationState = {
1215
+ ...animationState,
1216
+ frozenByEvent: spatialScene?.timeFrozen ?? false,
1217
+ };
1218
+ if (animationState.frozenByEvent) {
1219
+ animationState = {
1220
+ ...animationState,
1221
+ playing: false,
1222
+ };
1223
+ stopAnimationLoop();
1224
+ }
1225
+ }
1226
+ function ensureAnimationFrame() {
1227
+ if (animationFrameId !== null || !animationState.playing || destroyed) {
1228
+ return;
1229
+ }
1230
+ animationFrameId = window.requestAnimationFrame(renderAnimationFrame);
1231
+ }
1232
+ function stopAnimationLoop() {
1233
+ if (animationFrameId !== null) {
1234
+ window.cancelAnimationFrame(animationFrameId);
1235
+ animationFrameId = null;
1236
+ }
1237
+ lastAnimationTimestamp = null;
1238
+ }
1239
+ function renderAnimationFrame(timestamp) {
1240
+ animationFrameId = null;
1241
+ if (!animationState.playing || destroyed) {
1242
+ lastAnimationTimestamp = null;
1243
+ return;
1244
+ }
1245
+ const previousTimestamp = lastAnimationTimestamp ?? timestamp;
1246
+ const deltaSeconds = Math.max((timestamp - previousTimestamp) / 1_000, 0);
1247
+ lastAnimationTimestamp = timestamp;
1248
+ animationState = {
1249
+ ...animationState,
1250
+ timeSeconds: animationState.timeSeconds + deltaSeconds * animationState.speed,
1251
+ };
1252
+ sync3DView();
1253
+ ensureAnimationFrame();
1254
+ }
1255
+ function syncRuntimePresentation() {
1256
+ updateCameraTransform();
1257
+ if (is3DView() && animationState.playing) {
1258
+ ensureAnimationFrame();
1259
+ }
1260
+ else if (!animationState.playing || !is3DView()) {
1261
+ stopAnimationLoop();
1262
+ }
1263
+ }
1264
+ function sync3DView() {
1265
+ if (!is3DView() || !runtime3d || !spatialScene) {
1266
+ return;
1267
+ }
1268
+ runtime3d.update({
1269
+ spatialScene,
1270
+ renderOptions,
1271
+ visibleObjectIds: getVisibleObjectIds(),
1272
+ selectedObjectId: state.selectedObjectId,
1273
+ hoveredObjectId,
1274
+ state,
1275
+ timeSeconds: animationState.timeSeconds,
1276
+ });
1277
+ updateMinimap();
1278
+ updateTooltip();
1279
+ }
1280
+ function create3DFocusState(objectId) {
1281
+ const target = spatialScene?.focusTargets.find((entry) => entry.objectId === objectId);
1282
+ if (!target) {
1283
+ return {
1284
+ ...DEFAULT_VIEWER_STATE,
1285
+ selectedObjectId: objectId,
1286
+ };
1287
+ }
1288
+ return {
1289
+ scale: 1.8,
1290
+ rotationDeg: state.rotationDeg,
1291
+ translateX: -target.center.x,
1292
+ translateY: -target.center.z,
1293
+ selectedObjectId: objectId,
1294
+ };
1295
+ }
967
1296
  }
968
1297
  function resolveInitialInput(options) {
969
1298
  if (options.scene) {
@@ -989,9 +1318,25 @@ function renderSceneFromInput(input, renderOptions) {
989
1318
  }
990
1319
  }
991
1320
  }
1321
+ function renderSpatialSceneFromInput(input, renderOptions, providedSpatialScene) {
1322
+ if (providedSpatialScene) {
1323
+ return providedSpatialScene;
1324
+ }
1325
+ switch (input.kind) {
1326
+ case "scene":
1327
+ return fallbackSpatialSceneFromRenderScene(input.value);
1328
+ case "document":
1329
+ return renderDocumentToSpatialScene(input.value, renderOptions);
1330
+ case "source": {
1331
+ const loaded = loadWorldOrbitSource(input.value);
1332
+ return renderDocumentToSpatialScene(loaded.document, resolveSourceRenderOptions(loaded, renderOptions));
1333
+ }
1334
+ }
1335
+ }
992
1336
  function cloneRenderOptions(renderOptions) {
993
1337
  return {
994
1338
  ...renderOptions,
1339
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
995
1340
  filter: renderOptions.filter ? { ...renderOptions.filter } : undefined,
996
1341
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : undefined,
997
1342
  layers: renderOptions.layers ? { ...renderOptions.layers } : undefined,
@@ -999,12 +1344,20 @@ function cloneRenderOptions(renderOptions) {
999
1344
  ? { ...renderOptions.theme }
1000
1345
  : renderOptions.theme,
1001
1346
  activeEventId: renderOptions.activeEventId ?? null,
1347
+ viewMode: renderOptions.viewMode ?? "2d",
1002
1348
  };
1003
1349
  }
1004
1350
  function mergeRenderOptions(current, next) {
1005
1351
  return {
1006
1352
  ...current,
1007
1353
  ...next,
1354
+ camera: next.camera !== undefined
1355
+ ? next.camera
1356
+ ? { ...next.camera }
1357
+ : null
1358
+ : current.camera
1359
+ ? { ...current.camera }
1360
+ : null,
1008
1361
  filter: next.filter !== undefined
1009
1362
  ? normalizeViewerFilter(next.filter)
1010
1363
  : current.filter
@@ -1029,6 +1382,7 @@ function mergeRenderOptions(current, next) {
1029
1382
  theme: next.theme && typeof next.theme === "object"
1030
1383
  ? { ...next.theme }
1031
1384
  : next.theme ?? current.theme,
1385
+ viewMode: next.viewMode ?? current.viewMode ?? "2d",
1032
1386
  };
1033
1387
  }
1034
1388
  function hasSceneAffectingRenderOptions(options) {
@@ -1037,6 +1391,7 @@ function hasSceneAffectingRenderOptions(options) {
1037
1391
  options.padding !== undefined ||
1038
1392
  options.preset !== undefined ||
1039
1393
  options.projection !== undefined ||
1394
+ options.camera !== undefined ||
1040
1395
  options.scaleModel !== undefined ||
1041
1396
  options.activeEventId !== undefined);
1042
1397
  }
@@ -1050,6 +1405,99 @@ function resolveSourceRenderOptions(loaded, renderOptions) {
1050
1405
  preset: atlasDocument.system.defaults.preset,
1051
1406
  };
1052
1407
  }
1408
+ function fallbackSpatialSceneFromRenderScene(scene) {
1409
+ return {
1410
+ width: scene.width,
1411
+ height: scene.height,
1412
+ padding: scene.padding,
1413
+ renderPreset: scene.renderPreset,
1414
+ projection: scene.projection,
1415
+ camera: scene.camera,
1416
+ scaleModel: {
1417
+ orbitDistanceMultiplier: 1,
1418
+ bodyRadiusMultiplier: 1,
1419
+ markerSizeMultiplier: 1,
1420
+ ringThicknessMultiplier: 1,
1421
+ focusPadding: 12,
1422
+ minBodyRadius: 4,
1423
+ maxBodyRadius: 40,
1424
+ },
1425
+ title: scene.title,
1426
+ subtitle: scene.subtitle,
1427
+ systemId: scene.systemId,
1428
+ viewMode: "3d",
1429
+ layoutPreset: scene.layoutPreset,
1430
+ metadata: {
1431
+ ...scene.metadata,
1432
+ "viewer.mode": "3d-fallback",
1433
+ },
1434
+ contentBounds: {
1435
+ minX: scene.contentBounds.minX - scene.contentBounds.centerX,
1436
+ minY: -40,
1437
+ minZ: scene.contentBounds.minY - scene.contentBounds.centerY,
1438
+ maxX: scene.contentBounds.maxX - scene.contentBounds.centerX,
1439
+ maxY: 40,
1440
+ maxZ: scene.contentBounds.maxY - scene.contentBounds.centerY,
1441
+ width: scene.contentBounds.width,
1442
+ height: 80,
1443
+ depth: scene.contentBounds.height,
1444
+ center: { x: 0, y: 0, z: 0 },
1445
+ },
1446
+ semanticGroups: scene.semanticGroups,
1447
+ viewpoints: scene.viewpoints,
1448
+ activeEventId: scene.activeEventId,
1449
+ timeFrozen: scene.activeEventId !== null,
1450
+ objects: scene.objects.map((object) => ({
1451
+ objectId: object.objectId,
1452
+ object: object.object,
1453
+ parentId: object.parentId,
1454
+ ancestorIds: object.ancestorIds.slice(),
1455
+ childIds: object.childIds.slice(),
1456
+ groupId: object.groupId,
1457
+ semanticGroupIds: object.semanticGroupIds.slice(),
1458
+ position: {
1459
+ x: object.x - scene.contentBounds.centerX,
1460
+ y: 0,
1461
+ z: object.y - scene.contentBounds.centerY,
1462
+ },
1463
+ radius: object.radius,
1464
+ visualRadius: object.visualRadius,
1465
+ label: object.label,
1466
+ secondaryLabel: object.secondaryLabel,
1467
+ fillColor: object.fillColor,
1468
+ imageHref: object.imageHref,
1469
+ hidden: object.hidden,
1470
+ motion: null,
1471
+ })),
1472
+ orbits: scene.orbitVisuals.map((orbit) => ({
1473
+ objectId: orbit.objectId,
1474
+ object: orbit.object,
1475
+ parentId: orbit.parentId,
1476
+ groupId: orbit.groupId,
1477
+ semanticGroupIds: orbit.semanticGroupIds.slice(),
1478
+ center: { x: 0, y: 0, z: 0 },
1479
+ kind: orbit.kind,
1480
+ radius: orbit.radius,
1481
+ semiMajor: orbit.radius ?? orbit.rx ?? 0,
1482
+ semiMinor: orbit.radius ?? orbit.ry ?? 0,
1483
+ rotationDeg: orbit.rotationDeg,
1484
+ inclinationDeg: 0,
1485
+ band: orbit.band,
1486
+ bandThickness: orbit.bandThickness,
1487
+ hidden: orbit.hidden,
1488
+ motion: null,
1489
+ })),
1490
+ focusTargets: scene.objects.map((object) => ({
1491
+ objectId: object.objectId,
1492
+ center: {
1493
+ x: object.x - scene.contentBounds.centerX,
1494
+ y: 0,
1495
+ z: object.y - scene.contentBounds.centerY,
1496
+ },
1497
+ radius: object.visualRadius + 12,
1498
+ })),
1499
+ };
1500
+ }
1053
1501
  function createTouchGestureState(scene, state, touchPoints) {
1054
1502
  const { center, distance } = getTouchCenterAndDistance(touchPoints);
1055
1503
  return {
@@ -1116,6 +1564,34 @@ function installViewerTooltipStyles() {
1116
1564
  const style = document.createElement("style");
1117
1565
  style.id = TOOLTIP_STYLE_ID;
1118
1566
  style.textContent = `
1567
+ .wo-viewer-3d-root {
1568
+ position: relative;
1569
+ min-height: 320px;
1570
+ width: 100%;
1571
+ border-radius: 22px;
1572
+ overflow: hidden;
1573
+ background:
1574
+ radial-gradient(circle at top left, rgba(240, 180, 100, 0.08), transparent 24%),
1575
+ linear-gradient(180deg, rgba(255,255,255,0.02), transparent);
1576
+ }
1577
+ .wo-viewer-3d-loading {
1578
+ display: grid;
1579
+ place-items: center;
1580
+ min-height: 320px;
1581
+ padding: 24px;
1582
+ color: rgba(237, 246, 255, 0.76);
1583
+ font: 600 14px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
1584
+ text-align: center;
1585
+ }
1586
+ .wo-viewer-3d-loading.is-error {
1587
+ color: #ffb2b2;
1588
+ }
1589
+ .wo-viewer-3d-canvas {
1590
+ display: block;
1591
+ width: 100%;
1592
+ height: 100%;
1593
+ min-height: 320px;
1594
+ }
1119
1595
  .wo-viewer-tooltip-root {
1120
1596
  position: absolute;
1121
1597
  z-index: 12;