worldorbit 3.0.5 → 3.0.7

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.
@@ -30,6 +30,15 @@ const THEME_PRESETS = {
30
30
  starCore: "#ffcc67",
31
31
  starStroke: "rgba(255, 245, 203, 0.85)",
32
32
  starGlow: "#ffe8a3",
33
+ spaceFog: "#07131d",
34
+ starfield: "rgba(226, 239, 255, 0.9)",
35
+ starfieldDim: "rgba(164, 194, 228, 0.45)",
36
+ objectSpecular: "#f5f8ff",
37
+ orbitOpacity: 0.34,
38
+ orbitBandOpacity: 0.24,
39
+ selectionHalo: "rgba(255, 214, 139, 0.9)",
40
+ atmosphere: "rgba(143, 202, 255, 0.4)",
41
+ cometTail: "rgba(193, 243, 255, 0.7)",
33
42
  fontFamily: "\"Segoe UI Variable\", \"Bahnschrift\", sans-serif",
34
43
  displayFont: "\"Bahnschrift\", \"Segoe UI Variable\", sans-serif"
35
44
  },
@@ -53,6 +62,15 @@ const THEME_PRESETS = {
53
62
  starCore: "#e5f98c",
54
63
  starStroke: "rgba(246, 255, 217, 0.9)",
55
64
  starGlow: "#fffab4",
65
+ spaceFog: "#071723",
66
+ starfield: "rgba(220, 255, 245, 0.9)",
67
+ starfieldDim: "rgba(124, 212, 195, 0.42)",
68
+ objectSpecular: "#ecfffb",
69
+ orbitOpacity: 0.3,
70
+ orbitBandOpacity: 0.22,
71
+ selectionHalo: "rgba(120, 255, 215, 0.85)",
72
+ atmosphere: "rgba(120, 255, 215, 0.32)",
73
+ cometTail: "rgba(181, 255, 236, 0.68)",
56
74
  fontFamily: "\"Segoe UI Variable\", \"Bahnschrift\", sans-serif",
57
75
  displayFont: "\"Bahnschrift\", \"Segoe UI Variable\", sans-serif"
58
76
  },
@@ -76,6 +94,15 @@ const THEME_PRESETS = {
76
94
  starCore: "#ffb766",
77
95
  starStroke: "rgba(255, 236, 205, 0.88)",
78
96
  starGlow: "#ffe2ad",
97
+ spaceFog: "#1c0d12",
98
+ starfield: "rgba(255, 232, 214, 0.88)",
99
+ starfieldDim: "rgba(255, 176, 138, 0.38)",
100
+ objectSpecular: "#fff0e6",
101
+ orbitOpacity: 0.3,
102
+ orbitBandOpacity: 0.24,
103
+ selectionHalo: "rgba(255, 178, 125, 0.85)",
104
+ atmosphere: "rgba(255, 190, 140, 0.26)",
105
+ cometTail: "rgba(255, 214, 173, 0.62)",
79
106
  fontFamily: "\"Segoe UI Variable\", \"Bahnschrift\", sans-serif",
80
107
  displayFont: "\"Bahnschrift\", \"Segoe UI Variable\", sans-serif"
81
108
  }
@@ -1,6 +1,8 @@
1
1
  import type { CoordinatePoint, RenderOrbitVisual, RenderProjectionFallback, RenderPresetName, RenderSceneEvent, RenderSceneGroup, RenderSceneLabel, RenderScaleModel, RenderScene, RenderSceneObject, RenderSceneViewpoint, SceneRenderOptions, SpatialScene, ViewProjection, WorldOrbitObject, WorldOrbitDocument, WorldOrbitViewCamera } from "@worldorbit/core";
2
2
  export type WorldOrbitThemeName = "atlas" | "nightglass" | "ember";
3
3
  export type WorldOrbitViewMode = "2d" | "3d";
4
+ export type WorldOrbit3DQuality = "low" | "balanced" | "high";
5
+ export type WorldOrbit3DStyle = "symbolic" | "cinematic";
4
6
  export type WorldOrbitEmbedMode = "static" | "interactive" | "interactive-2d" | "interactive-3d";
5
7
  export type TooltipMode = "hover" | "pinned" | "disabled";
6
8
  export interface WorldOrbitTheme {
@@ -23,6 +25,15 @@ export interface WorldOrbitTheme {
23
25
  starCore: string;
24
26
  starStroke: string;
25
27
  starGlow: string;
28
+ spaceFog: string;
29
+ starfield: string;
30
+ starfieldDim: string;
31
+ objectSpecular: string;
32
+ orbitOpacity: number;
33
+ orbitBandOpacity: number;
34
+ selectionHalo: string;
35
+ atmosphere: string;
36
+ cometTail: string;
26
37
  fontFamily: string;
27
38
  displayFont: string;
28
39
  }
@@ -64,6 +75,8 @@ export interface ViewerRenderOptions extends Omit<SvgRenderOptions, "selectedObj
64
75
  projection?: "document" | ViewProjection;
65
76
  scaleModel?: Partial<RenderScaleModel>;
66
77
  viewMode?: WorldOrbitViewMode;
78
+ quality?: WorldOrbit3DQuality;
79
+ style3d?: WorldOrbit3DStyle;
67
80
  }
68
81
  export interface ViewerState {
69
82
  scale: number;
@@ -118,6 +131,8 @@ export interface ViewerAtlasState {
118
131
  scaleModel?: Partial<RenderScaleModel>;
119
132
  activeEventId?: string | null;
120
133
  viewMode?: WorldOrbitViewMode;
134
+ quality?: WorldOrbit3DQuality;
135
+ style3d?: WorldOrbit3DStyle;
121
136
  };
122
137
  filter: ViewerFilter | null;
123
138
  }
@@ -274,6 +289,8 @@ export interface WorldOrbitEmbedPayload {
274
289
  subtitle?: string;
275
290
  preset?: SceneRenderOptions["preset"];
276
291
  viewMode?: WorldOrbitViewMode;
292
+ quality?: WorldOrbit3DQuality;
293
+ style3d?: WorldOrbit3DStyle;
277
294
  initialViewpointId?: string;
278
295
  initialSelectionObjectId?: string;
279
296
  initialFilter?: ViewerFilter | null;
@@ -45,6 +45,8 @@ export function createInteractiveViewer(container, options) {
45
45
  preset: options.preset,
46
46
  projection: options.projection,
47
47
  viewMode: options.viewMode ?? "2d",
48
+ quality: options.quality ?? "balanced",
49
+ style3d: options.style3d ?? "symbolic",
48
50
  camera: options.camera ? { ...options.camera } : null,
49
51
  scaleModel: options.scaleModel ? { ...options.scaleModel } : undefined,
50
52
  theme: options.theme,
@@ -66,6 +68,7 @@ export function createInteractiveViewer(container, options) {
66
68
  let cameraRoot = null;
67
69
  let runtime3d = null;
68
70
  let minimapRoot = null;
71
+ let labelRoot = null;
69
72
  let tooltipRoot = null;
70
73
  let suppressClick = false;
71
74
  let activePointerId = null;
@@ -90,7 +93,7 @@ export function createInteractiveViewer(container, options) {
90
93
  if (previousTabIndex === null) {
91
94
  container.tabIndex = 0;
92
95
  }
93
- installViewerTooltipStyles();
96
+ installViewerOverlayStyles();
94
97
  container.classList.add("wo-viewer-container");
95
98
  container.style.touchAction = behavior.touch ? "none" : previousTouchAction;
96
99
  if (!container.style.position) {
@@ -671,6 +674,8 @@ export function createInteractiveViewer(container, options) {
671
674
  stopAnimationLoop();
672
675
  runtime3d?.destroy();
673
676
  runtime3d = null;
677
+ labelRoot?.remove();
678
+ labelRoot = null;
674
679
  tooltipRoot?.remove();
675
680
  tooltipRoot = null;
676
681
  minimapRoot?.remove();
@@ -704,6 +709,7 @@ export function createInteractiveViewer(container, options) {
704
709
  svgElement = null;
705
710
  cameraRoot = null;
706
711
  minimapRoot = null;
712
+ labelRoot = null;
707
713
  tooltipRoot = null;
708
714
  if (is3DView()) {
709
715
  spatialScene = spatialScene ?? renderSpatialSceneFromInput(currentInput, renderOptions, providedSpatialScene);
@@ -723,6 +729,10 @@ export function createInteractiveViewer(container, options) {
723
729
  minimapRoot.dataset.worldorbitMinimapRoot = "true";
724
730
  container.append(minimapRoot);
725
731
  }
732
+ labelRoot = document.createElement("div");
733
+ labelRoot.className = "wo-viewer-label-root";
734
+ labelRoot.dataset.worldorbitLabelRoot = "true";
735
+ container.append(labelRoot);
726
736
  if (behavior.tooltipMode !== "disabled") {
727
737
  tooltipRoot = document.createElement("div");
728
738
  tooltipRoot.className = "wo-viewer-tooltip-root";
@@ -734,6 +744,7 @@ export function createInteractiveViewer(container, options) {
734
744
  if (!is3DView() && (!svgElement || !cameraRoot)) {
735
745
  throw new Error("Interactive viewer could not locate the rendered SVG camera root.");
736
746
  }
747
+ suppressStaticLabelLayers();
737
748
  state = resetView
738
749
  ? is3DView()
739
750
  ? { ...DEFAULT_VIEWER_STATE }
@@ -783,14 +794,15 @@ export function createInteractiveViewer(container, options) {
783
794
  return;
784
795
  }
785
796
  cameraRoot.setAttribute("transform", composeViewerTransform(scene, state));
797
+ updateScreenLabels();
786
798
  updateMinimap();
787
799
  updateTooltip();
788
800
  }
789
801
  function applySelection(objectId, emitCallback = true) {
790
802
  if (!is3DView() && state.selectedObjectId) {
791
- container
792
- .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
793
- ?.classList.remove("wo-object-selected");
803
+ for (const element of container.querySelectorAll(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)) {
804
+ element.classList.remove("wo-object-selected");
805
+ }
794
806
  }
795
807
  state = {
796
808
  ...state,
@@ -799,9 +811,9 @@ export function createInteractiveViewer(container, options) {
799
811
  : null,
800
812
  };
801
813
  if (!is3DView() && state.selectedObjectId) {
802
- container
803
- .querySelector(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)
804
- ?.classList.add("wo-object-selected");
814
+ for (const element of container.querySelectorAll(`[data-object-id="${cssEscape(state.selectedObjectId)}"]`)) {
815
+ element.classList.add("wo-object-selected");
816
+ }
805
817
  }
806
818
  syncAtlasHighlights();
807
819
  updateTooltip();
@@ -1160,15 +1172,18 @@ export function createInteractiveViewer(container, options) {
1160
1172
  };
1161
1173
  }
1162
1174
  function project2DTooltipPoint(renderObject) {
1163
- if (!svgElement) {
1164
- return null;
1165
- }
1166
1175
  const anchor = {
1167
1176
  x: renderObject.anchorX ?? renderObject.x,
1168
1177
  y: renderObject.anchorY ??
1169
1178
  renderObject.y - Math.max(renderObject.visualRadius, renderObject.radius),
1170
1179
  };
1171
- const viewportPoint = projectWorldPoint(anchor);
1180
+ return project2DScenePointToContainer(anchor);
1181
+ }
1182
+ function project2DScenePointToContainer(point) {
1183
+ if (!svgElement) {
1184
+ return null;
1185
+ }
1186
+ const viewportPoint = projectWorldPoint(point);
1172
1187
  const svgRect = svgElement.getBoundingClientRect();
1173
1188
  const containerRect = container.getBoundingClientRect();
1174
1189
  return {
@@ -1274,9 +1289,192 @@ export function createInteractiveViewer(container, options) {
1274
1289
  state,
1275
1290
  timeSeconds: animationState.timeSeconds,
1276
1291
  });
1292
+ updateScreenLabels();
1277
1293
  updateMinimap();
1278
1294
  updateTooltip();
1279
1295
  }
1296
+ function suppressStaticLabelLayers() {
1297
+ if (is3DView()) {
1298
+ return;
1299
+ }
1300
+ container
1301
+ .querySelector('[data-layer-id="labels"]')
1302
+ ?.setAttribute("display", "none");
1303
+ for (const element of container.querySelectorAll(".wo-event-label")) {
1304
+ element.setAttribute("display", "none");
1305
+ }
1306
+ }
1307
+ function updateScreenLabels() {
1308
+ if (!labelRoot) {
1309
+ return;
1310
+ }
1311
+ const descriptors = buildScreenLabelDescriptors();
1312
+ labelRoot.replaceChildren(...descriptors.map((descriptor) => createScreenLabelElement(descriptor)));
1313
+ labelRoot.hidden = descriptors.length === 0;
1314
+ }
1315
+ function buildScreenLabelDescriptors() {
1316
+ const descriptors = [];
1317
+ const visibleObjectIds = getVisibleObjectIds();
1318
+ if (layerEnabled("labels")) {
1319
+ for (const label of scene.labels) {
1320
+ if (label.hidden || !visibleObjectIds.has(label.objectId)) {
1321
+ continue;
1322
+ }
1323
+ if (is3DView() && !shouldRender3DLabel(label.objectId, visibleObjectIds)) {
1324
+ continue;
1325
+ }
1326
+ const point = is3DView()
1327
+ ? runtime3d?.projectObjectToContainer(label.objectId) ?? null
1328
+ : project2DScenePointToContainer({ x: label.x, y: label.y });
1329
+ if (!point) {
1330
+ continue;
1331
+ }
1332
+ descriptors.push({
1333
+ key: `object:${label.renderId}`,
1334
+ kind: "object",
1335
+ point: is3DView()
1336
+ ? { x: point.x, y: point.y - 18 }
1337
+ : point,
1338
+ textAnchor: label.textAnchor,
1339
+ objectId: label.objectId,
1340
+ primaryText: label.label,
1341
+ secondaryText: label.secondaryLabel,
1342
+ secondaryOffset: Math.max(label.secondaryY - label.y, 12),
1343
+ });
1344
+ }
1345
+ }
1346
+ if (!is3DView() && layerEnabled("events")) {
1347
+ for (const event of scene.events) {
1348
+ if (event.hidden || !isEventVisible(event, visibleObjectIds)) {
1349
+ continue;
1350
+ }
1351
+ const point = project2DScenePointToContainer({ x: event.x, y: event.y - 10 });
1352
+ if (!point) {
1353
+ continue;
1354
+ }
1355
+ descriptors.push({
1356
+ key: `event:${event.renderId}`,
1357
+ kind: "event",
1358
+ point,
1359
+ textAnchor: "middle",
1360
+ primaryText: event.event.label || event.event.id,
1361
+ });
1362
+ }
1363
+ }
1364
+ return descriptors;
1365
+ }
1366
+ function isEventVisible(event, visibleObjectIds) {
1367
+ return event.objectIds.some((objectId) => visibleObjectIds.has(objectId));
1368
+ }
1369
+ function shouldRender3DLabel(objectId, visibleObjectIds) {
1370
+ if (!is3DView()) {
1371
+ return true;
1372
+ }
1373
+ if (objectId === state.selectedObjectId || objectId === hoveredObjectId) {
1374
+ return true;
1375
+ }
1376
+ const object = getObjectById(objectId);
1377
+ if (!object || object.hidden || !visibleObjectIds.has(objectId)) {
1378
+ return false;
1379
+ }
1380
+ if (object.object.type === "star") {
1381
+ return true;
1382
+ }
1383
+ const selected = state.selectedObjectId ? buildObjectDetails(state.selectedObjectId) : null;
1384
+ const hovered = hoveredObjectId ? buildObjectDetails(hoveredObjectId) : null;
1385
+ const selectedFocus = selected
1386
+ ? new Set([
1387
+ selected.objectId,
1388
+ ...selected.renderObject.ancestorIds,
1389
+ ...selected.renderObject.childIds,
1390
+ ])
1391
+ : null;
1392
+ const hoveredFocus = hovered
1393
+ ? new Set([
1394
+ hovered.objectId,
1395
+ ...hovered.renderObject.ancestorIds,
1396
+ ...hovered.renderObject.childIds,
1397
+ ])
1398
+ : null;
1399
+ if (selectedFocus?.has(objectId) || hoveredFocus?.has(objectId)) {
1400
+ return true;
1401
+ }
1402
+ if (object.semanticGroupIds.length > 0 && object.visualRadius >= 12) {
1403
+ return true;
1404
+ }
1405
+ return object.childIds.length > 0 && object.visualRadius >= 10;
1406
+ }
1407
+ function createScreenLabelElement(descriptor) {
1408
+ const element = document.createElement("div");
1409
+ element.className = `wo-viewer-label wo-viewer-label-${descriptor.kind}`;
1410
+ element.dataset.worldorbitScreenLabel = "true";
1411
+ element.dataset.labelKey = descriptor.key;
1412
+ element.dataset.anchor = descriptor.textAnchor;
1413
+ element.style.left = `${descriptor.point.x}px`;
1414
+ element.style.top = `${descriptor.point.y}px`;
1415
+ if (descriptor.objectId) {
1416
+ element.dataset.objectId = descriptor.objectId;
1417
+ for (const className of resolveScreenLabelClasses(descriptor.objectId)) {
1418
+ element.classList.add(className);
1419
+ }
1420
+ }
1421
+ const primary = document.createElement("span");
1422
+ primary.className = "wo-viewer-label-primary";
1423
+ if (descriptor.kind === "object") {
1424
+ primary.style.fontSize = `${14 * scene.scaleModel.labelMultiplier}px`;
1425
+ }
1426
+ primary.textContent = descriptor.primaryText;
1427
+ element.append(primary);
1428
+ if (descriptor.secondaryText) {
1429
+ const secondary = document.createElement("span");
1430
+ secondary.className = "wo-viewer-label-secondary";
1431
+ secondary.style.fontSize = `${11 * scene.scaleModel.labelMultiplier}px`;
1432
+ secondary.style.marginTop = `${Math.max(descriptor.secondaryOffset ?? 12, 10) - 10}px`;
1433
+ secondary.textContent = descriptor.secondaryText;
1434
+ element.append(secondary);
1435
+ }
1436
+ return element;
1437
+ }
1438
+ function layerEnabled(id) {
1439
+ return renderOptions.layers?.[id] !== false;
1440
+ }
1441
+ function resolveScreenLabelClasses(objectId) {
1442
+ const classes = [];
1443
+ const selectedDetails = buildObjectDetails(state.selectedObjectId);
1444
+ const hoveredDetails = buildObjectDetails(hoveredObjectId);
1445
+ if (state.selectedObjectId === objectId) {
1446
+ classes.push("wo-object-selected");
1447
+ }
1448
+ if (selectedDetails) {
1449
+ const selectedChain = new Set([
1450
+ selectedDetails.objectId,
1451
+ ...selectedDetails.renderObject.childIds,
1452
+ ...selectedDetails.renderObject.ancestorIds,
1453
+ ]);
1454
+ const selectedAncestors = new Set(selectedDetails.ancestors.map((ancestor) => ancestor.objectId));
1455
+ if (selectedChain.has(objectId)) {
1456
+ classes.push("wo-chain-selected");
1457
+ }
1458
+ if (selectedAncestors.has(objectId)) {
1459
+ classes.push("wo-ancestor-selected");
1460
+ }
1461
+ }
1462
+ if (hoveredDetails) {
1463
+ const hoveredChain = new Set([
1464
+ hoveredDetails.objectId,
1465
+ ...hoveredDetails.renderObject.childIds,
1466
+ ...hoveredDetails.renderObject.ancestorIds,
1467
+ ]);
1468
+ const hoveredAncestors = new Set(hoveredDetails.ancestors.map((ancestor) => ancestor.objectId));
1469
+ if (hoveredChain.has(objectId)) {
1470
+ classes.push("wo-chain-hover");
1471
+ }
1472
+ if (hoveredAncestors.has(objectId)) {
1473
+ classes.push("wo-ancestor-hover");
1474
+ }
1475
+ }
1476
+ return classes;
1477
+ }
1280
1478
  function create3DFocusState(objectId) {
1281
1479
  const target = spatialScene?.focusTargets.find((entry) => entry.objectId === objectId);
1282
1480
  if (!target) {
@@ -1345,6 +1543,8 @@ function cloneRenderOptions(renderOptions) {
1345
1543
  : renderOptions.theme,
1346
1544
  activeEventId: renderOptions.activeEventId ?? null,
1347
1545
  viewMode: renderOptions.viewMode ?? "2d",
1546
+ quality: renderOptions.quality ?? "balanced",
1547
+ style3d: renderOptions.style3d ?? "symbolic",
1348
1548
  };
1349
1549
  }
1350
1550
  function mergeRenderOptions(current, next) {
@@ -1383,6 +1583,8 @@ function mergeRenderOptions(current, next) {
1383
1583
  ? { ...next.theme }
1384
1584
  : next.theme ?? current.theme,
1385
1585
  viewMode: next.viewMode ?? current.viewMode ?? "2d",
1586
+ quality: next.quality ?? current.quality ?? "balanced",
1587
+ style3d: next.style3d ?? current.style3d ?? "symbolic",
1386
1588
  };
1387
1589
  }
1388
1590
  function hasSceneAffectingRenderOptions(options) {
@@ -1557,7 +1759,7 @@ function cssEscape(value) {
1557
1759
  }
1558
1760
  return value.replace(/["\\]/g, "\\$&");
1559
1761
  }
1560
- function installViewerTooltipStyles() {
1762
+ function installViewerOverlayStyles() {
1561
1763
  if (typeof document === "undefined" || document.getElementById(TOOLTIP_STYLE_ID)) {
1562
1764
  return;
1563
1765
  }
@@ -1592,6 +1794,60 @@ function installViewerTooltipStyles() {
1592
1794
  height: 100%;
1593
1795
  min-height: 320px;
1594
1796
  }
1797
+ .wo-viewer-label-root {
1798
+ position: absolute;
1799
+ inset: 0;
1800
+ z-index: 8;
1801
+ pointer-events: none;
1802
+ overflow: hidden;
1803
+ }
1804
+ .wo-viewer-label {
1805
+ position: absolute;
1806
+ display: grid;
1807
+ gap: 2px;
1808
+ padding: 4px 8px;
1809
+ border-radius: 999px;
1810
+ background: linear-gradient(180deg, rgba(5, 16, 26, 0.72), rgba(5, 16, 26, 0.38));
1811
+ border: 1px solid rgba(164, 194, 228, 0.16);
1812
+ color: #edf6ff;
1813
+ font-family: "Segoe UI Variable", "Segoe UI", sans-serif;
1814
+ line-height: 1.15;
1815
+ text-shadow: 0 1px 2px rgba(7, 16, 25, 0.65), 0 0 18px rgba(7, 16, 25, 0.18);
1816
+ white-space: nowrap;
1817
+ }
1818
+ .wo-viewer-label[data-anchor="middle"] { transform: translate(-50%, 0); }
1819
+ .wo-viewer-label[data-anchor="end"] { transform: translate(-100%, 0); }
1820
+ .wo-viewer-label-primary {
1821
+ font-size: 14px;
1822
+ font-weight: 600;
1823
+ letter-spacing: 0.02em;
1824
+ }
1825
+ .wo-viewer-label-secondary {
1826
+ font-size: 11px;
1827
+ font-weight: 500;
1828
+ color: rgba(237, 246, 255, 0.72);
1829
+ }
1830
+ .wo-viewer-label-event {
1831
+ color: #ffce8a;
1832
+ text-transform: uppercase;
1833
+ letter-spacing: 0.04em;
1834
+ }
1835
+ .wo-viewer-label-event .wo-viewer-label-primary {
1836
+ font-size: 10px;
1837
+ font-weight: 700;
1838
+ }
1839
+ .wo-viewer-label.wo-object-selected .wo-viewer-label-primary,
1840
+ .wo-viewer-label.wo-chain-selected .wo-viewer-label-primary,
1841
+ .wo-viewer-label.wo-chain-hover .wo-viewer-label-primary {
1842
+ color: #ffce8a;
1843
+ }
1844
+ .wo-viewer-label.wo-object-selected .wo-viewer-label-secondary {
1845
+ color: #8fcaff;
1846
+ }
1847
+ .wo-viewer-label.wo-ancestor-selected .wo-viewer-label-primary,
1848
+ .wo-viewer-label.wo-ancestor-hover .wo-viewer-label-primary {
1849
+ opacity: 0.82;
1850
+ }
1595
1851
  .wo-viewer-tooltip-root {
1596
1852
  position: absolute;
1597
1853
  z-index: 12;