worldorbit 2.6.0 → 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 (196) hide show
  1. package/README.md +12 -5
  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 -6542
  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 -12250
  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 -6179
  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 -8334
  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 -6614
  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 -12275
  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 -6207
  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 -8391
  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 -904
  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 -9704
  173. package/dist/unpkg/worldorbit.min.js +2 -263
  174. package/package.json +1 -1
  175. package/packages/core/dist/index.d.ts +1 -0
  176. package/packages/core/dist/index.js +1 -0
  177. package/packages/core/dist/spatial-scene.d.ts +3 -0
  178. package/packages/core/dist/spatial-scene.js +420 -0
  179. package/packages/core/dist/types.d.ts +105 -0
  180. package/packages/editor/dist/editor.js +25 -4
  181. package/packages/editor/dist/types.d.ts +4 -0
  182. package/packages/markdown/dist/html.js +10 -3
  183. package/packages/viewer/dist/atlas-state.js +3 -0
  184. package/packages/viewer/dist/atlas-viewer.js +1 -0
  185. package/packages/viewer/dist/custom-element.js +18 -4
  186. package/packages/viewer/dist/embed.d.ts +5 -1
  187. package/packages/viewer/dist/embed.js +58 -24
  188. package/packages/viewer/dist/errors.d.ts +6 -0
  189. package/packages/viewer/dist/errors.js +12 -0
  190. package/packages/viewer/dist/index.d.ts +1 -0
  191. package/packages/viewer/dist/index.js +1 -0
  192. package/packages/viewer/dist/runtime-3d.d.ts +19 -0
  193. package/packages/viewer/dist/runtime-3d.js +494 -0
  194. package/packages/viewer/dist/types.d.ts +21 -2
  195. package/packages/viewer/dist/vendor/three.module.js +53032 -0
  196. package/packages/viewer/dist/viewer.js +501 -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,7 @@ 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",
46
48
  camera: options.camera ? { ...options.camera } : null,
47
49
  scaleModel: options.scaleModel ? { ...options.scaleModel } : undefined,
48
50
  theme: options.theme,
@@ -55,9 +57,14 @@ export function createInteractiveViewer(container, options) {
55
57
  const previousPosition = container.style.position;
56
58
  let currentInput = resolveInitialInput(options);
57
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;
58
64
  let state = { ...DEFAULT_VIEWER_STATE };
59
65
  let svgElement = null;
60
66
  let cameraRoot = null;
67
+ let runtime3d = null;
61
68
  let minimapRoot = null;
62
69
  let tooltipRoot = null;
63
70
  let suppressClick = false;
@@ -72,6 +79,14 @@ export function createInteractiveViewer(container, options) {
72
79
  let activeTooltipObjectId = null;
73
80
  let activeTooltipDetails = null;
74
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
+ };
75
90
  if (previousTabIndex === null) {
76
91
  container.tabIndex = 0;
77
92
  }
@@ -81,12 +96,18 @@ export function createInteractiveViewer(container, options) {
81
96
  if (!container.style.position) {
82
97
  container.style.position = "relative";
83
98
  }
99
+ syncAnimationFrozenState();
84
100
  const handleWheel = (event) => {
85
101
  if (!behavior.pointer || destroyed) {
86
102
  return;
87
103
  }
88
104
  event.preventDefault();
89
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
+ }
90
111
  const anchor = getWorldPointFromClient(event.clientX, event.clientY);
91
112
  const factor = clampValue(Math.exp(-event.deltaY * 0.002), 0.6, 1.6);
92
113
  updateState(zoomViewerStateAt(scene, state, factor, anchor, constraints));
@@ -99,7 +120,7 @@ export function createInteractiveViewer(container, options) {
99
120
  if ((isTouch && !behavior.touch) || (!isTouch && !behavior.pointer)) {
100
121
  return;
101
122
  }
102
- if (!isTouch && event.button !== 0) {
123
+ if (!isTouch && event.button !== 0 && !is3DView()) {
103
124
  return;
104
125
  }
105
126
  container.focus();
@@ -127,6 +148,9 @@ export function createInteractiveViewer(container, options) {
127
148
  }
128
149
  const isTouch = event.pointerType === "touch";
129
150
  if (isTouch) {
151
+ if (is3DView()) {
152
+ return;
153
+ }
130
154
  if (!behavior.touch || !touchPoints.has(event.pointerId)) {
131
155
  return;
132
156
  }
@@ -155,6 +179,29 @@ export function createInteractiveViewer(container, options) {
155
179
  }
156
180
  return;
157
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
+ }
158
205
  if (!behavior.pointer || activePointerId !== event.pointerId || !lastPointerPoint) {
159
206
  return;
160
207
  }
@@ -189,7 +236,9 @@ export function createInteractiveViewer(container, options) {
189
236
  suppressClick = false;
190
237
  return;
191
238
  }
192
- const objectId = getClosestObjectId(event.target);
239
+ const objectId = is3DView()
240
+ ? runtime3d?.hitTest(event.clientX, event.clientY) ?? null
241
+ : getClosestObjectId(event.target);
193
242
  applySelection(objectId);
194
243
  if (behavior.tooltipMode === "pinned") {
195
244
  pinnedTooltipObjectId = objectId;
@@ -197,6 +246,9 @@ export function createInteractiveViewer(container, options) {
197
246
  }
198
247
  };
199
248
  const handleMouseOver = (event) => {
249
+ if (is3DView()) {
250
+ return;
251
+ }
200
252
  const objectId = getClosestObjectId(event.target);
201
253
  applyHover(objectId);
202
254
  };
@@ -204,6 +256,9 @@ export function createInteractiveViewer(container, options) {
204
256
  applyHover(null);
205
257
  };
206
258
  const handleFocusIn = (event) => {
259
+ if (is3DView()) {
260
+ return;
261
+ }
207
262
  const objectId = getClosestObjectId(event.target);
208
263
  if (!objectId) {
209
264
  return;
@@ -217,7 +272,9 @@ export function createInteractiveViewer(container, options) {
217
272
  if (!behavior.keyboard || destroyed) {
218
273
  return;
219
274
  }
220
- const objectId = getClosestObjectId(event.target);
275
+ const objectId = is3DView()
276
+ ? state.selectedObjectId
277
+ : getClosestObjectId(event.target);
221
278
  if ((event.key === "Enter" || event.key === " ") && objectId) {
222
279
  event.preventDefault();
223
280
  applySelection(objectId);
@@ -294,18 +351,36 @@ export function createInteractiveViewer(container, options) {
294
351
  setSource(source) {
295
352
  currentInput = { kind: "source", value: source };
296
353
  scene = renderSceneFromInput(currentInput, renderOptions);
354
+ providedSpatialScene = null;
355
+ spatialScene =
356
+ renderOptions.viewMode === "3d"
357
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
358
+ : null;
359
+ syncAnimationFrozenState();
297
360
  activeViewpointId = null;
298
361
  rerenderScene(true);
299
362
  },
300
363
  setDocument(document) {
301
364
  currentInput = { kind: "document", value: document };
302
365
  scene = renderSceneFromInput(currentInput, renderOptions);
366
+ providedSpatialScene = null;
367
+ spatialScene =
368
+ renderOptions.viewMode === "3d"
369
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
370
+ : null;
371
+ syncAnimationFrozenState();
303
372
  activeViewpointId = null;
304
373
  rerenderScene(true);
305
374
  },
306
375
  setScene(nextScene) {
307
376
  currentInput = { kind: "scene", value: nextScene };
308
377
  scene = nextScene;
378
+ providedSpatialScene = null;
379
+ spatialScene =
380
+ renderOptions.viewMode === "3d"
381
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
382
+ : null;
383
+ syncAnimationFrozenState();
309
384
  activeViewpointId = null;
310
385
  rerenderScene(true);
311
386
  },
@@ -315,6 +390,30 @@ export function createInteractiveViewer(container, options) {
315
390
  getRenderOptions() {
316
391
  return cloneRenderOptions(renderOptions);
317
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
+ },
318
417
  listViewpoints() {
319
418
  return scene.viewpoints.slice();
320
419
  },
@@ -368,6 +467,53 @@ export function createInteractiveViewer(container, options) {
368
467
  setActiveEvent(id) {
369
468
  api.setRenderOptions({ activeEventId: id });
370
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
+ },
371
517
  search(query, limit = 12) {
372
518
  return searchSceneObjects(scene, query, limit);
373
519
  },
@@ -427,11 +573,32 @@ export function createInteractiveViewer(container, options) {
427
573
  },
428
574
  setRenderOptions(options) {
429
575
  const sceneAffecting = hasSceneAffectingRenderOptions(options);
430
- 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;
431
581
  if (currentInput.kind !== "scene" && sceneAffecting) {
432
- 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;
433
601
  }
434
- rerenderScene(sceneAffecting);
435
602
  },
436
603
  getState() {
437
604
  return { ...state };
@@ -449,11 +616,15 @@ export function createInteractiveViewer(container, options) {
449
616
  updateState(rotateViewerState(state, deg));
450
617
  },
451
618
  fitToSystem() {
452
- updateState(fitViewerState(scene, state, constraints));
619
+ updateState(is3DView()
620
+ ? { ...DEFAULT_VIEWER_STATE, selectedObjectId: state.selectedObjectId }
621
+ : fitViewerState(scene, state, constraints));
453
622
  },
454
623
  focusObject(id) {
455
624
  activeViewpointId = null;
456
- updateState(focusViewerState(scene, state, id, constraints));
625
+ updateState(is3DView()
626
+ ? create3DFocusState(id)
627
+ : focusViewerState(scene, state, id, constraints));
457
628
  applySelection(id);
458
629
  if (behavior.tooltipMode === "pinned") {
459
630
  pinnedTooltipObjectId = getObjectById(id)?.objectId ?? null;
@@ -465,7 +636,9 @@ export function createInteractiveViewer(container, options) {
465
636
  updateTooltip();
466
637
  },
467
638
  resetView() {
468
- const resetState = fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints);
639
+ const resetState = is3DView()
640
+ ? { ...DEFAULT_VIEWER_STATE }
641
+ : fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints);
469
642
  activeViewpointId = null;
470
643
  updateState(resetState);
471
644
  applySelection(null);
@@ -495,6 +668,9 @@ export function createInteractiveViewer(container, options) {
495
668
  container.removeEventListener("focusin", handleFocusIn);
496
669
  container.removeEventListener("focusout", handleFocusOut);
497
670
  container.removeEventListener("keydown", handleKeyDown);
671
+ stopAnimationLoop();
672
+ runtime3d?.destroy();
673
+ runtime3d = null;
498
674
  tooltipRoot?.remove();
499
675
  tooltipRoot = null;
500
676
  minimapRoot?.remove();
@@ -522,15 +698,26 @@ export function createInteractiveViewer(container, options) {
522
698
  }
523
699
  return api;
524
700
  function rerenderScene(resetView) {
525
- container.innerHTML = renderSceneToSvg(scene, {
526
- ...renderOptions,
527
- filter: renderOptions.filter ?? null,
528
- selectedObjectId: state.selectedObjectId,
529
- });
530
- svgElement = container.querySelector('[data-worldorbit-svg="true"]');
531
- cameraRoot = container.querySelector("#worldorbit-camera-root");
701
+ runtime3d?.destroy();
702
+ runtime3d = null;
703
+ container.innerHTML = "";
704
+ svgElement = null;
705
+ cameraRoot = null;
532
706
  minimapRoot = null;
533
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
+ }
534
721
  if (behavior.minimap) {
535
722
  minimapRoot = document.createElement("div");
536
723
  minimapRoot.dataset.worldorbitMinimapRoot = "true";
@@ -544,11 +731,13 @@ export function createInteractiveViewer(container, options) {
544
731
  tooltipRoot.addEventListener("click", handleTooltipClick);
545
732
  container.append(tooltipRoot);
546
733
  }
547
- if (!svgElement || !cameraRoot) {
734
+ if (!is3DView() && (!svgElement || !cameraRoot)) {
548
735
  throw new Error("Interactive viewer could not locate the rendered SVG camera root.");
549
736
  }
550
737
  state = resetView
551
- ? fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints)
738
+ ? is3DView()
739
+ ? { ...DEFAULT_VIEWER_STATE }
740
+ : fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints)
552
741
  : sanitizeState(state);
553
742
  applySelection(state.selectedObjectId &&
554
743
  getObjectById(state.selectedObjectId)
@@ -562,7 +751,7 @@ export function createInteractiveViewer(container, options) {
562
751
  pinnedTooltipObjectId && getObjectById(pinnedTooltipObjectId)
563
752
  ? pinnedTooltipObjectId
564
753
  : null;
565
- updateCameraTransform();
754
+ syncRuntimePresentation();
566
755
  notifyFilterChange();
567
756
  notifyViewpointChange();
568
757
  options.onViewChange?.({ ...state });
@@ -570,7 +759,7 @@ export function createInteractiveViewer(container, options) {
570
759
  }
571
760
  function updateState(nextState) {
572
761
  state = sanitizeState(nextState);
573
- updateCameraTransform();
762
+ syncRuntimePresentation();
574
763
  options.onViewChange?.({ ...state });
575
764
  emitAtlasStateChange();
576
765
  }
@@ -586,6 +775,10 @@ export function createInteractiveViewer(container, options) {
586
775
  };
587
776
  }
588
777
  function updateCameraTransform() {
778
+ if (is3DView()) {
779
+ sync3DView();
780
+ return;
781
+ }
589
782
  if (!cameraRoot) {
590
783
  return;
591
784
  }
@@ -594,7 +787,7 @@ export function createInteractiveViewer(container, options) {
594
787
  updateTooltip();
595
788
  }
596
789
  function applySelection(objectId, emitCallback = true) {
597
- if (state.selectedObjectId) {
790
+ if (!is3DView() && state.selectedObjectId) {
598
791
  container
599
792
  .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
600
793
  ?.classList.remove("wo-object-selected");
@@ -605,7 +798,7 @@ export function createInteractiveViewer(container, options) {
605
798
  ? objectId
606
799
  : null,
607
800
  };
608
- if (state.selectedObjectId) {
801
+ if (!is3DView() && state.selectedObjectId) {
609
802
  container
610
803
  .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
611
804
  ?.classList.add("wo-object-selected");
@@ -678,6 +871,10 @@ export function createInteractiveViewer(container, options) {
678
871
  };
679
872
  }
680
873
  function syncAtlasHighlights() {
874
+ if (is3DView()) {
875
+ sync3DView();
876
+ return;
877
+ }
681
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")) {
682
879
  element.classList.remove("wo-chain-selected", "wo-chain-hover", "wo-ancestor-selected", "wo-ancestor-hover", "wo-orbit-related-selected", "wo-orbit-related-hover");
683
880
  }
@@ -719,6 +916,19 @@ export function createInteractiveViewer(container, options) {
719
916
  }
720
917
  }
721
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
+ }
722
932
  if (!svgElement) {
723
933
  return {
724
934
  x: scene.width / 2,
@@ -764,6 +974,19 @@ export function createInteractiveViewer(container, options) {
764
974
  const scale = viewpoint.scale !== null && viewpoint.scale !== undefined
765
975
  ? clampValue(viewpoint.scale, constraints.minScale, constraints.maxScale)
766
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
+ }
767
990
  const targetObject = viewpoint.objectId &&
768
991
  scene.objects.find((object) => object.objectId === viewpoint.objectId && !object.hidden);
769
992
  if (targetObject) {
@@ -908,32 +1131,24 @@ export function createInteractiveViewer(container, options) {
908
1131
  }
909
1132
  }
910
1133
  function positionTooltip(element, renderObject) {
911
- if (!svgElement) {
1134
+ const point = is3DView()
1135
+ ? runtime3d?.projectObjectToContainer(renderObject.objectId) ?? null
1136
+ : project2DTooltipPoint(renderObject);
1137
+ if (!point) {
912
1138
  return;
913
1139
  }
914
- const anchor = {
915
- x: renderObject.anchorX ?? renderObject.x,
916
- y: renderObject.anchorY ??
917
- renderObject.y - Math.max(renderObject.visualRadius, renderObject.radius),
918
- };
919
- const viewportPoint = projectWorldPoint(anchor);
920
- const svgRect = svgElement.getBoundingClientRect();
921
- const containerRect = container.getBoundingClientRect();
922
- const pointX = svgRect.left -
923
- containerRect.left +
924
- (viewportPoint.x / Math.max(scene.width, 1)) * svgRect.width;
925
- const pointY = svgRect.top -
926
- containerRect.top +
927
- (viewportPoint.y / Math.max(scene.height, 1)) * svgRect.height;
928
1140
  const maxLeft = Math.max(container.clientWidth - element.offsetWidth - 12, 12);
929
1141
  const maxTop = Math.max(container.clientHeight - element.offsetHeight - 12, 12);
930
- const preferAbove = pointY > container.clientHeight * 0.48;
931
- const nextLeft = clampValue(pointX + 18, 12, maxLeft);
932
- 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);
933
1145
  element.style.left = `${nextLeft}px`;
934
1146
  element.style.top = `${nextTop}px`;
935
1147
  }
936
1148
  function projectWorldPoint(point) {
1149
+ if (is3DView()) {
1150
+ return point;
1151
+ }
937
1152
  const center = {
938
1153
  x: scene.width / 2,
939
1154
  y: scene.height / 2,
@@ -944,6 +1159,27 @@ export function createInteractiveViewer(container, options) {
944
1159
  y: center.y + (rotated.y - center.y) * state.scale + state.translateY,
945
1160
  };
946
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
+ }
947
1183
  function handleTooltipClick(event) {
948
1184
  const target = event.target?.closest("[data-tooltip-action]");
949
1185
  if (!target) {
@@ -971,6 +1207,92 @@ export function createInteractiveViewer(container, options) {
971
1207
  options.onTooltipChange?.(details);
972
1208
  }
973
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
+ }
974
1296
  }
975
1297
  function resolveInitialInput(options) {
976
1298
  if (options.scene) {
@@ -996,6 +1318,21 @@ function renderSceneFromInput(input, renderOptions) {
996
1318
  }
997
1319
  }
998
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
+ }
999
1336
  function cloneRenderOptions(renderOptions) {
1000
1337
  return {
1001
1338
  ...renderOptions,
@@ -1007,6 +1344,7 @@ function cloneRenderOptions(renderOptions) {
1007
1344
  ? { ...renderOptions.theme }
1008
1345
  : renderOptions.theme,
1009
1346
  activeEventId: renderOptions.activeEventId ?? null,
1347
+ viewMode: renderOptions.viewMode ?? "2d",
1010
1348
  };
1011
1349
  }
1012
1350
  function mergeRenderOptions(current, next) {
@@ -1044,6 +1382,7 @@ function mergeRenderOptions(current, next) {
1044
1382
  theme: next.theme && typeof next.theme === "object"
1045
1383
  ? { ...next.theme }
1046
1384
  : next.theme ?? current.theme,
1385
+ viewMode: next.viewMode ?? current.viewMode ?? "2d",
1047
1386
  };
1048
1387
  }
1049
1388
  function hasSceneAffectingRenderOptions(options) {
@@ -1066,6 +1405,99 @@ function resolveSourceRenderOptions(loaded, renderOptions) {
1066
1405
  preset: atlasDocument.system.defaults.preset,
1067
1406
  };
1068
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
+ }
1069
1501
  function createTouchGestureState(scene, state, touchPoints) {
1070
1502
  const { center, distance } = getTouchCenterAndDistance(touchPoints);
1071
1503
  return {
@@ -1132,6 +1564,34 @@ function installViewerTooltipStyles() {
1132
1564
  const style = document.createElement("style");
1133
1565
  style.id = TOOLTIP_STYLE_ID;
1134
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
+ }
1135
1595
  .wo-viewer-tooltip-root {
1136
1596
  position: absolute;
1137
1597
  z-index: 12;