worldorbit 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE.md +5 -0
  2. package/README.md +250 -0
  3. package/dist/browser/core/dist/index.js +4009 -0
  4. package/dist/browser/markdown/dist/index.js +3951 -0
  5. package/dist/browser/viewer/dist/index.js +5981 -0
  6. package/dist/constants.d.ts +8 -0
  7. package/dist/constants.js +84 -0
  8. package/dist/errors.d.ts +7 -0
  9. package/dist/errors.js +16 -0
  10. package/dist/index.d.ts +18 -0
  11. package/dist/index.js +25 -0
  12. package/dist/normalize.d.ts +2 -0
  13. package/dist/normalize.js +243 -0
  14. package/dist/parse.d.ts +2 -0
  15. package/dist/parse.js +126 -0
  16. package/dist/render.d.ts +6 -0
  17. package/dist/render.js +683 -0
  18. package/dist/tokenize.d.ts +4 -0
  19. package/dist/tokenize.js +68 -0
  20. package/dist/types.d.ts +208 -0
  21. package/dist/types.js +1 -0
  22. package/dist/unpkg/core/dist/index.js +4081 -0
  23. package/dist/unpkg/markdown/dist/index.js +3979 -0
  24. package/dist/unpkg/test.html +1 -0
  25. package/dist/unpkg/viewer/dist/index.js +6038 -0
  26. package/dist/unpkg/worldorbit-core.min.js +5 -0
  27. package/dist/unpkg/worldorbit-markdown.min.js +81 -0
  28. package/dist/unpkg/worldorbit-viewer.min.js +232 -0
  29. package/dist/unpkg/worldorbit.d.ts +2 -0
  30. package/dist/unpkg/worldorbit.js +2 -0
  31. package/dist/unpkg/worldorbit.min.js +236 -0
  32. package/dist/validate.d.ts +2 -0
  33. package/dist/validate.js +31 -0
  34. package/dist/viewer-state.d.ts +16 -0
  35. package/dist/viewer-state.js +130 -0
  36. package/dist/viewer.d.ts +2 -0
  37. package/dist/viewer.js +434 -0
  38. package/package.json +64 -0
  39. package/packages/core/README.md +13 -0
  40. package/packages/core/dist/atlas-edit.d.ts +11 -0
  41. package/packages/core/dist/atlas-edit.js +210 -0
  42. package/packages/core/dist/diagnostics.d.ts +10 -0
  43. package/packages/core/dist/diagnostics.js +109 -0
  44. package/packages/core/dist/draft-parse.d.ts +3 -0
  45. package/packages/core/dist/draft-parse.js +642 -0
  46. package/packages/core/dist/draft.d.ts +15 -0
  47. package/packages/core/dist/draft.js +343 -0
  48. package/packages/core/dist/errors.d.ts +7 -0
  49. package/packages/core/dist/errors.js +16 -0
  50. package/packages/core/dist/format.d.ts +4 -0
  51. package/packages/core/dist/format.js +364 -0
  52. package/packages/core/dist/index.d.ts +28 -0
  53. package/packages/core/dist/index.js +44 -0
  54. package/packages/core/dist/load.d.ts +4 -0
  55. package/packages/core/dist/load.js +130 -0
  56. package/packages/core/dist/markdown.d.ts +2 -0
  57. package/packages/core/dist/markdown.js +37 -0
  58. package/packages/core/dist/normalize.d.ts +2 -0
  59. package/packages/core/dist/normalize.js +304 -0
  60. package/packages/core/dist/parse.d.ts +2 -0
  61. package/packages/core/dist/parse.js +133 -0
  62. package/packages/core/dist/scene.d.ts +3 -0
  63. package/packages/core/dist/scene.js +1484 -0
  64. package/packages/core/dist/schema.d.ts +8 -0
  65. package/packages/core/dist/schema.js +298 -0
  66. package/packages/core/dist/tokenize.d.ts +4 -0
  67. package/packages/core/dist/tokenize.js +68 -0
  68. package/packages/core/dist/types.d.ts +382 -0
  69. package/packages/core/dist/types.js +1 -0
  70. package/packages/core/dist/validate.d.ts +2 -0
  71. package/packages/core/dist/validate.js +56 -0
  72. package/packages/editor/dist/editor.d.ts +2 -0
  73. package/packages/editor/dist/editor.js +2620 -0
  74. package/packages/editor/dist/index.d.ts +2 -0
  75. package/packages/editor/dist/index.js +1 -0
  76. package/packages/editor/dist/types.d.ts +53 -0
  77. package/packages/editor/dist/types.js +1 -0
  78. package/packages/markdown/README.md +9 -0
  79. package/packages/markdown/dist/html.d.ts +3 -0
  80. package/packages/markdown/dist/html.js +57 -0
  81. package/packages/markdown/dist/index.d.ts +4 -0
  82. package/packages/markdown/dist/index.js +3 -0
  83. package/packages/markdown/dist/rehype.d.ts +10 -0
  84. package/packages/markdown/dist/rehype.js +49 -0
  85. package/packages/markdown/dist/remark.d.ts +9 -0
  86. package/packages/markdown/dist/remark.js +28 -0
  87. package/packages/markdown/dist/types.d.ts +11 -0
  88. package/packages/markdown/dist/types.js +1 -0
  89. package/packages/viewer/README.md +12 -0
  90. package/packages/viewer/dist/atlas-state.d.ts +12 -0
  91. package/packages/viewer/dist/atlas-state.js +251 -0
  92. package/packages/viewer/dist/atlas-viewer.d.ts +2 -0
  93. package/packages/viewer/dist/atlas-viewer.js +448 -0
  94. package/packages/viewer/dist/custom-element.d.ts +1 -0
  95. package/packages/viewer/dist/custom-element.js +64 -0
  96. package/packages/viewer/dist/embed.d.ts +20 -0
  97. package/packages/viewer/dist/embed.js +138 -0
  98. package/packages/viewer/dist/index.d.ts +9 -0
  99. package/packages/viewer/dist/index.js +8 -0
  100. package/packages/viewer/dist/minimap.d.ts +3 -0
  101. package/packages/viewer/dist/minimap.js +63 -0
  102. package/packages/viewer/dist/render.d.ts +6 -0
  103. package/packages/viewer/dist/render.js +585 -0
  104. package/packages/viewer/dist/theme.d.ts +4 -0
  105. package/packages/viewer/dist/theme.js +98 -0
  106. package/packages/viewer/dist/tooltip.d.ts +3 -0
  107. package/packages/viewer/dist/tooltip.js +154 -0
  108. package/packages/viewer/dist/types.d.ts +256 -0
  109. package/packages/viewer/dist/types.js +1 -0
  110. package/packages/viewer/dist/viewer-state.d.ts +19 -0
  111. package/packages/viewer/dist/viewer-state.js +162 -0
  112. package/packages/viewer/dist/viewer.d.ts +2 -0
  113. package/packages/viewer/dist/viewer.js +1156 -0
@@ -0,0 +1,1156 @@
1
+ import { loadWorldOrbitSource, renderDocumentToScene, 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 { buildViewerTooltipDetails, renderDefaultTooltipContent, } from "./tooltip.js";
6
+ import { DEFAULT_VIEWER_STATE, composeViewerTransform, fitViewerState, focusViewerState, invertViewerPoint, panViewerState, rotateViewerState, zoomViewerStateAt, } from "./viewer-state.js";
7
+ const DEFAULT_VIEWER_LIMITS = {
8
+ minScale: 0.2,
9
+ maxScale: 8,
10
+ fitPadding: 48,
11
+ panStep: 40,
12
+ zoomStep: 1.2,
13
+ rotationStep: 15,
14
+ };
15
+ const TOOLTIP_STYLE_ID = "worldorbit-viewer-tooltip-style";
16
+ export function createInteractiveViewer(container, options) {
17
+ ensureBrowserEnvironment(container);
18
+ const inputCount = Number(Boolean(options.source)) +
19
+ Number(Boolean(options.document)) +
20
+ Number(Boolean(options.scene));
21
+ if (inputCount !== 1) {
22
+ throw new Error('Interactive viewer requires exactly one of "source", "document", or "scene".');
23
+ }
24
+ const constraints = {
25
+ minScale: options.minScale ?? DEFAULT_VIEWER_LIMITS.minScale,
26
+ maxScale: options.maxScale ?? DEFAULT_VIEWER_LIMITS.maxScale,
27
+ fitPadding: options.fitPadding ?? DEFAULT_VIEWER_LIMITS.fitPadding,
28
+ };
29
+ const behavior = {
30
+ keyboard: options.keyboard ?? true,
31
+ pointer: options.pointer ?? true,
32
+ touch: options.touch ?? true,
33
+ selection: options.selection ?? true,
34
+ tooltipMode: options.tooltipMode ?? "hover",
35
+ minimap: options.minimap ?? false,
36
+ panStep: options.panStep ?? DEFAULT_VIEWER_LIMITS.panStep,
37
+ zoomStep: options.zoomStep ?? DEFAULT_VIEWER_LIMITS.zoomStep,
38
+ rotationStep: options.rotationStep ?? DEFAULT_VIEWER_LIMITS.rotationStep,
39
+ };
40
+ let renderOptions = {
41
+ width: options.width,
42
+ height: options.height,
43
+ padding: options.padding,
44
+ preset: options.preset,
45
+ projection: options.projection,
46
+ scaleModel: options.scaleModel ? { ...options.scaleModel } : undefined,
47
+ theme: options.theme,
48
+ layers: options.layers,
49
+ filter: normalizeViewerFilter(options.initialFilter),
50
+ subtitle: options.subtitle,
51
+ };
52
+ const previousTabIndex = container.getAttribute("tabindex");
53
+ const previousTouchAction = container.style.touchAction;
54
+ const previousPosition = container.style.position;
55
+ let currentInput = resolveInitialInput(options);
56
+ let scene = renderSceneFromInput(currentInput, renderOptions);
57
+ let state = { ...DEFAULT_VIEWER_STATE };
58
+ let svgElement = null;
59
+ let cameraRoot = null;
60
+ let minimapRoot = null;
61
+ let tooltipRoot = null;
62
+ let suppressClick = false;
63
+ let activePointerId = null;
64
+ let lastPointerPoint = null;
65
+ let dragDistance = 0;
66
+ let destroyed = false;
67
+ let touchPoints = new Map();
68
+ let touchGesture = null;
69
+ let hoveredObjectId = null;
70
+ let pinnedTooltipObjectId = null;
71
+ let activeTooltipObjectId = null;
72
+ let activeTooltipDetails = null;
73
+ let activeViewpointId = null;
74
+ if (previousTabIndex === null) {
75
+ container.tabIndex = 0;
76
+ }
77
+ installViewerTooltipStyles();
78
+ container.classList.add("wo-viewer-container");
79
+ container.style.touchAction = behavior.touch ? "none" : previousTouchAction;
80
+ if (!container.style.position) {
81
+ container.style.position = "relative";
82
+ }
83
+ const handleWheel = (event) => {
84
+ if (!behavior.pointer || destroyed) {
85
+ return;
86
+ }
87
+ event.preventDefault();
88
+ container.focus();
89
+ const anchor = getWorldPointFromClient(event.clientX, event.clientY);
90
+ const factor = clampValue(Math.exp(-event.deltaY * 0.002), 0.6, 1.6);
91
+ updateState(zoomViewerStateAt(scene, state, factor, anchor, constraints));
92
+ };
93
+ const handlePointerDown = (event) => {
94
+ if (destroyed) {
95
+ return;
96
+ }
97
+ const isTouch = event.pointerType === "touch";
98
+ if ((isTouch && !behavior.touch) || (!isTouch && !behavior.pointer)) {
99
+ return;
100
+ }
101
+ if (!isTouch && event.button !== 0) {
102
+ return;
103
+ }
104
+ container.focus();
105
+ container.setPointerCapture?.(event.pointerId);
106
+ const point = getViewportPointFromClient(event.clientX, event.clientY);
107
+ if (isTouch) {
108
+ touchPoints.set(event.pointerId, point);
109
+ if (touchPoints.size === 2) {
110
+ touchGesture = createTouchGestureState(scene, state, touchPoints);
111
+ }
112
+ return;
113
+ }
114
+ activePointerId = event.pointerId;
115
+ lastPointerPoint = point;
116
+ dragDistance = 0;
117
+ suppressClick = false;
118
+ };
119
+ const handlePointerMove = (event) => {
120
+ if (destroyed) {
121
+ return;
122
+ }
123
+ const isTouch = event.pointerType === "touch";
124
+ if (isTouch) {
125
+ if (!behavior.touch || !touchPoints.has(event.pointerId)) {
126
+ return;
127
+ }
128
+ touchPoints.set(event.pointerId, getViewportPointFromClient(event.clientX, event.clientY));
129
+ if (touchPoints.size === 2) {
130
+ if (!touchGesture) {
131
+ touchGesture = createTouchGestureState(scene, state, touchPoints);
132
+ }
133
+ const current = getTouchCenterAndDistance(touchPoints);
134
+ const factor = current.distance / Math.max(touchGesture.startDistance, 1);
135
+ const zoomedState = zoomViewerStateAt(scene, touchGesture.startState, factor, touchGesture.startCenter, constraints);
136
+ const deltaX = current.center.x - touchGesture.startViewportCenter.x;
137
+ const deltaY = current.center.y - touchGesture.startViewportCenter.y;
138
+ updateState(panViewerState(zoomedState, deltaX, deltaY));
139
+ }
140
+ return;
141
+ }
142
+ if (!behavior.pointer || activePointerId !== event.pointerId || !lastPointerPoint) {
143
+ return;
144
+ }
145
+ const nextPoint = getViewportPointFromClient(event.clientX, event.clientY);
146
+ const deltaX = nextPoint.x - lastPointerPoint.x;
147
+ const deltaY = nextPoint.y - lastPointerPoint.y;
148
+ dragDistance += Math.abs(deltaX) + Math.abs(deltaY);
149
+ lastPointerPoint = nextPoint;
150
+ if (dragDistance > 2) {
151
+ suppressClick = true;
152
+ }
153
+ updateState(panViewerState(state, deltaX, deltaY));
154
+ };
155
+ const handlePointerEnd = (event) => {
156
+ if (event.pointerType === "touch") {
157
+ touchPoints.delete(event.pointerId);
158
+ if (touchPoints.size < 2) {
159
+ touchGesture = null;
160
+ }
161
+ return;
162
+ }
163
+ if (activePointerId === event.pointerId) {
164
+ activePointerId = null;
165
+ lastPointerPoint = null;
166
+ }
167
+ };
168
+ const handleClick = (event) => {
169
+ if (!behavior.selection || destroyed) {
170
+ return;
171
+ }
172
+ if (suppressClick) {
173
+ suppressClick = false;
174
+ return;
175
+ }
176
+ const objectId = getClosestObjectId(event.target);
177
+ applySelection(objectId);
178
+ if (behavior.tooltipMode === "pinned") {
179
+ pinnedTooltipObjectId = objectId;
180
+ updateTooltip();
181
+ }
182
+ };
183
+ const handleMouseOver = (event) => {
184
+ const objectId = getClosestObjectId(event.target);
185
+ applyHover(objectId);
186
+ };
187
+ const handleMouseLeave = () => {
188
+ applyHover(null);
189
+ };
190
+ const handleFocusIn = (event) => {
191
+ const objectId = getClosestObjectId(event.target);
192
+ if (!objectId) {
193
+ return;
194
+ }
195
+ applyHover(objectId);
196
+ };
197
+ const handleFocusOut = () => {
198
+ applyHover(null);
199
+ };
200
+ const handleKeyDown = (event) => {
201
+ if (!behavior.keyboard || destroyed) {
202
+ return;
203
+ }
204
+ const objectId = getClosestObjectId(event.target);
205
+ if ((event.key === "Enter" || event.key === " ") && objectId) {
206
+ event.preventDefault();
207
+ applySelection(objectId);
208
+ if (behavior.tooltipMode === "pinned") {
209
+ pinnedTooltipObjectId = objectId;
210
+ updateTooltip();
211
+ }
212
+ return;
213
+ }
214
+ switch (event.key) {
215
+ case "Escape":
216
+ if (behavior.tooltipMode === "pinned" && pinnedTooltipObjectId) {
217
+ event.preventDefault();
218
+ pinnedTooltipObjectId = null;
219
+ updateTooltip();
220
+ }
221
+ return;
222
+ case "+":
223
+ case "=":
224
+ event.preventDefault();
225
+ api.zoomBy(behavior.zoomStep);
226
+ return;
227
+ case "-":
228
+ event.preventDefault();
229
+ api.zoomBy(1 / behavior.zoomStep);
230
+ return;
231
+ case "ArrowLeft":
232
+ event.preventDefault();
233
+ api.panBy(-behavior.panStep, 0);
234
+ return;
235
+ case "ArrowRight":
236
+ event.preventDefault();
237
+ api.panBy(behavior.panStep, 0);
238
+ return;
239
+ case "ArrowUp":
240
+ event.preventDefault();
241
+ api.panBy(0, -behavior.panStep);
242
+ return;
243
+ case "ArrowDown":
244
+ event.preventDefault();
245
+ api.panBy(0, behavior.panStep);
246
+ return;
247
+ case "[":
248
+ event.preventDefault();
249
+ api.rotateBy(-behavior.rotationStep);
250
+ return;
251
+ case "]":
252
+ event.preventDefault();
253
+ api.rotateBy(behavior.rotationStep);
254
+ return;
255
+ case "f":
256
+ case "F":
257
+ event.preventDefault();
258
+ api.fitToSystem();
259
+ return;
260
+ case "0":
261
+ event.preventDefault();
262
+ api.resetView();
263
+ return;
264
+ }
265
+ };
266
+ container.addEventListener("wheel", handleWheel, { passive: false });
267
+ container.addEventListener("pointerdown", handlePointerDown);
268
+ container.addEventListener("pointermove", handlePointerMove);
269
+ container.addEventListener("pointerup", handlePointerEnd);
270
+ container.addEventListener("pointercancel", handlePointerEnd);
271
+ container.addEventListener("click", handleClick);
272
+ container.addEventListener("mouseover", handleMouseOver);
273
+ container.addEventListener("mouseleave", handleMouseLeave);
274
+ container.addEventListener("focusin", handleFocusIn);
275
+ container.addEventListener("focusout", handleFocusOut);
276
+ container.addEventListener("keydown", handleKeyDown);
277
+ const api = {
278
+ setSource(source) {
279
+ currentInput = { kind: "source", value: source };
280
+ scene = renderSceneFromInput(currentInput, renderOptions);
281
+ activeViewpointId = null;
282
+ rerenderScene(true);
283
+ },
284
+ setDocument(document) {
285
+ currentInput = { kind: "document", value: document };
286
+ scene = renderSceneFromInput(currentInput, renderOptions);
287
+ activeViewpointId = null;
288
+ rerenderScene(true);
289
+ },
290
+ setScene(nextScene) {
291
+ currentInput = { kind: "scene", value: nextScene };
292
+ scene = nextScene;
293
+ activeViewpointId = null;
294
+ rerenderScene(true);
295
+ },
296
+ getScene() {
297
+ return scene;
298
+ },
299
+ getRenderOptions() {
300
+ return cloneRenderOptions(renderOptions);
301
+ },
302
+ listViewpoints() {
303
+ return scene.viewpoints.slice();
304
+ },
305
+ getActiveViewpoint() {
306
+ return getViewpointById(activeViewpointId);
307
+ },
308
+ goToViewpoint(id) {
309
+ const viewpoint = getViewpointById(id);
310
+ if (!viewpoint) {
311
+ return false;
312
+ }
313
+ const nextRenderOptions = {};
314
+ const viewpointLayers = sceneViewpointToLayerOptions(viewpoint);
315
+ if (viewpoint.preset !== null) {
316
+ nextRenderOptions.preset = viewpoint.preset;
317
+ }
318
+ if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
319
+ nextRenderOptions.projection = viewpoint.projection;
320
+ }
321
+ if (viewpointLayers) {
322
+ nextRenderOptions.layers = viewpointLayers;
323
+ }
324
+ activeViewpointId = viewpoint.id;
325
+ if (Object.keys(nextRenderOptions).length > 0) {
326
+ const sceneAffecting = hasSceneAffectingRenderOptions(nextRenderOptions);
327
+ renderOptions = mergeRenderOptions(renderOptions, nextRenderOptions);
328
+ if (currentInput.kind !== "scene" && sceneAffecting) {
329
+ scene = renderSceneFromInput(currentInput, renderOptions);
330
+ }
331
+ rerenderScene(sceneAffecting);
332
+ }
333
+ setFilterInternal(viewpointToViewerFilter(viewpoint), false, false);
334
+ const nextState = createViewpointState(viewpoint);
335
+ updateState(nextState);
336
+ applySelection(viewpoint.selectedObjectId ?? viewpoint.objectId ?? null, false);
337
+ options.onSelectionChange?.(getSelectedObject());
338
+ options.onSelectionDetailsChange?.(buildObjectDetails(state.selectedObjectId));
339
+ notifyViewpointChange();
340
+ emitAtlasStateChange();
341
+ return true;
342
+ },
343
+ search(query, limit = 12) {
344
+ return searchSceneObjects(scene, query, limit);
345
+ },
346
+ getFilter() {
347
+ return renderOptions.filter ? { ...renderOptions.filter } : null;
348
+ },
349
+ setFilter(filter) {
350
+ setFilterInternal(filter, true, true);
351
+ },
352
+ getVisibleObjects() {
353
+ return getVisibleSceneObjects();
354
+ },
355
+ getFocusPath(id) {
356
+ return buildFocusPath(id);
357
+ },
358
+ getObjectDetails(id) {
359
+ return buildObjectDetails(id);
360
+ },
361
+ getSelectionDetails() {
362
+ return buildObjectDetails(state.selectedObjectId);
363
+ },
364
+ getTooltipDetails() {
365
+ return activeTooltipDetails;
366
+ },
367
+ getAtlasState() {
368
+ return createAtlasStateSnapshot(state, renderOptions, renderOptions.filter ?? null, activeViewpointId);
369
+ },
370
+ setAtlasState(nextAtlasState) {
371
+ const atlasState = typeof nextAtlasState === "string"
372
+ ? deserializeViewerAtlasState(nextAtlasState)
373
+ : nextAtlasState;
374
+ if (atlasState.viewpointId) {
375
+ api.goToViewpoint(atlasState.viewpointId);
376
+ }
377
+ api.setRenderOptions(atlasState.renderOptions);
378
+ setFilterInternal(atlasState.filter ?? null, false, false);
379
+ updateState(sanitizeState({ ...state, ...atlasState.viewerState }));
380
+ applySelection(atlasState.viewerState.selectedObjectId ?? null, false);
381
+ notifyViewpointChange();
382
+ options.onSelectionChange?.(getSelectedObject());
383
+ options.onSelectionDetailsChange?.(buildObjectDetails(state.selectedObjectId));
384
+ emitAtlasStateChange();
385
+ },
386
+ serializeAtlasState() {
387
+ return serializeViewerAtlasState(api.getAtlasState());
388
+ },
389
+ captureBookmark(name, label) {
390
+ return createViewerBookmark(name, label, api.getAtlasState());
391
+ },
392
+ applyBookmark(bookmark) {
393
+ if (typeof bookmark === "string") {
394
+ api.setAtlasState(bookmark);
395
+ return true;
396
+ }
397
+ api.setAtlasState(bookmark.atlasState);
398
+ return true;
399
+ },
400
+ setRenderOptions(options) {
401
+ const sceneAffecting = hasSceneAffectingRenderOptions(options);
402
+ renderOptions = mergeRenderOptions(renderOptions, options);
403
+ if (currentInput.kind !== "scene" && sceneAffecting) {
404
+ scene = renderSceneFromInput(currentInput, renderOptions);
405
+ }
406
+ rerenderScene(sceneAffecting);
407
+ },
408
+ getState() {
409
+ return { ...state };
410
+ },
411
+ setState(nextState) {
412
+ updateState(sanitizeState({ ...state, ...nextState }));
413
+ },
414
+ zoomBy(factor, anchor) {
415
+ updateState(zoomViewerStateAt(scene, state, factor, anchor ?? { x: scene.width / 2, y: scene.height / 2 }, constraints));
416
+ },
417
+ panBy(dx, dy) {
418
+ updateState(panViewerState(state, dx, dy));
419
+ },
420
+ rotateBy(deg) {
421
+ updateState(rotateViewerState(state, deg));
422
+ },
423
+ fitToSystem() {
424
+ updateState(fitViewerState(scene, state, constraints));
425
+ },
426
+ focusObject(id) {
427
+ activeViewpointId = null;
428
+ updateState(focusViewerState(scene, state, id, constraints));
429
+ applySelection(id);
430
+ if (behavior.tooltipMode === "pinned") {
431
+ pinnedTooltipObjectId = getObjectById(id)?.objectId ?? null;
432
+ updateTooltip();
433
+ }
434
+ },
435
+ pinTooltip(id) {
436
+ pinnedTooltipObjectId = getObjectById(id)?.objectId ?? null;
437
+ updateTooltip();
438
+ },
439
+ resetView() {
440
+ const resetState = fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints);
441
+ activeViewpointId = null;
442
+ updateState(resetState);
443
+ applySelection(null);
444
+ pinnedTooltipObjectId = null;
445
+ updateTooltip();
446
+ },
447
+ exportSvg() {
448
+ return renderSceneToSvg(scene, {
449
+ ...renderOptions,
450
+ filter: renderOptions.filter ?? null,
451
+ selectedObjectId: state.selectedObjectId,
452
+ });
453
+ },
454
+ destroy() {
455
+ if (destroyed) {
456
+ return;
457
+ }
458
+ destroyed = true;
459
+ container.removeEventListener("wheel", handleWheel);
460
+ container.removeEventListener("pointerdown", handlePointerDown);
461
+ container.removeEventListener("pointermove", handlePointerMove);
462
+ container.removeEventListener("pointerup", handlePointerEnd);
463
+ container.removeEventListener("pointercancel", handlePointerEnd);
464
+ container.removeEventListener("click", handleClick);
465
+ container.removeEventListener("mouseover", handleMouseOver);
466
+ container.removeEventListener("mouseleave", handleMouseLeave);
467
+ container.removeEventListener("focusin", handleFocusIn);
468
+ container.removeEventListener("focusout", handleFocusOut);
469
+ container.removeEventListener("keydown", handleKeyDown);
470
+ tooltipRoot?.remove();
471
+ tooltipRoot = null;
472
+ minimapRoot?.remove();
473
+ minimapRoot = null;
474
+ container.classList.remove("wo-viewer-container");
475
+ container.style.touchAction = previousTouchAction;
476
+ container.style.position = previousPosition;
477
+ if (previousTabIndex === null) {
478
+ container.removeAttribute("tabindex");
479
+ }
480
+ else {
481
+ container.setAttribute("tabindex", previousTabIndex);
482
+ }
483
+ },
484
+ };
485
+ rerenderScene(true);
486
+ if (options.initialViewpointId) {
487
+ api.goToViewpoint(options.initialViewpointId);
488
+ }
489
+ else if (options.initialSelectionObjectId) {
490
+ api.focusObject(options.initialSelectionObjectId);
491
+ }
492
+ else {
493
+ emitAtlasStateChange();
494
+ }
495
+ return api;
496
+ function rerenderScene(resetView) {
497
+ container.innerHTML = renderSceneToSvg(scene, {
498
+ ...renderOptions,
499
+ filter: renderOptions.filter ?? null,
500
+ selectedObjectId: state.selectedObjectId,
501
+ });
502
+ svgElement = container.querySelector('[data-worldorbit-svg="true"]');
503
+ cameraRoot = container.querySelector("#worldorbit-camera-root");
504
+ minimapRoot = null;
505
+ tooltipRoot = null;
506
+ if (behavior.minimap) {
507
+ minimapRoot = document.createElement("div");
508
+ minimapRoot.dataset.worldorbitMinimapRoot = "true";
509
+ container.append(minimapRoot);
510
+ }
511
+ if (behavior.tooltipMode !== "disabled") {
512
+ tooltipRoot = document.createElement("div");
513
+ tooltipRoot.className = "wo-viewer-tooltip-root";
514
+ tooltipRoot.dataset.worldorbitTooltip = "true";
515
+ tooltipRoot.hidden = true;
516
+ tooltipRoot.addEventListener("click", handleTooltipClick);
517
+ container.append(tooltipRoot);
518
+ }
519
+ if (!svgElement || !cameraRoot) {
520
+ throw new Error("Interactive viewer could not locate the rendered SVG camera root.");
521
+ }
522
+ state = resetView
523
+ ? fitViewerState(scene, { ...DEFAULT_VIEWER_STATE }, constraints)
524
+ : sanitizeState(state);
525
+ applySelection(state.selectedObjectId &&
526
+ getObjectById(state.selectedObjectId)
527
+ ? state.selectedObjectId
528
+ : null, false);
529
+ applyHover(hoveredObjectId &&
530
+ getObjectById(hoveredObjectId)
531
+ ? hoveredObjectId
532
+ : null, false);
533
+ pinnedTooltipObjectId =
534
+ pinnedTooltipObjectId && getObjectById(pinnedTooltipObjectId)
535
+ ? pinnedTooltipObjectId
536
+ : null;
537
+ updateCameraTransform();
538
+ notifyFilterChange();
539
+ notifyViewpointChange();
540
+ options.onViewChange?.({ ...state });
541
+ emitAtlasStateChange();
542
+ }
543
+ function updateState(nextState) {
544
+ state = sanitizeState(nextState);
545
+ updateCameraTransform();
546
+ options.onViewChange?.({ ...state });
547
+ emitAtlasStateChange();
548
+ }
549
+ function sanitizeState(nextState) {
550
+ return {
551
+ scale: clampValue(nextState.scale, constraints.minScale, constraints.maxScale),
552
+ rotationDeg: normalizeRotation(nextState.rotationDeg),
553
+ translateX: Number.isFinite(nextState.translateX) ? nextState.translateX : state.translateX,
554
+ translateY: Number.isFinite(nextState.translateY) ? nextState.translateY : state.translateY,
555
+ selectedObjectId: nextState.selectedObjectId && getObjectById(nextState.selectedObjectId)
556
+ ? nextState.selectedObjectId
557
+ : null,
558
+ };
559
+ }
560
+ function updateCameraTransform() {
561
+ if (!cameraRoot) {
562
+ return;
563
+ }
564
+ cameraRoot.setAttribute("transform", composeViewerTransform(scene, state));
565
+ updateMinimap();
566
+ updateTooltip();
567
+ }
568
+ function applySelection(objectId, emitCallback = true) {
569
+ if (state.selectedObjectId) {
570
+ container
571
+ .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
572
+ ?.classList.remove("wo-object-selected");
573
+ }
574
+ state = {
575
+ ...state,
576
+ selectedObjectId: objectId && getObjectById(objectId)
577
+ ? objectId
578
+ : null,
579
+ };
580
+ if (state.selectedObjectId) {
581
+ container
582
+ .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
583
+ ?.classList.add("wo-object-selected");
584
+ }
585
+ syncAtlasHighlights();
586
+ updateTooltip();
587
+ if (emitCallback) {
588
+ options.onSelectionChange?.(getSelectedObject());
589
+ options.onSelectionDetailsChange?.(buildObjectDetails(state.selectedObjectId));
590
+ options.onViewChange?.({ ...state });
591
+ emitAtlasStateChange();
592
+ }
593
+ }
594
+ function applyHover(objectId, emitCallback = true) {
595
+ if (hoveredObjectId === objectId && emitCallback) {
596
+ return;
597
+ }
598
+ hoveredObjectId =
599
+ objectId && getObjectById(objectId)
600
+ ? objectId
601
+ : null;
602
+ syncAtlasHighlights();
603
+ updateTooltip();
604
+ if (emitCallback) {
605
+ options.onHoverChange?.(getObjectById(hoveredObjectId));
606
+ options.onHoverDetailsChange?.(buildObjectDetails(hoveredObjectId));
607
+ }
608
+ }
609
+ function getSelectedObject() {
610
+ return getObjectById(state.selectedObjectId);
611
+ }
612
+ function getObjectById(objectId) {
613
+ if (!objectId) {
614
+ return null;
615
+ }
616
+ const visibleObjectIds = getVisibleObjectIds();
617
+ return (scene.objects.find((object) => object.objectId === objectId &&
618
+ !object.hidden &&
619
+ visibleObjectIds.has(object.objectId)) ?? null);
620
+ }
621
+ function buildObjectDetails(objectId) {
622
+ const renderObject = getObjectById(objectId);
623
+ if (!renderObject) {
624
+ return null;
625
+ }
626
+ return {
627
+ objectId: renderObject.objectId,
628
+ object: renderObject.object,
629
+ renderObject,
630
+ label: scene.labels.find((label) => label.objectId === renderObject.objectId && !label.hidden) ?? null,
631
+ group: scene.groups.find((group) => group.renderId === renderObject.groupId) ?? null,
632
+ orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
633
+ relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden &&
634
+ (orbit.objectId === renderObject.objectId ||
635
+ renderObject.ancestorIds.includes(orbit.objectId) ||
636
+ renderObject.childIds.includes(orbit.objectId))),
637
+ parent: getObjectById(renderObject.parentId),
638
+ children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
639
+ ancestors: renderObject.ancestorIds
640
+ .map((ancestorId) => getObjectById(ancestorId))
641
+ .filter(Boolean),
642
+ focusPath: buildFocusPath(renderObject.objectId),
643
+ };
644
+ }
645
+ function syncAtlasHighlights() {
646
+ 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")) {
647
+ element.classList.remove("wo-chain-selected", "wo-chain-hover", "wo-ancestor-selected", "wo-ancestor-hover", "wo-orbit-related-selected", "wo-orbit-related-hover");
648
+ }
649
+ applyChainClasses(state.selectedObjectId, {
650
+ objectClass: "wo-chain-selected",
651
+ ancestorClass: "wo-ancestor-selected",
652
+ orbitClass: "wo-orbit-related-selected",
653
+ });
654
+ applyChainClasses(hoveredObjectId, {
655
+ objectClass: "wo-chain-hover",
656
+ ancestorClass: "wo-ancestor-hover",
657
+ orbitClass: "wo-orbit-related-hover",
658
+ });
659
+ }
660
+ function applyChainClasses(objectId, classes) {
661
+ const details = buildObjectDetails(objectId);
662
+ if (!details) {
663
+ return;
664
+ }
665
+ const chainIds = new Set([
666
+ details.objectId,
667
+ ...details.renderObject.childIds,
668
+ ...details.renderObject.ancestorIds,
669
+ ]);
670
+ for (const id of chainIds) {
671
+ for (const element of container.querySelectorAll(`[data-object-id="${cssEscape(id)}"]`)) {
672
+ element.classList.add(classes.objectClass);
673
+ }
674
+ }
675
+ for (const ancestor of details.ancestors) {
676
+ for (const element of container.querySelectorAll(`[data-object-id="${cssEscape(ancestor.objectId)}"]`)) {
677
+ element.classList.add(classes.ancestorClass);
678
+ }
679
+ }
680
+ for (const orbit of details.relatedOrbits) {
681
+ for (const element of container.querySelectorAll(`[data-orbit-object-id="${cssEscape(orbit.objectId)}"]`)) {
682
+ element.classList.add(classes.orbitClass);
683
+ }
684
+ }
685
+ }
686
+ function getViewportPointFromClient(clientX, clientY) {
687
+ if (!svgElement) {
688
+ return {
689
+ x: scene.width / 2,
690
+ y: scene.height / 2,
691
+ };
692
+ }
693
+ const rect = svgElement.getBoundingClientRect();
694
+ if (!rect.width || !rect.height) {
695
+ return {
696
+ x: scene.width / 2,
697
+ y: scene.height / 2,
698
+ };
699
+ }
700
+ return {
701
+ x: ((clientX - rect.left) / rect.width) * scene.width,
702
+ y: ((clientY - rect.top) / rect.height) * scene.height,
703
+ };
704
+ }
705
+ function getWorldPointFromClient(clientX, clientY) {
706
+ return invertViewerPoint(scene, state, getViewportPointFromClient(clientX, clientY));
707
+ }
708
+ function getVisibleObjectIds() {
709
+ return computeVisibleObjectIds(scene, renderOptions.filter ?? null);
710
+ }
711
+ function getVisibleSceneObjects() {
712
+ const visibleObjectIds = getVisibleObjectIds();
713
+ return scene.objects.filter((object) => !object.hidden && visibleObjectIds.has(object.objectId));
714
+ }
715
+ function buildFocusPath(objectId) {
716
+ const object = scene.objects.find((entry) => entry.objectId === objectId && !entry.hidden);
717
+ if (!object) {
718
+ return [];
719
+ }
720
+ return [...object.ancestorIds, object.objectId]
721
+ .map((entryId) => getObjectById(entryId))
722
+ .filter(Boolean);
723
+ }
724
+ function getViewpointById(id) {
725
+ return scene.viewpoints.find((viewpoint) => viewpoint.id === id) ?? null;
726
+ }
727
+ function createViewpointState(viewpoint) {
728
+ const rotationDeg = normalizeRotation(viewpoint.rotationDeg);
729
+ const scale = viewpoint.scale !== null && viewpoint.scale !== undefined
730
+ ? clampValue(viewpoint.scale, constraints.minScale, constraints.maxScale)
731
+ : null;
732
+ const targetObject = viewpoint.objectId &&
733
+ scene.objects.find((object) => object.objectId === viewpoint.objectId && !object.hidden);
734
+ if (targetObject) {
735
+ return createCenteredState({ x: targetObject.x, y: targetObject.y }, scale ?? Math.max(1.8, DEFAULT_VIEWER_STATE.scale), rotationDeg, viewpoint.selectedObjectId ?? targetObject.objectId);
736
+ }
737
+ const baseState = fitViewerState(scene, { ...DEFAULT_VIEWER_STATE, rotationDeg }, constraints);
738
+ if (scale === null) {
739
+ return {
740
+ ...baseState,
741
+ rotationDeg,
742
+ selectedObjectId: viewpoint.selectedObjectId ?? null,
743
+ };
744
+ }
745
+ return createCenteredState({
746
+ x: scene.contentBounds.centerX,
747
+ y: scene.contentBounds.centerY,
748
+ }, scale, rotationDeg, viewpoint.selectedObjectId ?? null);
749
+ }
750
+ function createCenteredState(target, scale, rotationDeg, selectedObjectId) {
751
+ const center = {
752
+ x: scene.width / 2,
753
+ y: scene.height / 2,
754
+ };
755
+ const rotatedTarget = rotatePoint(target, center, rotationDeg);
756
+ return {
757
+ scale,
758
+ rotationDeg,
759
+ translateX: center.x - (center.x + (rotatedTarget.x - center.x) * scale),
760
+ translateY: center.y - (center.y + (rotatedTarget.y - center.y) * scale),
761
+ selectedObjectId,
762
+ };
763
+ }
764
+ function setFilterInternal(filter, emitCallbacks, clearActiveViewpoint) {
765
+ renderOptions = {
766
+ ...renderOptions,
767
+ filter: normalizeViewerFilter(filter),
768
+ };
769
+ if (clearActiveViewpoint) {
770
+ activeViewpointId = null;
771
+ }
772
+ rerenderScene(false);
773
+ if (!emitCallbacks) {
774
+ return;
775
+ }
776
+ }
777
+ function notifyFilterChange() {
778
+ options.onFilterChange?.(renderOptions.filter ?? null, getVisibleSceneObjects());
779
+ }
780
+ function notifyViewpointChange() {
781
+ options.onViewpointChange?.(getViewpointById(activeViewpointId));
782
+ }
783
+ function emitAtlasStateChange() {
784
+ options.onAtlasStateChange?.(api.getAtlasState());
785
+ }
786
+ function updateMinimap() {
787
+ if (!behavior.minimap || !minimapRoot) {
788
+ return;
789
+ }
790
+ minimapRoot.innerHTML = renderViewerMinimap(scene, state, getVisibleSceneObjects());
791
+ }
792
+ function updateTooltip() {
793
+ if (behavior.tooltipMode === "disabled" || !tooltipRoot) {
794
+ setTooltipDetails(null);
795
+ return;
796
+ }
797
+ const resolved = resolveTooltipTarget();
798
+ if (!resolved) {
799
+ tooltipRoot.hidden = true;
800
+ tooltipRoot.innerHTML = "";
801
+ tooltipRoot.removeAttribute("data-mode");
802
+ setTooltipDetails(null);
803
+ return;
804
+ }
805
+ const details = buildObjectDetails(resolved.objectId);
806
+ if (!details) {
807
+ tooltipRoot.hidden = true;
808
+ tooltipRoot.innerHTML = "";
809
+ tooltipRoot.removeAttribute("data-mode");
810
+ setTooltipDetails(null);
811
+ return;
812
+ }
813
+ const tooltipDetails = buildViewerTooltipDetails(details);
814
+ activeTooltipObjectId = resolved.objectId;
815
+ tooltipRoot.hidden = false;
816
+ tooltipRoot.dataset.mode = resolved.mode;
817
+ tooltipRoot.classList.toggle("is-pinned", resolved.mode === "pinned");
818
+ tooltipRoot.style.pointerEvents = "auto";
819
+ tooltipRoot.style.visibility = "hidden";
820
+ renderTooltipContent(tooltipRoot, tooltipDetails, resolved.mode);
821
+ positionTooltip(tooltipRoot, details.renderObject);
822
+ tooltipRoot.style.visibility = "visible";
823
+ setTooltipDetails(tooltipDetails);
824
+ }
825
+ function resolveTooltipTarget() {
826
+ if (pinnedTooltipObjectId && getObjectById(pinnedTooltipObjectId)) {
827
+ return {
828
+ objectId: pinnedTooltipObjectId,
829
+ mode: "pinned",
830
+ };
831
+ }
832
+ if (hoveredObjectId && getObjectById(hoveredObjectId)) {
833
+ return {
834
+ objectId: hoveredObjectId,
835
+ mode: "hover",
836
+ };
837
+ }
838
+ return null;
839
+ }
840
+ function renderTooltipContent(element, details, mode) {
841
+ const customMarkup = options.tooltipRenderer?.(details, mode);
842
+ element.innerHTML = "";
843
+ if (typeof customMarkup === "string") {
844
+ element.innerHTML = customMarkup;
845
+ }
846
+ else if (customMarkup instanceof HTMLElement) {
847
+ element.append(customMarkup);
848
+ }
849
+ else {
850
+ element.innerHTML = renderDefaultTooltipContent(details, mode);
851
+ }
852
+ const actions = document.createElement("div");
853
+ actions.className = "wo-tooltip-actions";
854
+ if (mode === "pinned") {
855
+ const unpinButton = document.createElement("button");
856
+ unpinButton.type = "button";
857
+ unpinButton.className = "wo-tooltip-action";
858
+ unpinButton.dataset.tooltipAction = "unpin";
859
+ unpinButton.textContent = "Unpin";
860
+ actions.append(unpinButton);
861
+ }
862
+ else {
863
+ const pinButton = document.createElement("button");
864
+ pinButton.type = "button";
865
+ pinButton.className = "wo-tooltip-action";
866
+ pinButton.dataset.tooltipAction = "pin";
867
+ pinButton.dataset.objectId = details.objectId;
868
+ pinButton.textContent = "Pin";
869
+ actions.append(pinButton);
870
+ }
871
+ if (actions.childElementCount > 0) {
872
+ element.append(actions);
873
+ }
874
+ }
875
+ function positionTooltip(element, renderObject) {
876
+ if (!svgElement) {
877
+ return;
878
+ }
879
+ const anchor = {
880
+ x: renderObject.anchorX ?? renderObject.x,
881
+ y: renderObject.anchorY ??
882
+ renderObject.y - Math.max(renderObject.visualRadius, renderObject.radius),
883
+ };
884
+ const viewportPoint = projectWorldPoint(anchor);
885
+ const svgRect = svgElement.getBoundingClientRect();
886
+ const containerRect = container.getBoundingClientRect();
887
+ const pointX = svgRect.left -
888
+ containerRect.left +
889
+ (viewportPoint.x / Math.max(scene.width, 1)) * svgRect.width;
890
+ const pointY = svgRect.top -
891
+ containerRect.top +
892
+ (viewportPoint.y / Math.max(scene.height, 1)) * svgRect.height;
893
+ const maxLeft = Math.max(container.clientWidth - element.offsetWidth - 12, 12);
894
+ const maxTop = Math.max(container.clientHeight - element.offsetHeight - 12, 12);
895
+ const preferAbove = pointY > container.clientHeight * 0.48;
896
+ const nextLeft = clampValue(pointX + 18, 12, maxLeft);
897
+ const nextTop = clampValue(preferAbove ? pointY - element.offsetHeight - 18 : pointY + 18, 12, maxTop);
898
+ element.style.left = `${nextLeft}px`;
899
+ element.style.top = `${nextTop}px`;
900
+ }
901
+ function projectWorldPoint(point) {
902
+ const center = {
903
+ x: scene.width / 2,
904
+ y: scene.height / 2,
905
+ };
906
+ const rotated = rotatePoint(point, center, state.rotationDeg);
907
+ return {
908
+ x: center.x + (rotated.x - center.x) * state.scale + state.translateX,
909
+ y: center.y + (rotated.y - center.y) * state.scale + state.translateY,
910
+ };
911
+ }
912
+ function handleTooltipClick(event) {
913
+ const target = event.target?.closest("[data-tooltip-action]");
914
+ if (!target) {
915
+ return;
916
+ }
917
+ event.preventDefault();
918
+ event.stopPropagation();
919
+ switch (target.dataset.tooltipAction) {
920
+ case "pin":
921
+ pinnedTooltipObjectId = target.dataset.objectId ?? activeTooltipObjectId;
922
+ break;
923
+ case "unpin":
924
+ pinnedTooltipObjectId = null;
925
+ break;
926
+ }
927
+ updateTooltip();
928
+ }
929
+ function setTooltipDetails(details) {
930
+ const changed = activeTooltipDetails?.objectId !== details?.objectId ||
931
+ activeTooltipDetails?.description !== details?.description ||
932
+ activeTooltipDetails?.imageHref !== details?.imageHref;
933
+ activeTooltipDetails = details;
934
+ activeTooltipObjectId = details?.objectId ?? null;
935
+ if (changed) {
936
+ options.onTooltipChange?.(details);
937
+ }
938
+ }
939
+ }
940
+ function resolveInitialInput(options) {
941
+ if (options.scene) {
942
+ return { kind: "scene", value: options.scene };
943
+ }
944
+ if (options.document) {
945
+ return { kind: "document", value: options.document };
946
+ }
947
+ if (options.source) {
948
+ return { kind: "source", value: options.source };
949
+ }
950
+ throw new Error("Interactive viewer requires an initial render input.");
951
+ }
952
+ function renderSceneFromInput(input, renderOptions) {
953
+ switch (input.kind) {
954
+ case "scene":
955
+ return input.value;
956
+ case "document":
957
+ return renderDocumentToScene(input.value, renderOptions);
958
+ case "source": {
959
+ const loaded = loadWorldOrbitSource(input.value);
960
+ return renderDocumentToScene(loaded.document, resolveSourceRenderOptions(loaded, renderOptions));
961
+ }
962
+ }
963
+ }
964
+ function cloneRenderOptions(renderOptions) {
965
+ return {
966
+ ...renderOptions,
967
+ filter: renderOptions.filter ? { ...renderOptions.filter } : undefined,
968
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : undefined,
969
+ layers: renderOptions.layers ? { ...renderOptions.layers } : undefined,
970
+ theme: renderOptions.theme && typeof renderOptions.theme === "object"
971
+ ? { ...renderOptions.theme }
972
+ : renderOptions.theme,
973
+ };
974
+ }
975
+ function mergeRenderOptions(current, next) {
976
+ return {
977
+ ...current,
978
+ ...next,
979
+ filter: next.filter !== undefined
980
+ ? normalizeViewerFilter(next.filter)
981
+ : current.filter
982
+ ? { ...current.filter }
983
+ : undefined,
984
+ scaleModel: next.scaleModel
985
+ ? {
986
+ ...(current.scaleModel ?? {}),
987
+ ...next.scaleModel,
988
+ }
989
+ : current.scaleModel
990
+ ? { ...current.scaleModel }
991
+ : undefined,
992
+ layers: next.layers
993
+ ? {
994
+ ...(current.layers ?? {}),
995
+ ...next.layers,
996
+ }
997
+ : current.layers
998
+ ? { ...current.layers }
999
+ : undefined,
1000
+ theme: next.theme && typeof next.theme === "object"
1001
+ ? { ...next.theme }
1002
+ : next.theme ?? current.theme,
1003
+ };
1004
+ }
1005
+ function hasSceneAffectingRenderOptions(options) {
1006
+ return (options.width !== undefined ||
1007
+ options.height !== undefined ||
1008
+ options.padding !== undefined ||
1009
+ options.preset !== undefined ||
1010
+ options.projection !== undefined ||
1011
+ options.scaleModel !== undefined);
1012
+ }
1013
+ function resolveSourceRenderOptions(loaded, renderOptions) {
1014
+ const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
1015
+ if (renderOptions.preset || !atlasDocument?.system?.defaults.preset) {
1016
+ return renderOptions;
1017
+ }
1018
+ return {
1019
+ ...renderOptions,
1020
+ preset: atlasDocument.system.defaults.preset,
1021
+ };
1022
+ }
1023
+ function createTouchGestureState(scene, state, touchPoints) {
1024
+ const { center, distance } = getTouchCenterAndDistance(touchPoints);
1025
+ return {
1026
+ startState: { ...state },
1027
+ startCenter: invertViewerPoint(scene, state, center),
1028
+ startViewportCenter: center,
1029
+ startDistance: distance,
1030
+ };
1031
+ }
1032
+ function getTouchCenterAndDistance(touchPoints) {
1033
+ const points = [...touchPoints.values()];
1034
+ if (points.length < 2) {
1035
+ return {
1036
+ center: points[0] ?? { x: 0, y: 0 },
1037
+ distance: 1,
1038
+ };
1039
+ }
1040
+ const [first, second] = points;
1041
+ return {
1042
+ center: {
1043
+ x: (first.x + second.x) / 2,
1044
+ y: (first.y + second.y) / 2,
1045
+ },
1046
+ distance: Math.hypot(second.x - first.x, second.y - first.y),
1047
+ };
1048
+ }
1049
+ function getClosestObjectId(target) {
1050
+ if (!(target instanceof Element)) {
1051
+ return null;
1052
+ }
1053
+ return target.closest("[data-object-id]")?.dataset.objectId ?? null;
1054
+ }
1055
+ function ensureBrowserEnvironment(container) {
1056
+ if (typeof window === "undefined" || typeof document === "undefined") {
1057
+ throw new Error("createInteractiveViewer can only run in a browser environment.");
1058
+ }
1059
+ if (!(container instanceof HTMLElement)) {
1060
+ throw new Error("Interactive viewer requires an HTMLElement container.");
1061
+ }
1062
+ }
1063
+ function clampValue(value, min, max) {
1064
+ return Math.min(Math.max(value, min), max);
1065
+ }
1066
+ function normalizeRotation(rotationDeg) {
1067
+ let normalized = rotationDeg % 360;
1068
+ if (normalized > 180) {
1069
+ normalized -= 360;
1070
+ }
1071
+ if (normalized <= -180) {
1072
+ normalized += 360;
1073
+ }
1074
+ return normalized;
1075
+ }
1076
+ function cssEscape(value) {
1077
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") {
1078
+ return CSS.escape(value);
1079
+ }
1080
+ return value.replace(/["\\]/g, "\\$&");
1081
+ }
1082
+ function installViewerTooltipStyles() {
1083
+ if (typeof document === "undefined" || document.getElementById(TOOLTIP_STYLE_ID)) {
1084
+ return;
1085
+ }
1086
+ const style = document.createElement("style");
1087
+ style.id = TOOLTIP_STYLE_ID;
1088
+ style.textContent = `
1089
+ .wo-viewer-tooltip-root {
1090
+ position: absolute;
1091
+ z-index: 12;
1092
+ min-width: 220px;
1093
+ max-width: min(320px, calc(100% - 24px));
1094
+ padding: 14px;
1095
+ border-radius: 18px;
1096
+ border: 1px solid rgba(255,255,255,0.1);
1097
+ background: rgba(7, 16, 25, 0.92);
1098
+ box-shadow: 0 18px 32px rgba(0,0,0,0.28);
1099
+ color: #edf6ff;
1100
+ backdrop-filter: blur(12px);
1101
+ font: 500 13px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
1102
+ }
1103
+ .wo-viewer-tooltip-root[data-mode="hover"] { pointer-events: auto; }
1104
+ .wo-viewer-tooltip-root[data-mode="pinned"] { pointer-events: auto; }
1105
+ .wo-tooltip-card { display: grid; gap: 10px; }
1106
+ .wo-tooltip-head { display: grid; grid-template-columns: 52px minmax(0, 1fr); gap: 12px; align-items: center; }
1107
+ .wo-tooltip-heading { display: grid; gap: 3px; }
1108
+ .wo-tooltip-heading strong { font: 700 16px/1.2 "Segoe UI Variable Display", "Segoe UI", sans-serif; }
1109
+ .wo-tooltip-heading span, .wo-tooltip-relations { color: rgba(237, 246, 255, 0.7); }
1110
+ .wo-tooltip-image {
1111
+ width: 52px;
1112
+ height: 52px;
1113
+ object-fit: cover;
1114
+ border-radius: 14px;
1115
+ border: 1px solid rgba(255,255,255,0.12);
1116
+ background: rgba(255,255,255,0.06);
1117
+ }
1118
+ .wo-tooltip-image-placeholder {
1119
+ display: grid;
1120
+ place-items: center;
1121
+ font: 700 18px/1 "Segoe UI Variable Display", "Segoe UI", sans-serif;
1122
+ color: #ffce8a;
1123
+ }
1124
+ .wo-tooltip-description { margin: 0; }
1125
+ .wo-tooltip-tags { display: flex; flex-wrap: wrap; gap: 6px; }
1126
+ .wo-tooltip-tag {
1127
+ padding: 3px 8px;
1128
+ border-radius: 999px;
1129
+ background: rgba(255,255,255,0.08);
1130
+ color: #ffdda9;
1131
+ font: 600 11px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
1132
+ text-transform: uppercase;
1133
+ letter-spacing: 0.06em;
1134
+ }
1135
+ .wo-tooltip-fields { display: grid; gap: 6px; margin: 0; }
1136
+ .wo-tooltip-field {
1137
+ display: grid;
1138
+ grid-template-columns: minmax(0, 1fr) auto;
1139
+ gap: 12px;
1140
+ align-items: baseline;
1141
+ }
1142
+ .wo-tooltip-field dt { color: rgba(237, 246, 255, 0.68); }
1143
+ .wo-tooltip-field dd { margin: 0; font-weight: 600; text-align: right; }
1144
+ .wo-tooltip-actions { display: flex; justify-content: flex-end; margin-top: 10px; }
1145
+ .wo-tooltip-action {
1146
+ border: 1px solid rgba(240, 180, 100, 0.24);
1147
+ border-radius: 999px;
1148
+ background: rgba(240, 180, 100, 0.12);
1149
+ color: #edf6ff;
1150
+ cursor: pointer;
1151
+ padding: 6px 12px;
1152
+ font: 600 12px/1.3 "Segoe UI Variable", "Segoe UI", sans-serif;
1153
+ }
1154
+ `;
1155
+ document.head.append(style);
1156
+ }