worldorbit 3.0.5 → 3.0.6

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.
package/README.md CHANGED
@@ -138,7 +138,7 @@ For direct browser usage, use the browser bundle:
138
138
  import {
139
139
  createInteractiveViewer,
140
140
  loadWorldOrbitSource
141
- } from "https://unpkg.com/worldorbit@3.0.5/dist/unpkg/worldorbit.esm.js";
141
+ } from "https://unpkg.com/worldorbit@3.0.6/dist/unpkg/worldorbit.esm.js";
142
142
 
143
143
  const source = `
144
144
  schema 2.5
@@ -432,7 +432,7 @@ const scene = renderDocumentToScene(loaded.document, {
432
432
 
433
433
  ## Viewer Capabilities
434
434
 
435
- Viewer features in `v3.0.5` include:
435
+ Viewer features in `v3.0.6` include:
436
436
 
437
437
  * scene-based SVG rendering
438
438
  * renderer-neutral spatial scenes through `renderDocumentToSpatialScene(...)`
@@ -67,6 +67,7 @@ export function createWorldOrbitEmbedMarkup(payload, options = {}) {
67
67
  }
68
68
  export function mountWorldOrbitEmbeds(root = document, options = {}) {
69
69
  const viewers = new Map();
70
+ const cleanupCallbacks = [];
70
71
  const elements = [...root.querySelectorAll(EMBED_SELECTOR)];
71
72
  for (const element of elements) {
72
73
  const payload = deserializePayloadFromElement(element);
@@ -82,14 +83,16 @@ export function mountWorldOrbitEmbeds(root = document, options = {}) {
82
83
  const viewMode = options.viewer?.viewMode ??
83
84
  payload.options?.viewMode ??
84
85
  embedModeToViewMode(mode);
86
+ const measureViewport = () => resolveEmbedViewport(element, payload.scene, options);
85
87
  if (mode === "interactive-2d" || mode === "interactive-3d") {
86
88
  try {
89
+ const viewport = measureViewport();
87
90
  const viewer = createInteractiveViewer(element, {
88
91
  ...options.viewer,
89
92
  scene: payload.scene,
90
93
  spatialScene: payload.spatialScene,
91
- width: options.width ?? payload.scene.width,
92
- height: options.height ?? payload.scene.height,
94
+ width: viewport.width,
95
+ height: viewport.height,
93
96
  padding: options.padding ?? payload.scene.padding,
94
97
  preset,
95
98
  theme,
@@ -105,6 +108,13 @@ export function mountWorldOrbitEmbeds(root = document, options = {}) {
105
108
  viewer.setAtlasState(payload.options.atlasState);
106
109
  }
107
110
  viewers.set(element, viewer);
111
+ cleanupCallbacks.push(bindEmbedResize(element, () => {
112
+ const nextViewport = measureViewport();
113
+ viewer.setRenderOptions({
114
+ width: nextViewport.width,
115
+ height: nextViewport.height,
116
+ });
117
+ }));
108
118
  options.onMount?.(viewer, element);
109
119
  }
110
120
  catch (error) {
@@ -118,17 +128,22 @@ export function mountWorldOrbitEmbeds(root = document, options = {}) {
118
128
  }
119
129
  }
120
130
  else {
121
- element.innerHTML = renderSceneToSvg(payload.scene, {
122
- width: options.width ?? payload.scene.width,
123
- height: options.height ?? payload.scene.height,
124
- padding: options.padding ?? payload.scene.padding,
125
- preset,
126
- theme,
127
- layers,
128
- filter: initialFilter,
129
- selectedObjectId: initialSelectionObjectId ?? null,
130
- subtitle,
131
- });
131
+ const renderStaticEmbed = () => {
132
+ const viewport = measureViewport();
133
+ element.innerHTML = renderSceneToSvg(payload.scene, {
134
+ width: viewport.width,
135
+ height: viewport.height,
136
+ padding: options.padding ?? payload.scene.padding,
137
+ preset,
138
+ theme,
139
+ layers,
140
+ filter: initialFilter,
141
+ selectedObjectId: initialSelectionObjectId ?? null,
142
+ subtitle,
143
+ });
144
+ };
145
+ renderStaticEmbed();
146
+ cleanupCallbacks.push(bindEmbedResize(element, renderStaticEmbed));
132
147
  options.onMount?.(null, element);
133
148
  }
134
149
  element.dataset.worldorbitMounted = "true";
@@ -136,14 +151,72 @@ export function mountWorldOrbitEmbeds(root = document, options = {}) {
136
151
  return {
137
152
  viewers: [...viewers.values()],
138
153
  destroy() {
154
+ for (const cleanup of cleanupCallbacks) {
155
+ cleanup();
156
+ }
139
157
  for (const [element, viewer] of viewers.entries()) {
140
158
  viewer.destroy();
141
159
  element.removeAttribute("data-worldorbit-mounted");
142
160
  }
161
+ for (const element of elements) {
162
+ element.removeAttribute("data-worldorbit-mounted");
163
+ }
143
164
  viewers.clear();
144
165
  },
145
166
  };
146
167
  }
168
+ function resolveEmbedViewport(element, scene, options) {
169
+ const rect = element.getBoundingClientRect();
170
+ const width = sanitizeViewportDimension(options.width) ??
171
+ sanitizeViewportDimension(element.clientWidth) ??
172
+ sanitizeViewportDimension(rect.width) ??
173
+ scene.width;
174
+ const explicitHeight = sanitizeViewportDimension(options.height) ??
175
+ sanitizeViewportDimension(element.clientHeight) ??
176
+ sanitizeViewportDimension(rect.height);
177
+ const fallbackHeight = Math.max(Math.round(width * (scene.height / Math.max(scene.width, 1))), Math.min(scene.height, 240));
178
+ return {
179
+ width,
180
+ height: explicitHeight ?? fallbackHeight,
181
+ };
182
+ }
183
+ function sanitizeViewportDimension(value) {
184
+ return typeof value === "number" && Number.isFinite(value) && value > 0
185
+ ? Math.round(value)
186
+ : null;
187
+ }
188
+ function bindEmbedResize(element, callback) {
189
+ let lastWidth = -1;
190
+ let lastHeight = -1;
191
+ const run = () => {
192
+ const rect = element.getBoundingClientRect();
193
+ const nextWidth = Math.round(Math.max(element.clientWidth || rect.width, 0));
194
+ const nextHeight = Math.round(Math.max(element.clientHeight || rect.height, 0));
195
+ if (nextWidth === lastWidth && nextHeight === lastHeight) {
196
+ return;
197
+ }
198
+ lastWidth = nextWidth;
199
+ lastHeight = nextHeight;
200
+ callback();
201
+ };
202
+ run();
203
+ if (typeof ResizeObserver !== "undefined") {
204
+ const observer = new ResizeObserver(() => {
205
+ run();
206
+ });
207
+ observer.observe(element);
208
+ return () => {
209
+ observer.disconnect();
210
+ };
211
+ }
212
+ const handleWindowResize = () => {
213
+ run();
214
+ };
215
+ window.addEventListener("resize", handleWindowResize);
216
+ return () => {
217
+ window.removeEventListener("resize", handleWindowResize);
218
+ };
219
+ }
147
220
  function deserializePayloadFromElement(element) {
148
221
  const serialized = element.dataset.worldorbitPayload;
149
222
  if (!serialized) {
@@ -66,6 +66,7 @@ export function createInteractiveViewer(container, options) {
66
66
  let cameraRoot = null;
67
67
  let runtime3d = null;
68
68
  let minimapRoot = null;
69
+ let labelRoot = null;
69
70
  let tooltipRoot = null;
70
71
  let suppressClick = false;
71
72
  let activePointerId = null;
@@ -90,7 +91,7 @@ export function createInteractiveViewer(container, options) {
90
91
  if (previousTabIndex === null) {
91
92
  container.tabIndex = 0;
92
93
  }
93
- installViewerTooltipStyles();
94
+ installViewerOverlayStyles();
94
95
  container.classList.add("wo-viewer-container");
95
96
  container.style.touchAction = behavior.touch ? "none" : previousTouchAction;
96
97
  if (!container.style.position) {
@@ -671,6 +672,8 @@ export function createInteractiveViewer(container, options) {
671
672
  stopAnimationLoop();
672
673
  runtime3d?.destroy();
673
674
  runtime3d = null;
675
+ labelRoot?.remove();
676
+ labelRoot = null;
674
677
  tooltipRoot?.remove();
675
678
  tooltipRoot = null;
676
679
  minimapRoot?.remove();
@@ -704,6 +707,7 @@ export function createInteractiveViewer(container, options) {
704
707
  svgElement = null;
705
708
  cameraRoot = null;
706
709
  minimapRoot = null;
710
+ labelRoot = null;
707
711
  tooltipRoot = null;
708
712
  if (is3DView()) {
709
713
  spatialScene = spatialScene ?? renderSpatialSceneFromInput(currentInput, renderOptions, providedSpatialScene);
@@ -723,6 +727,10 @@ export function createInteractiveViewer(container, options) {
723
727
  minimapRoot.dataset.worldorbitMinimapRoot = "true";
724
728
  container.append(minimapRoot);
725
729
  }
730
+ labelRoot = document.createElement("div");
731
+ labelRoot.className = "wo-viewer-label-root";
732
+ labelRoot.dataset.worldorbitLabelRoot = "true";
733
+ container.append(labelRoot);
726
734
  if (behavior.tooltipMode !== "disabled") {
727
735
  tooltipRoot = document.createElement("div");
728
736
  tooltipRoot.className = "wo-viewer-tooltip-root";
@@ -734,6 +742,7 @@ export function createInteractiveViewer(container, options) {
734
742
  if (!is3DView() && (!svgElement || !cameraRoot)) {
735
743
  throw new Error("Interactive viewer could not locate the rendered SVG camera root.");
736
744
  }
745
+ suppressStaticLabelLayers();
737
746
  state = resetView
738
747
  ? is3DView()
739
748
  ? { ...DEFAULT_VIEWER_STATE }
@@ -783,14 +792,15 @@ export function createInteractiveViewer(container, options) {
783
792
  return;
784
793
  }
785
794
  cameraRoot.setAttribute("transform", composeViewerTransform(scene, state));
795
+ updateScreenLabels();
786
796
  updateMinimap();
787
797
  updateTooltip();
788
798
  }
789
799
  function applySelection(objectId, emitCallback = true) {
790
800
  if (!is3DView() && state.selectedObjectId) {
791
- container
792
- .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
793
- ?.classList.remove("wo-object-selected");
801
+ for (const element of container.querySelectorAll(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)) {
802
+ element.classList.remove("wo-object-selected");
803
+ }
794
804
  }
795
805
  state = {
796
806
  ...state,
@@ -799,9 +809,9 @@ export function createInteractiveViewer(container, options) {
799
809
  : null,
800
810
  };
801
811
  if (!is3DView() && state.selectedObjectId) {
802
- container
803
- .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
804
- ?.classList.add("wo-object-selected");
812
+ for (const element of container.querySelectorAll(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)) {
813
+ element.classList.add("wo-object-selected");
814
+ }
805
815
  }
806
816
  syncAtlasHighlights();
807
817
  updateTooltip();
@@ -1160,15 +1170,18 @@ export function createInteractiveViewer(container, options) {
1160
1170
  };
1161
1171
  }
1162
1172
  function project2DTooltipPoint(renderObject) {
1163
- if (!svgElement) {
1164
- return null;
1165
- }
1166
1173
  const anchor = {
1167
1174
  x: renderObject.anchorX ?? renderObject.x,
1168
1175
  y: renderObject.anchorY ??
1169
1176
  renderObject.y - Math.max(renderObject.visualRadius, renderObject.radius),
1170
1177
  };
1171
- const viewportPoint = projectWorldPoint(anchor);
1178
+ return project2DScenePointToContainer(anchor);
1179
+ }
1180
+ function project2DScenePointToContainer(point) {
1181
+ if (!svgElement) {
1182
+ return null;
1183
+ }
1184
+ const viewportPoint = projectWorldPoint(point);
1172
1185
  const svgRect = svgElement.getBoundingClientRect();
1173
1186
  const containerRect = container.getBoundingClientRect();
1174
1187
  return {
@@ -1274,9 +1287,151 @@ export function createInteractiveViewer(container, options) {
1274
1287
  state,
1275
1288
  timeSeconds: animationState.timeSeconds,
1276
1289
  });
1290
+ updateScreenLabels();
1277
1291
  updateMinimap();
1278
1292
  updateTooltip();
1279
1293
  }
1294
+ function suppressStaticLabelLayers() {
1295
+ if (is3DView()) {
1296
+ return;
1297
+ }
1298
+ container
1299
+ .querySelector('[data-layer-id="labels"]')
1300
+ ?.setAttribute("display", "none");
1301
+ for (const element of container.querySelectorAll(".wo-event-label")) {
1302
+ element.setAttribute("display", "none");
1303
+ }
1304
+ }
1305
+ function updateScreenLabels() {
1306
+ if (!labelRoot) {
1307
+ return;
1308
+ }
1309
+ const descriptors = buildScreenLabelDescriptors();
1310
+ labelRoot.replaceChildren(...descriptors.map((descriptor) => createScreenLabelElement(descriptor)));
1311
+ labelRoot.hidden = descriptors.length === 0;
1312
+ }
1313
+ function buildScreenLabelDescriptors() {
1314
+ const descriptors = [];
1315
+ const visibleObjectIds = getVisibleObjectIds();
1316
+ if (layerEnabled("labels")) {
1317
+ for (const label of scene.labels) {
1318
+ if (label.hidden || !visibleObjectIds.has(label.objectId)) {
1319
+ continue;
1320
+ }
1321
+ const point = is3DView()
1322
+ ? runtime3d?.projectObjectToContainer(label.objectId) ?? null
1323
+ : project2DScenePointToContainer({ x: label.x, y: label.y });
1324
+ if (!point) {
1325
+ continue;
1326
+ }
1327
+ descriptors.push({
1328
+ key: `object:${label.renderId}`,
1329
+ kind: "object",
1330
+ point: is3DView()
1331
+ ? { x: point.x, y: point.y - 18 }
1332
+ : point,
1333
+ textAnchor: label.textAnchor,
1334
+ objectId: label.objectId,
1335
+ primaryText: label.label,
1336
+ secondaryText: label.secondaryLabel,
1337
+ secondaryOffset: Math.max(label.secondaryY - label.y, 12),
1338
+ });
1339
+ }
1340
+ }
1341
+ if (!is3DView() && layerEnabled("events")) {
1342
+ for (const event of scene.events) {
1343
+ if (event.hidden || !isEventVisible(event, visibleObjectIds)) {
1344
+ continue;
1345
+ }
1346
+ const point = project2DScenePointToContainer({ x: event.x, y: event.y - 10 });
1347
+ if (!point) {
1348
+ continue;
1349
+ }
1350
+ descriptors.push({
1351
+ key: `event:${event.renderId}`,
1352
+ kind: "event",
1353
+ point,
1354
+ textAnchor: "middle",
1355
+ primaryText: event.event.label || event.event.id,
1356
+ });
1357
+ }
1358
+ }
1359
+ return descriptors;
1360
+ }
1361
+ function isEventVisible(event, visibleObjectIds) {
1362
+ return event.objectIds.some((objectId) => visibleObjectIds.has(objectId));
1363
+ }
1364
+ function createScreenLabelElement(descriptor) {
1365
+ const element = document.createElement("div");
1366
+ element.className = `wo-viewer-label wo-viewer-label-${descriptor.kind}`;
1367
+ element.dataset.worldorbitScreenLabel = "true";
1368
+ element.dataset.labelKey = descriptor.key;
1369
+ element.dataset.anchor = descriptor.textAnchor;
1370
+ element.style.left = `${descriptor.point.x}px`;
1371
+ element.style.top = `${descriptor.point.y}px`;
1372
+ if (descriptor.objectId) {
1373
+ element.dataset.objectId = descriptor.objectId;
1374
+ for (const className of resolveScreenLabelClasses(descriptor.objectId)) {
1375
+ element.classList.add(className);
1376
+ }
1377
+ }
1378
+ const primary = document.createElement("span");
1379
+ primary.className = "wo-viewer-label-primary";
1380
+ if (descriptor.kind === "object") {
1381
+ primary.style.fontSize = `${14 * scene.scaleModel.labelMultiplier}px`;
1382
+ }
1383
+ primary.textContent = descriptor.primaryText;
1384
+ element.append(primary);
1385
+ if (descriptor.secondaryText) {
1386
+ const secondary = document.createElement("span");
1387
+ secondary.className = "wo-viewer-label-secondary";
1388
+ secondary.style.fontSize = `${11 * scene.scaleModel.labelMultiplier}px`;
1389
+ secondary.style.marginTop = `${Math.max(descriptor.secondaryOffset ?? 12, 10) - 10}px`;
1390
+ secondary.textContent = descriptor.secondaryText;
1391
+ element.append(secondary);
1392
+ }
1393
+ return element;
1394
+ }
1395
+ function layerEnabled(id) {
1396
+ return renderOptions.layers?.[id] !== false;
1397
+ }
1398
+ function resolveScreenLabelClasses(objectId) {
1399
+ const classes = [];
1400
+ const selectedDetails = buildObjectDetails(state.selectedObjectId);
1401
+ const hoveredDetails = buildObjectDetails(hoveredObjectId);
1402
+ if (state.selectedObjectId === objectId) {
1403
+ classes.push("wo-object-selected");
1404
+ }
1405
+ if (selectedDetails) {
1406
+ const selectedChain = new Set([
1407
+ selectedDetails.objectId,
1408
+ ...selectedDetails.renderObject.childIds,
1409
+ ...selectedDetails.renderObject.ancestorIds,
1410
+ ]);
1411
+ const selectedAncestors = new Set(selectedDetails.ancestors.map((ancestor) => ancestor.objectId));
1412
+ if (selectedChain.has(objectId)) {
1413
+ classes.push("wo-chain-selected");
1414
+ }
1415
+ if (selectedAncestors.has(objectId)) {
1416
+ classes.push("wo-ancestor-selected");
1417
+ }
1418
+ }
1419
+ if (hoveredDetails) {
1420
+ const hoveredChain = new Set([
1421
+ hoveredDetails.objectId,
1422
+ ...hoveredDetails.renderObject.childIds,
1423
+ ...hoveredDetails.renderObject.ancestorIds,
1424
+ ]);
1425
+ const hoveredAncestors = new Set(hoveredDetails.ancestors.map((ancestor) => ancestor.objectId));
1426
+ if (hoveredChain.has(objectId)) {
1427
+ classes.push("wo-chain-hover");
1428
+ }
1429
+ if (hoveredAncestors.has(objectId)) {
1430
+ classes.push("wo-ancestor-hover");
1431
+ }
1432
+ }
1433
+ return classes;
1434
+ }
1280
1435
  function create3DFocusState(objectId) {
1281
1436
  const target = spatialScene?.focusTargets.find((entry) => entry.objectId === objectId);
1282
1437
  if (!target) {
@@ -1557,7 +1712,7 @@ function cssEscape(value) {
1557
1712
  }
1558
1713
  return value.replace(/["\\]/g, "\\$&");
1559
1714
  }
1560
- function installViewerTooltipStyles() {
1715
+ function installViewerOverlayStyles() {
1561
1716
  if (typeof document === "undefined" || document.getElementById(TOOLTIP_STYLE_ID)) {
1562
1717
  return;
1563
1718
  }
@@ -1592,6 +1747,56 @@ function installViewerTooltipStyles() {
1592
1747
  height: 100%;
1593
1748
  min-height: 320px;
1594
1749
  }
1750
+ .wo-viewer-label-root {
1751
+ position: absolute;
1752
+ inset: 0;
1753
+ z-index: 8;
1754
+ pointer-events: none;
1755
+ overflow: hidden;
1756
+ }
1757
+ .wo-viewer-label {
1758
+ position: absolute;
1759
+ display: grid;
1760
+ gap: 2px;
1761
+ color: #edf6ff;
1762
+ font-family: "Segoe UI Variable", "Segoe UI", sans-serif;
1763
+ line-height: 1.15;
1764
+ text-shadow: 0 1px 2px rgba(7, 16, 25, 0.65), 0 0 18px rgba(7, 16, 25, 0.18);
1765
+ white-space: nowrap;
1766
+ }
1767
+ .wo-viewer-label[data-anchor="middle"] { transform: translate(-50%, 0); }
1768
+ .wo-viewer-label[data-anchor="end"] { transform: translate(-100%, 0); }
1769
+ .wo-viewer-label-primary {
1770
+ font-size: 14px;
1771
+ font-weight: 600;
1772
+ letter-spacing: 0.02em;
1773
+ }
1774
+ .wo-viewer-label-secondary {
1775
+ font-size: 11px;
1776
+ font-weight: 500;
1777
+ color: rgba(237, 246, 255, 0.72);
1778
+ }
1779
+ .wo-viewer-label-event {
1780
+ color: #ffce8a;
1781
+ text-transform: uppercase;
1782
+ letter-spacing: 0.04em;
1783
+ }
1784
+ .wo-viewer-label-event .wo-viewer-label-primary {
1785
+ font-size: 10px;
1786
+ font-weight: 700;
1787
+ }
1788
+ .wo-viewer-label.wo-object-selected .wo-viewer-label-primary,
1789
+ .wo-viewer-label.wo-chain-selected .wo-viewer-label-primary,
1790
+ .wo-viewer-label.wo-chain-hover .wo-viewer-label-primary {
1791
+ color: #ffce8a;
1792
+ }
1793
+ .wo-viewer-label.wo-object-selected .wo-viewer-label-secondary {
1794
+ color: #8fcaff;
1795
+ }
1796
+ .wo-viewer-label.wo-ancestor-selected .wo-viewer-label-primary,
1797
+ .wo-viewer-label.wo-ancestor-hover .wo-viewer-label-primary {
1798
+ opacity: 0.82;
1799
+ }
1595
1800
  .wo-viewer-tooltip-root {
1596
1801
  position: absolute;
1597
1802
  z-index: 12;
@@ -67,6 +67,7 @@ export function createWorldOrbitEmbedMarkup(payload, options = {}) {
67
67
  }
68
68
  export function mountWorldOrbitEmbeds(root = document, options = {}) {
69
69
  const viewers = new Map();
70
+ const cleanupCallbacks = [];
70
71
  const elements = [...root.querySelectorAll(EMBED_SELECTOR)];
71
72
  for (const element of elements) {
72
73
  const payload = deserializePayloadFromElement(element);
@@ -82,14 +83,16 @@ export function mountWorldOrbitEmbeds(root = document, options = {}) {
82
83
  const viewMode = options.viewer?.viewMode ??
83
84
  payload.options?.viewMode ??
84
85
  embedModeToViewMode(mode);
86
+ const measureViewport = () => resolveEmbedViewport(element, payload.scene, options);
85
87
  if (mode === "interactive-2d" || mode === "interactive-3d") {
86
88
  try {
89
+ const viewport = measureViewport();
87
90
  const viewer = createInteractiveViewer(element, {
88
91
  ...options.viewer,
89
92
  scene: payload.scene,
90
93
  spatialScene: payload.spatialScene,
91
- width: options.width ?? payload.scene.width,
92
- height: options.height ?? payload.scene.height,
94
+ width: viewport.width,
95
+ height: viewport.height,
93
96
  padding: options.padding ?? payload.scene.padding,
94
97
  preset,
95
98
  theme,
@@ -105,6 +108,13 @@ export function mountWorldOrbitEmbeds(root = document, options = {}) {
105
108
  viewer.setAtlasState(payload.options.atlasState);
106
109
  }
107
110
  viewers.set(element, viewer);
111
+ cleanupCallbacks.push(bindEmbedResize(element, () => {
112
+ const nextViewport = measureViewport();
113
+ viewer.setRenderOptions({
114
+ width: nextViewport.width,
115
+ height: nextViewport.height,
116
+ });
117
+ }));
108
118
  options.onMount?.(viewer, element);
109
119
  }
110
120
  catch (error) {
@@ -118,17 +128,22 @@ export function mountWorldOrbitEmbeds(root = document, options = {}) {
118
128
  }
119
129
  }
120
130
  else {
121
- element.innerHTML = renderSceneToSvg(payload.scene, {
122
- width: options.width ?? payload.scene.width,
123
- height: options.height ?? payload.scene.height,
124
- padding: options.padding ?? payload.scene.padding,
125
- preset,
126
- theme,
127
- layers,
128
- filter: initialFilter,
129
- selectedObjectId: initialSelectionObjectId ?? null,
130
- subtitle,
131
- });
131
+ const renderStaticEmbed = () => {
132
+ const viewport = measureViewport();
133
+ element.innerHTML = renderSceneToSvg(payload.scene, {
134
+ width: viewport.width,
135
+ height: viewport.height,
136
+ padding: options.padding ?? payload.scene.padding,
137
+ preset,
138
+ theme,
139
+ layers,
140
+ filter: initialFilter,
141
+ selectedObjectId: initialSelectionObjectId ?? null,
142
+ subtitle,
143
+ });
144
+ };
145
+ renderStaticEmbed();
146
+ cleanupCallbacks.push(bindEmbedResize(element, renderStaticEmbed));
132
147
  options.onMount?.(null, element);
133
148
  }
134
149
  element.dataset.worldorbitMounted = "true";
@@ -136,14 +151,72 @@ export function mountWorldOrbitEmbeds(root = document, options = {}) {
136
151
  return {
137
152
  viewers: [...viewers.values()],
138
153
  destroy() {
154
+ for (const cleanup of cleanupCallbacks) {
155
+ cleanup();
156
+ }
139
157
  for (const [element, viewer] of viewers.entries()) {
140
158
  viewer.destroy();
141
159
  element.removeAttribute("data-worldorbit-mounted");
142
160
  }
161
+ for (const element of elements) {
162
+ element.removeAttribute("data-worldorbit-mounted");
163
+ }
143
164
  viewers.clear();
144
165
  },
145
166
  };
146
167
  }
168
+ function resolveEmbedViewport(element, scene, options) {
169
+ const rect = element.getBoundingClientRect();
170
+ const width = sanitizeViewportDimension(options.width) ??
171
+ sanitizeViewportDimension(element.clientWidth) ??
172
+ sanitizeViewportDimension(rect.width) ??
173
+ scene.width;
174
+ const explicitHeight = sanitizeViewportDimension(options.height) ??
175
+ sanitizeViewportDimension(element.clientHeight) ??
176
+ sanitizeViewportDimension(rect.height);
177
+ const fallbackHeight = Math.max(Math.round(width * (scene.height / Math.max(scene.width, 1))), Math.min(scene.height, 240));
178
+ return {
179
+ width,
180
+ height: explicitHeight ?? fallbackHeight,
181
+ };
182
+ }
183
+ function sanitizeViewportDimension(value) {
184
+ return typeof value === "number" && Number.isFinite(value) && value > 0
185
+ ? Math.round(value)
186
+ : null;
187
+ }
188
+ function bindEmbedResize(element, callback) {
189
+ let lastWidth = -1;
190
+ let lastHeight = -1;
191
+ const run = () => {
192
+ const rect = element.getBoundingClientRect();
193
+ const nextWidth = Math.round(Math.max(element.clientWidth || rect.width, 0));
194
+ const nextHeight = Math.round(Math.max(element.clientHeight || rect.height, 0));
195
+ if (nextWidth === lastWidth && nextHeight === lastHeight) {
196
+ return;
197
+ }
198
+ lastWidth = nextWidth;
199
+ lastHeight = nextHeight;
200
+ callback();
201
+ };
202
+ run();
203
+ if (typeof ResizeObserver !== "undefined") {
204
+ const observer = new ResizeObserver(() => {
205
+ run();
206
+ });
207
+ observer.observe(element);
208
+ return () => {
209
+ observer.disconnect();
210
+ };
211
+ }
212
+ const handleWindowResize = () => {
213
+ run();
214
+ };
215
+ window.addEventListener("resize", handleWindowResize);
216
+ return () => {
217
+ window.removeEventListener("resize", handleWindowResize);
218
+ };
219
+ }
147
220
  function deserializePayloadFromElement(element) {
148
221
  const serialized = element.dataset.worldorbitPayload;
149
222
  if (!serialized) {