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
@@ -0,0 +1,1662 @@
1
+ import { loadWorldOrbitSource, renderDocumentToScene, renderDocumentToSpatialScene, rotatePoint, } from "@worldorbit/core";
2
+ import { computeVisibleObjectIds, createAtlasStateSnapshot, createViewerBookmark, deserializeViewerAtlasState, normalizeViewerFilter, sceneViewpointToLayerOptions, searchSceneObjects, serializeViewerAtlasState, viewpointToViewerFilter, } from "./atlas-state.js";
3
+ import { renderViewerMinimap } from "./minimap.js";
4
+ import { renderSceneToSvg } from "./render.js";
5
+ import { createViewer3DRuntime, } from "./runtime-3d.js";
6
+ import { buildViewerTooltipDetails, renderDefaultTooltipContent, } from "./tooltip.js";
7
+ import { DEFAULT_VIEWER_STATE, composeViewerTransform, fitViewerState, focusViewerState, invertViewerPoint, panViewerState, rotateViewerState, zoomViewerStateAt, } from "./viewer-state.js";
8
+ const DEFAULT_VIEWER_LIMITS = {
9
+ minScale: 0.2,
10
+ maxScale: 8,
11
+ fitPadding: 48,
12
+ panStep: 40,
13
+ zoomStep: 1.2,
14
+ rotationStep: 15,
15
+ };
16
+ const TOOLTIP_STYLE_ID = "worldorbit-viewer-tooltip-style";
17
+ export function createInteractiveViewer(container, options) {
18
+ ensureBrowserEnvironment(container);
19
+ const inputCount = Number(Boolean(options.source)) +
20
+ Number(Boolean(options.document)) +
21
+ Number(Boolean(options.scene));
22
+ if (inputCount !== 1) {
23
+ throw new Error('Interactive viewer requires exactly one of "source", "document", or "scene".');
24
+ }
25
+ const constraints = {
26
+ minScale: options.minScale ?? DEFAULT_VIEWER_LIMITS.minScale,
27
+ maxScale: options.maxScale ?? DEFAULT_VIEWER_LIMITS.maxScale,
28
+ fitPadding: options.fitPadding ?? DEFAULT_VIEWER_LIMITS.fitPadding,
29
+ };
30
+ const behavior = {
31
+ keyboard: options.keyboard ?? true,
32
+ pointer: options.pointer ?? true,
33
+ touch: options.touch ?? true,
34
+ selection: options.selection ?? true,
35
+ tooltipMode: options.tooltipMode ?? "hover",
36
+ minimap: options.minimap ?? false,
37
+ panStep: options.panStep ?? DEFAULT_VIEWER_LIMITS.panStep,
38
+ zoomStep: options.zoomStep ?? DEFAULT_VIEWER_LIMITS.zoomStep,
39
+ rotationStep: options.rotationStep ?? DEFAULT_VIEWER_LIMITS.rotationStep,
40
+ };
41
+ let renderOptions = {
42
+ width: options.width,
43
+ height: options.height,
44
+ padding: options.padding,
45
+ preset: options.preset,
46
+ projection: options.projection,
47
+ viewMode: options.viewMode ?? "2d",
48
+ camera: options.camera ? { ...options.camera } : null,
49
+ scaleModel: options.scaleModel ? { ...options.scaleModel } : undefined,
50
+ theme: options.theme,
51
+ layers: options.layers,
52
+ filter: normalizeViewerFilter(options.initialFilter),
53
+ subtitle: options.subtitle,
54
+ };
55
+ const previousTabIndex = container.getAttribute("tabindex");
56
+ const previousTouchAction = container.style.touchAction;
57
+ const previousPosition = container.style.position;
58
+ let currentInput = resolveInitialInput(options);
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;
64
+ let state = { ...DEFAULT_VIEWER_STATE };
65
+ let svgElement = null;
66
+ let cameraRoot = null;
67
+ let runtime3d = null;
68
+ let minimapRoot = null;
69
+ let tooltipRoot = null;
70
+ let suppressClick = false;
71
+ let activePointerId = null;
72
+ let lastPointerPoint = null;
73
+ let dragDistance = 0;
74
+ let destroyed = false;
75
+ let touchPoints = new Map();
76
+ let touchGesture = null;
77
+ let hoveredObjectId = null;
78
+ let pinnedTooltipObjectId = null;
79
+ let activeTooltipObjectId = null;
80
+ let activeTooltipDetails = null;
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
+ };
90
+ if (previousTabIndex === null) {
91
+ container.tabIndex = 0;
92
+ }
93
+ installViewerTooltipStyles();
94
+ container.classList.add("wo-viewer-container");
95
+ container.style.touchAction = behavior.touch ? "none" : previousTouchAction;
96
+ if (!container.style.position) {
97
+ container.style.position = "relative";
98
+ }
99
+ syncAnimationFrozenState();
100
+ const handleWheel = (event) => {
101
+ if (!behavior.pointer || destroyed) {
102
+ return;
103
+ }
104
+ event.preventDefault();
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
+ }
111
+ const anchor = getWorldPointFromClient(event.clientX, event.clientY);
112
+ const factor = clampValue(Math.exp(-event.deltaY * 0.002), 0.6, 1.6);
113
+ updateState(zoomViewerStateAt(scene, state, factor, anchor, constraints));
114
+ };
115
+ const handlePointerDown = (event) => {
116
+ if (destroyed) {
117
+ return;
118
+ }
119
+ const isTouch = event.pointerType === "touch";
120
+ if ((isTouch && !behavior.touch) || (!isTouch && !behavior.pointer)) {
121
+ return;
122
+ }
123
+ if (!isTouch && event.button !== 0 && !is3DView()) {
124
+ return;
125
+ }
126
+ container.focus();
127
+ container.setPointerCapture?.(event.pointerId);
128
+ const point = getViewportPointFromClient(event.clientX, event.clientY);
129
+ if (isTouch) {
130
+ touchPoints.set(event.pointerId, point);
131
+ if (touchPoints.size === 2) {
132
+ touchGesture = createTouchGestureState(scene, state, touchPoints);
133
+ }
134
+ else if (touchPoints.size === 1) {
135
+ dragDistance = 0;
136
+ suppressClick = false;
137
+ }
138
+ return;
139
+ }
140
+ activePointerId = event.pointerId;
141
+ lastPointerPoint = point;
142
+ dragDistance = 0;
143
+ suppressClick = false;
144
+ };
145
+ const handlePointerMove = (event) => {
146
+ if (destroyed) {
147
+ return;
148
+ }
149
+ const isTouch = event.pointerType === "touch";
150
+ if (isTouch) {
151
+ if (is3DView()) {
152
+ return;
153
+ }
154
+ if (!behavior.touch || !touchPoints.has(event.pointerId)) {
155
+ return;
156
+ }
157
+ const prevPoint = touchPoints.get(event.pointerId);
158
+ const nextPoint = getViewportPointFromClient(event.clientX, event.clientY);
159
+ touchPoints.set(event.pointerId, nextPoint);
160
+ if (touchPoints.size === 2) {
161
+ if (!touchGesture) {
162
+ touchGesture = createTouchGestureState(scene, state, touchPoints);
163
+ }
164
+ const current = getTouchCenterAndDistance(touchPoints);
165
+ const factor = current.distance / Math.max(touchGesture.startDistance, 1);
166
+ const zoomedState = zoomViewerStateAt(scene, touchGesture.startState, factor, touchGesture.startCenter, constraints);
167
+ const deltaX = current.center.x - touchGesture.startViewportCenter.x;
168
+ const deltaY = current.center.y - touchGesture.startViewportCenter.y;
169
+ updateState(panViewerState(zoomedState, deltaX, deltaY));
170
+ }
171
+ else if (touchPoints.size === 1) {
172
+ const deltaX = nextPoint.x - prevPoint.x;
173
+ const deltaY = nextPoint.y - prevPoint.y;
174
+ dragDistance += Math.abs(deltaX) + Math.abs(deltaY);
175
+ if (dragDistance > 2) {
176
+ suppressClick = true;
177
+ }
178
+ updateState(panViewerState(state, deltaX, deltaY));
179
+ }
180
+ return;
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
+ }
205
+ if (!behavior.pointer || activePointerId !== event.pointerId || !lastPointerPoint) {
206
+ return;
207
+ }
208
+ const nextPoint = getViewportPointFromClient(event.clientX, event.clientY);
209
+ const deltaX = nextPoint.x - lastPointerPoint.x;
210
+ const deltaY = nextPoint.y - lastPointerPoint.y;
211
+ dragDistance += Math.abs(deltaX) + Math.abs(deltaY);
212
+ lastPointerPoint = nextPoint;
213
+ if (dragDistance > 2) {
214
+ suppressClick = true;
215
+ }
216
+ updateState(panViewerState(state, deltaX, deltaY));
217
+ };
218
+ const handlePointerEnd = (event) => {
219
+ if (event.pointerType === "touch") {
220
+ touchPoints.delete(event.pointerId);
221
+ if (touchPoints.size < 2) {
222
+ touchGesture = null;
223
+ }
224
+ return;
225
+ }
226
+ if (activePointerId === event.pointerId) {
227
+ activePointerId = null;
228
+ lastPointerPoint = null;
229
+ }
230
+ };
231
+ const handleClick = (event) => {
232
+ if (!behavior.selection || destroyed) {
233
+ return;
234
+ }
235
+ if (suppressClick) {
236
+ suppressClick = false;
237
+ return;
238
+ }
239
+ const objectId = is3DView()
240
+ ? runtime3d?.hitTest(event.clientX, event.clientY) ?? null
241
+ : getClosestObjectId(event.target);
242
+ applySelection(objectId);
243
+ if (behavior.tooltipMode === "pinned") {
244
+ pinnedTooltipObjectId = objectId;
245
+ updateTooltip();
246
+ }
247
+ };
248
+ const handleMouseOver = (event) => {
249
+ if (is3DView()) {
250
+ return;
251
+ }
252
+ const objectId = getClosestObjectId(event.target);
253
+ applyHover(objectId);
254
+ };
255
+ const handleMouseLeave = () => {
256
+ applyHover(null);
257
+ };
258
+ const handleFocusIn = (event) => {
259
+ if (is3DView()) {
260
+ return;
261
+ }
262
+ const objectId = getClosestObjectId(event.target);
263
+ if (!objectId) {
264
+ return;
265
+ }
266
+ applyHover(objectId);
267
+ };
268
+ const handleFocusOut = () => {
269
+ applyHover(null);
270
+ };
271
+ const handleKeyDown = (event) => {
272
+ if (!behavior.keyboard || destroyed) {
273
+ return;
274
+ }
275
+ const objectId = is3DView()
276
+ ? state.selectedObjectId
277
+ : getClosestObjectId(event.target);
278
+ if ((event.key === "Enter" || event.key === " ") && objectId) {
279
+ event.preventDefault();
280
+ applySelection(objectId);
281
+ if (behavior.tooltipMode === "pinned") {
282
+ pinnedTooltipObjectId = objectId;
283
+ updateTooltip();
284
+ }
285
+ return;
286
+ }
287
+ switch (event.key) {
288
+ case "Escape":
289
+ if (behavior.tooltipMode === "pinned" && pinnedTooltipObjectId) {
290
+ event.preventDefault();
291
+ pinnedTooltipObjectId = null;
292
+ updateTooltip();
293
+ }
294
+ return;
295
+ case "+":
296
+ case "=":
297
+ event.preventDefault();
298
+ api.zoomBy(behavior.zoomStep);
299
+ return;
300
+ case "-":
301
+ event.preventDefault();
302
+ api.zoomBy(1 / behavior.zoomStep);
303
+ return;
304
+ case "ArrowLeft":
305
+ event.preventDefault();
306
+ api.panBy(-behavior.panStep, 0);
307
+ return;
308
+ case "ArrowRight":
309
+ event.preventDefault();
310
+ api.panBy(behavior.panStep, 0);
311
+ return;
312
+ case "ArrowUp":
313
+ event.preventDefault();
314
+ api.panBy(0, -behavior.panStep);
315
+ return;
316
+ case "ArrowDown":
317
+ event.preventDefault();
318
+ api.panBy(0, behavior.panStep);
319
+ return;
320
+ case "[":
321
+ event.preventDefault();
322
+ api.rotateBy(-behavior.rotationStep);
323
+ return;
324
+ case "]":
325
+ event.preventDefault();
326
+ api.rotateBy(behavior.rotationStep);
327
+ return;
328
+ case "f":
329
+ case "F":
330
+ event.preventDefault();
331
+ api.fitToSystem();
332
+ return;
333
+ case "0":
334
+ event.preventDefault();
335
+ api.resetView();
336
+ return;
337
+ }
338
+ };
339
+ container.addEventListener("wheel", handleWheel, { passive: false });
340
+ container.addEventListener("pointerdown", handlePointerDown);
341
+ container.addEventListener("pointermove", handlePointerMove);
342
+ container.addEventListener("pointerup", handlePointerEnd);
343
+ container.addEventListener("pointercancel", handlePointerEnd);
344
+ container.addEventListener("click", handleClick);
345
+ container.addEventListener("mouseover", handleMouseOver);
346
+ container.addEventListener("mouseleave", handleMouseLeave);
347
+ container.addEventListener("focusin", handleFocusIn);
348
+ container.addEventListener("focusout", handleFocusOut);
349
+ container.addEventListener("keydown", handleKeyDown);
350
+ const api = {
351
+ setSource(source) {
352
+ currentInput = { kind: "source", value: source };
353
+ scene = renderSceneFromInput(currentInput, renderOptions);
354
+ providedSpatialScene = null;
355
+ spatialScene =
356
+ renderOptions.viewMode === "3d"
357
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
358
+ : null;
359
+ syncAnimationFrozenState();
360
+ activeViewpointId = null;
361
+ rerenderScene(true);
362
+ },
363
+ setDocument(document) {
364
+ currentInput = { kind: "document", value: document };
365
+ scene = renderSceneFromInput(currentInput, renderOptions);
366
+ providedSpatialScene = null;
367
+ spatialScene =
368
+ renderOptions.viewMode === "3d"
369
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
370
+ : null;
371
+ syncAnimationFrozenState();
372
+ activeViewpointId = null;
373
+ rerenderScene(true);
374
+ },
375
+ setScene(nextScene) {
376
+ currentInput = { kind: "scene", value: nextScene };
377
+ scene = nextScene;
378
+ providedSpatialScene = null;
379
+ spatialScene =
380
+ renderOptions.viewMode === "3d"
381
+ ? renderSpatialSceneFromInput(currentInput, renderOptions, null)
382
+ : null;
383
+ syncAnimationFrozenState();
384
+ activeViewpointId = null;
385
+ rerenderScene(true);
386
+ },
387
+ getScene() {
388
+ return scene;
389
+ },
390
+ getRenderOptions() {
391
+ return cloneRenderOptions(renderOptions);
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
+ },
417
+ listViewpoints() {
418
+ return scene.viewpoints.slice();
419
+ },
420
+ getActiveViewpoint() {
421
+ return getViewpointById(activeViewpointId);
422
+ },
423
+ goToViewpoint(id) {
424
+ const viewpoint = getViewpointById(id);
425
+ if (!viewpoint) {
426
+ return false;
427
+ }
428
+ const nextRenderOptions = {};
429
+ const viewpointLayers = sceneViewpointToLayerOptions(viewpoint);
430
+ if (viewpoint.preset !== null) {
431
+ nextRenderOptions.preset = viewpoint.preset;
432
+ }
433
+ if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
434
+ nextRenderOptions.projection = viewpoint.projection;
435
+ }
436
+ if (viewpoint.camera) {
437
+ nextRenderOptions.camera = { ...viewpoint.camera };
438
+ }
439
+ else if (renderOptions.camera) {
440
+ nextRenderOptions.camera = null;
441
+ }
442
+ if (viewpointLayers) {
443
+ nextRenderOptions.layers = viewpointLayers;
444
+ }
445
+ activeViewpointId = viewpoint.id;
446
+ if (Object.keys(nextRenderOptions).length > 0) {
447
+ const sceneAffecting = hasSceneAffectingRenderOptions(nextRenderOptions);
448
+ renderOptions = mergeRenderOptions(renderOptions, nextRenderOptions);
449
+ if (currentInput.kind !== "scene" && sceneAffecting) {
450
+ scene = renderSceneFromInput(currentInput, renderOptions);
451
+ }
452
+ rerenderScene(sceneAffecting);
453
+ }
454
+ setFilterInternal(viewpointToViewerFilter(viewpoint), false, false);
455
+ const nextState = createViewpointState(viewpoint);
456
+ updateState(nextState);
457
+ applySelection(viewpoint.selectedObjectId ?? viewpoint.objectId ?? null, false);
458
+ options.onSelectionChange?.(getSelectedObject());
459
+ options.onSelectionDetailsChange?.(buildObjectDetails(state.selectedObjectId));
460
+ notifyViewpointChange();
461
+ emitAtlasStateChange();
462
+ return true;
463
+ },
464
+ getActiveEventId() {
465
+ return renderOptions.activeEventId ?? null;
466
+ },
467
+ setActiveEvent(id) {
468
+ api.setRenderOptions({ activeEventId: id });
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
+ },
517
+ search(query, limit = 12) {
518
+ return searchSceneObjects(scene, query, limit);
519
+ },
520
+ getFilter() {
521
+ return renderOptions.filter ? { ...renderOptions.filter } : null;
522
+ },
523
+ setFilter(filter) {
524
+ setFilterInternal(filter, true, true);
525
+ },
526
+ getVisibleObjects() {
527
+ return getVisibleSceneObjects();
528
+ },
529
+ getFocusPath(id) {
530
+ return buildFocusPath(id);
531
+ },
532
+ getObjectDetails(id) {
533
+ return buildObjectDetails(id);
534
+ },
535
+ getSelectionDetails() {
536
+ return buildObjectDetails(state.selectedObjectId);
537
+ },
538
+ getTooltipDetails() {
539
+ return activeTooltipDetails;
540
+ },
541
+ getAtlasState() {
542
+ return createAtlasStateSnapshot(state, renderOptions, renderOptions.filter ?? null, activeViewpointId);
543
+ },
544
+ setAtlasState(nextAtlasState) {
545
+ const atlasState = typeof nextAtlasState === "string"
546
+ ? deserializeViewerAtlasState(nextAtlasState)
547
+ : nextAtlasState;
548
+ if (atlasState.viewpointId) {
549
+ api.goToViewpoint(atlasState.viewpointId);
550
+ }
551
+ api.setRenderOptions(atlasState.renderOptions);
552
+ setFilterInternal(atlasState.filter ?? null, false, false);
553
+ updateState(sanitizeState({ ...state, ...atlasState.viewerState }));
554
+ applySelection(atlasState.viewerState.selectedObjectId ?? null, false);
555
+ notifyViewpointChange();
556
+ options.onSelectionChange?.(getSelectedObject());
557
+ options.onSelectionDetailsChange?.(buildObjectDetails(state.selectedObjectId));
558
+ emitAtlasStateChange();
559
+ },
560
+ serializeAtlasState() {
561
+ return serializeViewerAtlasState(api.getAtlasState());
562
+ },
563
+ captureBookmark(name, label) {
564
+ return createViewerBookmark(name, label, api.getAtlasState());
565
+ },
566
+ applyBookmark(bookmark) {
567
+ if (typeof bookmark === "string") {
568
+ api.setAtlasState(bookmark);
569
+ return true;
570
+ }
571
+ api.setAtlasState(bookmark.atlasState);
572
+ return true;
573
+ },
574
+ setRenderOptions(options) {
575
+ const sceneAffecting = hasSceneAffectingRenderOptions(options);
576
+ const previousRenderOptions = renderOptions;
577
+ const previousScene = scene;
578
+ const previousSpatialScene = spatialScene;
579
+ const nextRenderOptions = mergeRenderOptions(renderOptions, options);
580
+ let nextScene = scene;
581
+ if (currentInput.kind !== "scene" && sceneAffecting) {
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;
601
+ }
602
+ },
603
+ getState() {
604
+ return { ...state };
605
+ },
606
+ setState(nextState) {
607
+ updateState(sanitizeState({ ...state, ...nextState }));
608
+ },
609
+ zoomBy(factor, anchor) {
610
+ updateState(zoomViewerStateAt(scene, state, factor, anchor ?? { x: scene.width / 2, y: scene.height / 2 }, constraints));
611
+ },
612
+ panBy(dx, dy) {
613
+ updateState(panViewerState(state, dx, dy));
614
+ },
615
+ rotateBy(deg) {
616
+ updateState(rotateViewerState(state, deg));
617
+ },
618
+ fitToSystem() {
619
+ updateState(is3DView()
620
+ ? { ...DEFAULT_VIEWER_STATE, selectedObjectId: state.selectedObjectId }
621
+ : fitViewerState(scene, state, constraints));
622
+ },
623
+ focusObject(id) {
624
+ activeViewpointId = null;
625
+ updateState(is3DView()
626
+ ? create3DFocusState(id)
627
+ : focusViewerState(scene, state, id, constraints));
628
+ applySelection(id);
629
+ if (behavior.tooltipMode === "pinned") {
630
+ pinnedTooltipObjectId = getObjectById(id)?.objectId ?? null;
631
+ updateTooltip();
632
+ }
633
+ },
634
+ pinTooltip(id) {
635
+ pinnedTooltipObjectId = getObjectById(id)?.objectId ?? null;
636
+ updateTooltip();
637
+ },
638
+ resetView() {
639
+ const resetState = is3DView()
640
+ ? { ...DEFAULT_VIEWER_STATE }
641
+ : fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints);
642
+ activeViewpointId = null;
643
+ updateState(resetState);
644
+ applySelection(null);
645
+ pinnedTooltipObjectId = null;
646
+ updateTooltip();
647
+ },
648
+ exportSvg() {
649
+ return renderSceneToSvg(scene, {
650
+ ...renderOptions,
651
+ filter: renderOptions.filter ?? null,
652
+ selectedObjectId: state.selectedObjectId,
653
+ });
654
+ },
655
+ destroy() {
656
+ if (destroyed) {
657
+ return;
658
+ }
659
+ destroyed = true;
660
+ container.removeEventListener("wheel", handleWheel);
661
+ container.removeEventListener("pointerdown", handlePointerDown);
662
+ container.removeEventListener("pointermove", handlePointerMove);
663
+ container.removeEventListener("pointerup", handlePointerEnd);
664
+ container.removeEventListener("pointercancel", handlePointerEnd);
665
+ container.removeEventListener("click", handleClick);
666
+ container.removeEventListener("mouseover", handleMouseOver);
667
+ container.removeEventListener("mouseleave", handleMouseLeave);
668
+ container.removeEventListener("focusin", handleFocusIn);
669
+ container.removeEventListener("focusout", handleFocusOut);
670
+ container.removeEventListener("keydown", handleKeyDown);
671
+ stopAnimationLoop();
672
+ runtime3d?.destroy();
673
+ runtime3d = null;
674
+ tooltipRoot?.remove();
675
+ tooltipRoot = null;
676
+ minimapRoot?.remove();
677
+ minimapRoot = null;
678
+ container.classList.remove("wo-viewer-container");
679
+ container.style.touchAction = previousTouchAction;
680
+ container.style.position = previousPosition;
681
+ if (previousTabIndex === null) {
682
+ container.removeAttribute("tabindex");
683
+ }
684
+ else {
685
+ container.setAttribute("tabindex", previousTabIndex);
686
+ }
687
+ },
688
+ };
689
+ rerenderScene(true);
690
+ if (options.initialViewpointId) {
691
+ api.goToViewpoint(options.initialViewpointId);
692
+ }
693
+ else if (options.initialSelectionObjectId) {
694
+ api.focusObject(options.initialSelectionObjectId);
695
+ }
696
+ else {
697
+ emitAtlasStateChange();
698
+ }
699
+ return api;
700
+ function rerenderScene(resetView) {
701
+ runtime3d?.destroy();
702
+ runtime3d = null;
703
+ container.innerHTML = "";
704
+ svgElement = null;
705
+ cameraRoot = null;
706
+ minimapRoot = null;
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
+ }
721
+ if (behavior.minimap) {
722
+ minimapRoot = document.createElement("div");
723
+ minimapRoot.dataset.worldorbitMinimapRoot = "true";
724
+ container.append(minimapRoot);
725
+ }
726
+ if (behavior.tooltipMode !== "disabled") {
727
+ tooltipRoot = document.createElement("div");
728
+ tooltipRoot.className = "wo-viewer-tooltip-root";
729
+ tooltipRoot.dataset.worldorbitTooltip = "true";
730
+ tooltipRoot.hidden = true;
731
+ tooltipRoot.addEventListener("click", handleTooltipClick);
732
+ container.append(tooltipRoot);
733
+ }
734
+ if (!is3DView() && (!svgElement || !cameraRoot)) {
735
+ throw new Error("Interactive viewer could not locate the rendered SVG camera root.");
736
+ }
737
+ state = resetView
738
+ ? is3DView()
739
+ ? { ...DEFAULT_VIEWER_STATE }
740
+ : fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints)
741
+ : sanitizeState(state);
742
+ applySelection(state.selectedObjectId &&
743
+ getObjectById(state.selectedObjectId)
744
+ ? state.selectedObjectId
745
+ : null, false);
746
+ applyHover(hoveredObjectId &&
747
+ getObjectById(hoveredObjectId)
748
+ ? hoveredObjectId
749
+ : null, false);
750
+ pinnedTooltipObjectId =
751
+ pinnedTooltipObjectId && getObjectById(pinnedTooltipObjectId)
752
+ ? pinnedTooltipObjectId
753
+ : null;
754
+ syncRuntimePresentation();
755
+ notifyFilterChange();
756
+ notifyViewpointChange();
757
+ options.onViewChange?.({ ...state });
758
+ emitAtlasStateChange();
759
+ }
760
+ function updateState(nextState) {
761
+ state = sanitizeState(nextState);
762
+ syncRuntimePresentation();
763
+ options.onViewChange?.({ ...state });
764
+ emitAtlasStateChange();
765
+ }
766
+ function sanitizeState(nextState) {
767
+ return {
768
+ scale: clampValue(nextState.scale, constraints.minScale, constraints.maxScale),
769
+ rotationDeg: normalizeRotation(nextState.rotationDeg),
770
+ translateX: Number.isFinite(nextState.translateX) ? nextState.translateX : state.translateX,
771
+ translateY: Number.isFinite(nextState.translateY) ? nextState.translateY : state.translateY,
772
+ selectedObjectId: nextState.selectedObjectId && getObjectById(nextState.selectedObjectId)
773
+ ? nextState.selectedObjectId
774
+ : null,
775
+ };
776
+ }
777
+ function updateCameraTransform() {
778
+ if (is3DView()) {
779
+ sync3DView();
780
+ return;
781
+ }
782
+ if (!cameraRoot) {
783
+ return;
784
+ }
785
+ cameraRoot.setAttribute("transform", composeViewerTransform(scene, state));
786
+ updateMinimap();
787
+ updateTooltip();
788
+ }
789
+ function applySelection(objectId, emitCallback = true) {
790
+ if (!is3DView() && state.selectedObjectId) {
791
+ container
792
+ .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
793
+ ?.classList.remove("wo-object-selected");
794
+ }
795
+ state = {
796
+ ...state,
797
+ selectedObjectId: objectId && getObjectById(objectId)
798
+ ? objectId
799
+ : null,
800
+ };
801
+ if (!is3DView() && state.selectedObjectId) {
802
+ container
803
+ .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
804
+ ?.classList.add("wo-object-selected");
805
+ }
806
+ syncAtlasHighlights();
807
+ updateTooltip();
808
+ if (emitCallback) {
809
+ options.onSelectionChange?.(getSelectedObject());
810
+ options.onSelectionDetailsChange?.(buildObjectDetails(state.selectedObjectId));
811
+ options.onViewChange?.({ ...state });
812
+ emitAtlasStateChange();
813
+ }
814
+ }
815
+ function applyHover(objectId, emitCallback = true) {
816
+ if (hoveredObjectId === objectId && emitCallback) {
817
+ return;
818
+ }
819
+ hoveredObjectId =
820
+ objectId && getObjectById(objectId)
821
+ ? objectId
822
+ : null;
823
+ syncAtlasHighlights();
824
+ updateTooltip();
825
+ if (emitCallback) {
826
+ options.onHoverChange?.(getObjectById(hoveredObjectId));
827
+ options.onHoverDetailsChange?.(buildObjectDetails(hoveredObjectId));
828
+ }
829
+ }
830
+ function getSelectedObject() {
831
+ return getObjectById(state.selectedObjectId);
832
+ }
833
+ function getObjectById(objectId) {
834
+ if (!objectId) {
835
+ return null;
836
+ }
837
+ const visibleObjectIds = getVisibleObjectIds();
838
+ return (scene.objects.find((object) => object.objectId === objectId &&
839
+ !object.hidden &&
840
+ visibleObjectIds.has(object.objectId)) ?? null);
841
+ }
842
+ function buildObjectDetails(objectId) {
843
+ const renderObject = getObjectById(objectId);
844
+ if (!renderObject) {
845
+ return null;
846
+ }
847
+ return {
848
+ objectId: renderObject.objectId,
849
+ object: renderObject.object,
850
+ renderObject,
851
+ label: scene.labels.find((label) => label.objectId === renderObject.objectId && !label.hidden) ?? null,
852
+ group: scene.groups.find((group) => group.renderId === renderObject.groupId) ?? null,
853
+ semanticGroups: scene.semanticGroups.filter((group) => renderObject.semanticGroupIds.includes(group.id)),
854
+ orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
855
+ relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden &&
856
+ (orbit.objectId === renderObject.objectId ||
857
+ renderObject.ancestorIds.includes(orbit.objectId) ||
858
+ renderObject.childIds.includes(orbit.objectId))),
859
+ relations: scene.relations.filter((relation) => !relation.hidden &&
860
+ (relation.fromObjectId === renderObject.objectId ||
861
+ relation.toObjectId === renderObject.objectId)),
862
+ relatedEvents: scene.events.filter((event) => !event.hidden &&
863
+ (event.targetObjectId === renderObject.objectId ||
864
+ event.objectIds.includes(renderObject.objectId))),
865
+ parent: getObjectById(renderObject.parentId),
866
+ children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
867
+ ancestors: renderObject.ancestorIds
868
+ .map((ancestorId) => getObjectById(ancestorId))
869
+ .filter(Boolean),
870
+ focusPath: buildFocusPath(renderObject.objectId),
871
+ };
872
+ }
873
+ function syncAtlasHighlights() {
874
+ if (is3DView()) {
875
+ sync3DView();
876
+ return;
877
+ }
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")) {
879
+ element.classList.remove("wo-chain-selected", "wo-chain-hover", "wo-ancestor-selected", "wo-ancestor-hover", "wo-orbit-related-selected", "wo-orbit-related-hover");
880
+ }
881
+ applyChainClasses(state.selectedObjectId, {
882
+ objectClass: "wo-chain-selected",
883
+ ancestorClass: "wo-ancestor-selected",
884
+ orbitClass: "wo-orbit-related-selected",
885
+ });
886
+ applyChainClasses(hoveredObjectId, {
887
+ objectClass: "wo-chain-hover",
888
+ ancestorClass: "wo-ancestor-hover",
889
+ orbitClass: "wo-orbit-related-hover",
890
+ });
891
+ }
892
+ function applyChainClasses(objectId, classes) {
893
+ const details = buildObjectDetails(objectId);
894
+ if (!details) {
895
+ return;
896
+ }
897
+ const chainIds = new Set([
898
+ details.objectId,
899
+ ...details.renderObject.childIds,
900
+ ...details.renderObject.ancestorIds,
901
+ ]);
902
+ for (const id of chainIds) {
903
+ for (const element of container.querySelectorAll(`[data-object-id="${cssEscape(id)}"]`)) {
904
+ element.classList.add(classes.objectClass);
905
+ }
906
+ }
907
+ for (const ancestor of details.ancestors) {
908
+ for (const element of container.querySelectorAll(`[data-object-id="${cssEscape(ancestor.objectId)}"]`)) {
909
+ element.classList.add(classes.ancestorClass);
910
+ }
911
+ }
912
+ for (const orbit of details.relatedOrbits) {
913
+ for (const element of container.querySelectorAll(`[data-orbit-object-id="${cssEscape(orbit.objectId)}"]`)) {
914
+ element.classList.add(classes.orbitClass);
915
+ }
916
+ }
917
+ }
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
+ }
932
+ if (!svgElement) {
933
+ return {
934
+ x: scene.width / 2,
935
+ y: scene.height / 2,
936
+ };
937
+ }
938
+ const rect = svgElement.getBoundingClientRect();
939
+ if (!rect.width || !rect.height) {
940
+ return {
941
+ x: scene.width / 2,
942
+ y: scene.height / 2,
943
+ };
944
+ }
945
+ return {
946
+ x: ((clientX - rect.left) / rect.width) * scene.width,
947
+ y: ((clientY - rect.top) / rect.height) * scene.height,
948
+ };
949
+ }
950
+ function getWorldPointFromClient(clientX, clientY) {
951
+ return invertViewerPoint(scene, state, getViewportPointFromClient(clientX, clientY));
952
+ }
953
+ function getVisibleObjectIds() {
954
+ return computeVisibleObjectIds(scene, renderOptions.filter ?? null);
955
+ }
956
+ function getVisibleSceneObjects() {
957
+ const visibleObjectIds = getVisibleObjectIds();
958
+ return scene.objects.filter((object) => !object.hidden && visibleObjectIds.has(object.objectId));
959
+ }
960
+ function buildFocusPath(objectId) {
961
+ const object = scene.objects.find((entry) => entry.objectId === objectId && !entry.hidden);
962
+ if (!object) {
963
+ return [];
964
+ }
965
+ return [...object.ancestorIds, object.objectId]
966
+ .map((entryId) => getObjectById(entryId))
967
+ .filter(Boolean);
968
+ }
969
+ function getViewpointById(id) {
970
+ return scene.viewpoints.find((viewpoint) => viewpoint.id === id) ?? null;
971
+ }
972
+ function createViewpointState(viewpoint) {
973
+ const rotationDeg = normalizeRotation(viewpoint.rotationDeg);
974
+ const scale = viewpoint.scale !== null && viewpoint.scale !== undefined
975
+ ? clampValue(viewpoint.scale, constraints.minScale, constraints.maxScale)
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
+ }
990
+ const targetObject = viewpoint.objectId &&
991
+ scene.objects.find((object) => object.objectId === viewpoint.objectId && !object.hidden);
992
+ if (targetObject) {
993
+ return createCenteredState({ x: targetObject.x, y: targetObject.y }, scale ?? Math.max(1.8, DEFAULT_VIEWER_STATE.scale), rotationDeg, viewpoint.selectedObjectId ?? targetObject.objectId);
994
+ }
995
+ const baseState = fitViewerState(scene, { ...DEFAULT_VIEWER_STATE, rotationDeg }, constraints);
996
+ if (scale === null) {
997
+ return {
998
+ ...baseState,
999
+ rotationDeg,
1000
+ selectedObjectId: viewpoint.selectedObjectId ?? null,
1001
+ };
1002
+ }
1003
+ return createCenteredState({
1004
+ x: scene.contentBounds.centerX,
1005
+ y: scene.contentBounds.centerY,
1006
+ }, scale, rotationDeg, viewpoint.selectedObjectId ?? null);
1007
+ }
1008
+ function createCenteredState(target, scale, rotationDeg, selectedObjectId) {
1009
+ const center = {
1010
+ x: scene.width / 2,
1011
+ y: scene.height / 2,
1012
+ };
1013
+ const rotatedTarget = rotatePoint(target, center, rotationDeg);
1014
+ return {
1015
+ scale,
1016
+ rotationDeg,
1017
+ translateX: center.x - (center.x + (rotatedTarget.x - center.x) * scale),
1018
+ translateY: center.y - (center.y + (rotatedTarget.y - center.y) * scale),
1019
+ selectedObjectId,
1020
+ };
1021
+ }
1022
+ function setFilterInternal(filter, emitCallbacks, clearActiveViewpoint) {
1023
+ renderOptions = {
1024
+ ...renderOptions,
1025
+ filter: normalizeViewerFilter(filter),
1026
+ };
1027
+ if (clearActiveViewpoint) {
1028
+ activeViewpointId = null;
1029
+ }
1030
+ rerenderScene(false);
1031
+ if (!emitCallbacks) {
1032
+ return;
1033
+ }
1034
+ }
1035
+ function notifyFilterChange() {
1036
+ options.onFilterChange?.(renderOptions.filter ?? null, getVisibleSceneObjects());
1037
+ }
1038
+ function notifyViewpointChange() {
1039
+ options.onViewpointChange?.(getViewpointById(activeViewpointId));
1040
+ }
1041
+ function emitAtlasStateChange() {
1042
+ options.onAtlasStateChange?.(api.getAtlasState());
1043
+ }
1044
+ function updateMinimap() {
1045
+ if (!behavior.minimap || !minimapRoot) {
1046
+ return;
1047
+ }
1048
+ minimapRoot.innerHTML = renderViewerMinimap(scene, state, getVisibleSceneObjects());
1049
+ }
1050
+ function updateTooltip() {
1051
+ if (behavior.tooltipMode === "disabled" || !tooltipRoot) {
1052
+ setTooltipDetails(null);
1053
+ return;
1054
+ }
1055
+ const resolved = resolveTooltipTarget();
1056
+ if (!resolved) {
1057
+ tooltipRoot.hidden = true;
1058
+ tooltipRoot.innerHTML = "";
1059
+ tooltipRoot.removeAttribute("data-mode");
1060
+ setTooltipDetails(null);
1061
+ return;
1062
+ }
1063
+ const details = buildObjectDetails(resolved.objectId);
1064
+ if (!details) {
1065
+ tooltipRoot.hidden = true;
1066
+ tooltipRoot.innerHTML = "";
1067
+ tooltipRoot.removeAttribute("data-mode");
1068
+ setTooltipDetails(null);
1069
+ return;
1070
+ }
1071
+ const tooltipDetails = buildViewerTooltipDetails(details);
1072
+ activeTooltipObjectId = resolved.objectId;
1073
+ tooltipRoot.hidden = false;
1074
+ tooltipRoot.dataset.mode = resolved.mode;
1075
+ tooltipRoot.classList.toggle("is-pinned", resolved.mode === "pinned");
1076
+ tooltipRoot.style.pointerEvents = "auto";
1077
+ tooltipRoot.style.visibility = "hidden";
1078
+ renderTooltipContent(tooltipRoot, tooltipDetails, resolved.mode);
1079
+ positionTooltip(tooltipRoot, details.renderObject);
1080
+ tooltipRoot.style.visibility = "visible";
1081
+ setTooltipDetails(tooltipDetails);
1082
+ }
1083
+ function resolveTooltipTarget() {
1084
+ if (pinnedTooltipObjectId && getObjectById(pinnedTooltipObjectId)) {
1085
+ return {
1086
+ objectId: pinnedTooltipObjectId,
1087
+ mode: "pinned",
1088
+ };
1089
+ }
1090
+ if (hoveredObjectId && getObjectById(hoveredObjectId)) {
1091
+ return {
1092
+ objectId: hoveredObjectId,
1093
+ mode: "hover",
1094
+ };
1095
+ }
1096
+ return null;
1097
+ }
1098
+ function renderTooltipContent(element, details, mode) {
1099
+ const customMarkup = options.tooltipRenderer?.(details, mode);
1100
+ element.innerHTML = "";
1101
+ if (typeof customMarkup === "string") {
1102
+ element.innerHTML = customMarkup;
1103
+ }
1104
+ else if (customMarkup instanceof HTMLElement) {
1105
+ element.append(customMarkup);
1106
+ }
1107
+ else {
1108
+ element.innerHTML = renderDefaultTooltipContent(details, mode);
1109
+ }
1110
+ const actions = document.createElement("div");
1111
+ actions.className = "wo-tooltip-actions";
1112
+ if (mode === "pinned") {
1113
+ const unpinButton = document.createElement("button");
1114
+ unpinButton.type = "button";
1115
+ unpinButton.className = "wo-tooltip-action";
1116
+ unpinButton.dataset.tooltipAction = "unpin";
1117
+ unpinButton.textContent = "Unpin";
1118
+ actions.append(unpinButton);
1119
+ }
1120
+ else {
1121
+ const pinButton = document.createElement("button");
1122
+ pinButton.type = "button";
1123
+ pinButton.className = "wo-tooltip-action";
1124
+ pinButton.dataset.tooltipAction = "pin";
1125
+ pinButton.dataset.objectId = details.objectId;
1126
+ pinButton.textContent = "Pin";
1127
+ actions.append(pinButton);
1128
+ }
1129
+ if (actions.childElementCount > 0) {
1130
+ element.append(actions);
1131
+ }
1132
+ }
1133
+ function positionTooltip(element, renderObject) {
1134
+ const point = is3DView()
1135
+ ? runtime3d?.projectObjectToContainer(renderObject.objectId) ?? null
1136
+ : project2DTooltipPoint(renderObject);
1137
+ if (!point) {
1138
+ return;
1139
+ }
1140
+ const maxLeft = Math.max(container.clientWidth - element.offsetWidth - 12, 12);
1141
+ const maxTop = Math.max(container.clientHeight - element.offsetHeight - 12, 12);
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);
1145
+ element.style.left = `${nextLeft}px`;
1146
+ element.style.top = `${nextTop}px`;
1147
+ }
1148
+ function projectWorldPoint(point) {
1149
+ if (is3DView()) {
1150
+ return point;
1151
+ }
1152
+ const center = {
1153
+ x: scene.width / 2,
1154
+ y: scene.height / 2,
1155
+ };
1156
+ const rotated = rotatePoint(point, center, state.rotationDeg);
1157
+ return {
1158
+ x: center.x + (rotated.x - center.x) * state.scale + state.translateX,
1159
+ y: center.y + (rotated.y - center.y) * state.scale + state.translateY,
1160
+ };
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
+ }
1183
+ function handleTooltipClick(event) {
1184
+ const target = event.target?.closest("[data-tooltip-action]");
1185
+ if (!target) {
1186
+ return;
1187
+ }
1188
+ event.preventDefault();
1189
+ event.stopPropagation();
1190
+ switch (target.dataset.tooltipAction) {
1191
+ case "pin":
1192
+ pinnedTooltipObjectId = target.dataset.objectId ?? activeTooltipObjectId;
1193
+ break;
1194
+ case "unpin":
1195
+ pinnedTooltipObjectId = null;
1196
+ break;
1197
+ }
1198
+ updateTooltip();
1199
+ }
1200
+ function setTooltipDetails(details) {
1201
+ const changed = activeTooltipDetails?.objectId !== details?.objectId ||
1202
+ activeTooltipDetails?.description !== details?.description ||
1203
+ activeTooltipDetails?.imageHref !== details?.imageHref;
1204
+ activeTooltipDetails = details;
1205
+ activeTooltipObjectId = details?.objectId ?? null;
1206
+ if (changed) {
1207
+ options.onTooltipChange?.(details);
1208
+ }
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
+ }
1296
+ }
1297
+ function resolveInitialInput(options) {
1298
+ if (options.scene) {
1299
+ return { kind: "scene", value: options.scene };
1300
+ }
1301
+ if (options.document) {
1302
+ return { kind: "document", value: options.document };
1303
+ }
1304
+ if (options.source) {
1305
+ return { kind: "source", value: options.source };
1306
+ }
1307
+ throw new Error("Interactive viewer requires an initial render input.");
1308
+ }
1309
+ function renderSceneFromInput(input, renderOptions) {
1310
+ switch (input.kind) {
1311
+ case "scene":
1312
+ return input.value;
1313
+ case "document":
1314
+ return renderDocumentToScene(input.value, renderOptions);
1315
+ case "source": {
1316
+ const loaded = loadWorldOrbitSource(input.value);
1317
+ return renderDocumentToScene(loaded.document, resolveSourceRenderOptions(loaded, renderOptions));
1318
+ }
1319
+ }
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
+ }
1336
+ function cloneRenderOptions(renderOptions) {
1337
+ return {
1338
+ ...renderOptions,
1339
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
1340
+ filter: renderOptions.filter ? { ...renderOptions.filter } : undefined,
1341
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : undefined,
1342
+ layers: renderOptions.layers ? { ...renderOptions.layers } : undefined,
1343
+ theme: renderOptions.theme && typeof renderOptions.theme === "object"
1344
+ ? { ...renderOptions.theme }
1345
+ : renderOptions.theme,
1346
+ activeEventId: renderOptions.activeEventId ?? null,
1347
+ viewMode: renderOptions.viewMode ?? "2d",
1348
+ };
1349
+ }
1350
+ function mergeRenderOptions(current, next) {
1351
+ return {
1352
+ ...current,
1353
+ ...next,
1354
+ camera: next.camera !== undefined
1355
+ ? next.camera
1356
+ ? { ...next.camera }
1357
+ : null
1358
+ : current.camera
1359
+ ? { ...current.camera }
1360
+ : null,
1361
+ filter: next.filter !== undefined
1362
+ ? normalizeViewerFilter(next.filter)
1363
+ : current.filter
1364
+ ? { ...current.filter }
1365
+ : undefined,
1366
+ scaleModel: next.scaleModel
1367
+ ? {
1368
+ ...(current.scaleModel ?? {}),
1369
+ ...next.scaleModel,
1370
+ }
1371
+ : current.scaleModel
1372
+ ? { ...current.scaleModel }
1373
+ : undefined,
1374
+ layers: next.layers
1375
+ ? {
1376
+ ...(current.layers ?? {}),
1377
+ ...next.layers,
1378
+ }
1379
+ : current.layers
1380
+ ? { ...current.layers }
1381
+ : undefined,
1382
+ theme: next.theme && typeof next.theme === "object"
1383
+ ? { ...next.theme }
1384
+ : next.theme ?? current.theme,
1385
+ viewMode: next.viewMode ?? current.viewMode ?? "2d",
1386
+ };
1387
+ }
1388
+ function hasSceneAffectingRenderOptions(options) {
1389
+ return (options.width !== undefined ||
1390
+ options.height !== undefined ||
1391
+ options.padding !== undefined ||
1392
+ options.preset !== undefined ||
1393
+ options.projection !== undefined ||
1394
+ options.camera !== undefined ||
1395
+ options.scaleModel !== undefined ||
1396
+ options.activeEventId !== undefined);
1397
+ }
1398
+ function resolveSourceRenderOptions(loaded, renderOptions) {
1399
+ const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
1400
+ if (renderOptions.preset || !atlasDocument?.system?.defaults.preset) {
1401
+ return renderOptions;
1402
+ }
1403
+ return {
1404
+ ...renderOptions,
1405
+ preset: atlasDocument.system.defaults.preset,
1406
+ };
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
+ }
1501
+ function createTouchGestureState(scene, state, touchPoints) {
1502
+ const { center, distance } = getTouchCenterAndDistance(touchPoints);
1503
+ return {
1504
+ startState: { ...state },
1505
+ startCenter: invertViewerPoint(scene, state, center),
1506
+ startViewportCenter: center,
1507
+ startDistance: distance,
1508
+ };
1509
+ }
1510
+ function getTouchCenterAndDistance(touchPoints) {
1511
+ const points = [...touchPoints.values()];
1512
+ if (points.length < 2) {
1513
+ return {
1514
+ center: points[0] ?? { x: 0, y: 0 },
1515
+ distance: 1,
1516
+ };
1517
+ }
1518
+ const [first, second] = points;
1519
+ return {
1520
+ center: {
1521
+ x: (first.x + second.x) / 2,
1522
+ y: (first.y + second.y) / 2,
1523
+ },
1524
+ distance: Math.hypot(second.x - first.x, second.y - first.y),
1525
+ };
1526
+ }
1527
+ function getClosestObjectId(target) {
1528
+ if (!(target instanceof Element)) {
1529
+ return null;
1530
+ }
1531
+ return target.closest("[data-object-id]")?.dataset.objectId ?? null;
1532
+ }
1533
+ function ensureBrowserEnvironment(container) {
1534
+ if (typeof window === "undefined" || typeof document === "undefined") {
1535
+ throw new Error("createInteractiveViewer can only run in a browser environment.");
1536
+ }
1537
+ if (!(container instanceof HTMLElement)) {
1538
+ throw new Error("Interactive viewer requires an HTMLElement container.");
1539
+ }
1540
+ }
1541
+ function clampValue(value, min, max) {
1542
+ return Math.min(Math.max(value, min), max);
1543
+ }
1544
+ function normalizeRotation(rotationDeg) {
1545
+ let normalized = rotationDeg % 360;
1546
+ if (normalized > 180) {
1547
+ normalized -= 360;
1548
+ }
1549
+ if (normalized <= -180) {
1550
+ normalized += 360;
1551
+ }
1552
+ return normalized;
1553
+ }
1554
+ function cssEscape(value) {
1555
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
1556
+ return CSS.escape(value);
1557
+ }
1558
+ return value.replace(/["\\]/g, "\\$&");
1559
+ }
1560
+ function installViewerTooltipStyles() {
1561
+ if (typeof document === "undefined" || document.getElementById(TOOLTIP_STYLE_ID)) {
1562
+ return;
1563
+ }
1564
+ const style = document.createElement("style");
1565
+ style.id = TOOLTIP_STYLE_ID;
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
+ }
1595
+ .wo-viewer-tooltip-root {
1596
+ position: absolute;
1597
+ z-index: 12;
1598
+ min-width: 220px;
1599
+ max-width: min(320px, calc(100% - 24px));
1600
+ padding: 14px;
1601
+ border-radius: 18px;
1602
+ border: 1px solid rgba(255,255,255,0.1);
1603
+ background: rgba(7, 16, 25, 0.92);
1604
+ box-shadow: 0 18px 32px rgba(0,0,0,0.28);
1605
+ color: #edf6ff;
1606
+ backdrop-filter: blur(12px);
1607
+ font: 500 13px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
1608
+ }
1609
+ .wo-viewer-tooltip-root[data-mode="hover"] { pointer-events: auto; }
1610
+ .wo-viewer-tooltip-root[data-mode="pinned"] { pointer-events: auto; }
1611
+ .wo-tooltip-card { display: grid; gap: 10px; }
1612
+ .wo-tooltip-head { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 12px; align-items: center; }
1613
+ .wo-tooltip-heading { display: grid; gap: 3px; }
1614
+ .wo-tooltip-heading strong { font: 700 16px/1.2 "Segoe UI Variable Display", "Segoe UI", sans-serif; }
1615
+ .wo-tooltip-heading span, .wo-tooltip-relations { color: rgba(237, 246, 255, 0.7); }
1616
+ .wo-tooltip-image {
1617
+ width: 52px;
1618
+ height: 52px;
1619
+ object-fit: cover;
1620
+ border-radius: 14px;
1621
+ border: 1px solid rgba(255,255,255,0.12);
1622
+ background: rgba(255,255,255,0.06);
1623
+ }
1624
+ .wo-tooltip-image-placeholder {
1625
+ display: grid;
1626
+ place-items: center;
1627
+ font: 700 18px/1 "Segoe UI Variable Display", "Segoe UI", sans-serif;
1628
+ color: #ffce8a;
1629
+ }
1630
+ .wo-tooltip-description { margin: 0; }
1631
+ .wo-tooltip-tags { display: flex; flex-wrap: wrap; gap: 6px; }
1632
+ .wo-tooltip-tag {
1633
+ padding: 3px 8px;
1634
+ border-radius: 999px;
1635
+ background: rgba(255,255,255,0.08);
1636
+ color: #ffdda9;
1637
+ font: 600 11px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
1638
+ text-transform: uppercase;
1639
+ letter-spacing: 0.06em;
1640
+ }
1641
+ .wo-tooltip-fields { display: grid; gap: 6px; margin: 0; }
1642
+ .wo-tooltip-field {
1643
+ display: grid;
1644
+ grid-template-columns: minmax(0, 1fr) auto;
1645
+ gap: 12px;
1646
+ align-items: baseline;
1647
+ }
1648
+ .wo-tooltip-field dt { color: rgba(237, 246, 255, 0.68); }
1649
+ .wo-tooltip-field dd { margin: 0; font-weight: 600; text-align: right; }
1650
+ .wo-tooltip-actions { display: flex; justify-content: flex-end; margin-top: 10px; }
1651
+ .wo-tooltip-action {
1652
+ border: 1px solid rgba(240, 180, 100, 0.24);
1653
+ border-radius: 999px;
1654
+ background: rgba(240, 180, 100, 0.12);
1655
+ color: #edf6ff;
1656
+ cursor: pointer;
1657
+ padding: 6px 12px;
1658
+ font: 600 12px/1.3 "Segoe UI Variable", "Segoe UI", sans-serif;
1659
+ }
1660
+ `;
1661
+ document.head.append(style);
1662
+ }