worldorbit 3.2.0 → 3.2.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 (103) hide show
  1. package/dist/browser/core/dist/scene.js +98 -16
  2. package/dist/browser/core/dist/spatial-scene.js +1 -0
  3. package/dist/browser/core/dist/types.d.ts +3 -0
  4. package/dist/browser/editor/dist/editor.js +55 -12
  5. package/dist/browser/obsidian-plugin/dist/diagnostics.d.ts +3 -0
  6. package/dist/browser/obsidian-plugin/dist/diagnostics.js +23 -0
  7. package/dist/browser/obsidian-plugin/dist/examples.d.ts +3 -0
  8. package/dist/browser/obsidian-plugin/dist/examples.js +77 -0
  9. package/dist/browser/obsidian-plugin/dist/index.d.ts +9 -0
  10. package/dist/browser/obsidian-plugin/dist/index.js +8 -0
  11. package/dist/browser/obsidian-plugin/dist/main.d.ts +2 -0
  12. package/dist/browser/obsidian-plugin/dist/main.js +2 -0
  13. package/dist/browser/obsidian-plugin/dist/navigation.d.ts +6 -0
  14. package/dist/browser/obsidian-plugin/dist/navigation.js +44 -0
  15. package/dist/browser/obsidian-plugin/dist/plugin.d.ts +8 -0
  16. package/dist/browser/obsidian-plugin/dist/plugin.js +508 -0
  17. package/dist/browser/obsidian-plugin/dist/positions.d.ts +7 -0
  18. package/dist/browser/obsidian-plugin/dist/positions.js +14 -0
  19. package/dist/browser/obsidian-plugin/dist/settings.d.ts +2 -0
  20. package/dist/browser/obsidian-plugin/dist/settings.js +5 -0
  21. package/dist/browser/obsidian-plugin/dist/theme.d.ts +2 -0
  22. package/dist/browser/obsidian-plugin/dist/theme.js +31 -0
  23. package/dist/browser/obsidian-plugin/dist/types.d.ts +42 -0
  24. package/dist/browser/obsidian-plugin/dist/types.js +1 -0
  25. package/dist/browser/obsidian-plugin/dist/viewer-host.d.ts +14 -0
  26. package/dist/browser/obsidian-plugin/dist/viewer-host.js +110 -0
  27. package/dist/browser/viewer/dist/atlas-state.js +3 -0
  28. package/dist/browser/viewer/dist/index.d.ts +1 -0
  29. package/dist/browser/viewer/dist/index.js +1 -0
  30. package/dist/browser/viewer/dist/interactive-2d.d.ts +21 -0
  31. package/dist/browser/viewer/dist/interactive-2d.js +201 -0
  32. package/dist/browser/viewer/dist/render.d.ts +1 -1
  33. package/dist/browser/viewer/dist/render.js +2 -1
  34. package/dist/browser/viewer/dist/types.d.ts +1 -0
  35. package/dist/browser/viewer/dist/viewer-state.d.ts +1 -1
  36. package/dist/browser/viewer/dist/viewer-state.js +1 -1
  37. package/dist/browser/viewer/dist/viewer.js +2 -0
  38. package/dist/obsidian-plugin/LICENSE +21 -0
  39. package/dist/obsidian-plugin/README.md +124 -0
  40. package/dist/obsidian-plugin/main.js +108 -0
  41. package/dist/obsidian-plugin/manifest.json +9 -0
  42. package/dist/obsidian-plugin/obsidian_scsh_1.png +0 -0
  43. package/dist/obsidian-plugin/obsidian_scsh_2.png +0 -0
  44. package/dist/obsidian-plugin/styles.css +164 -0
  45. package/dist/unpkg/core/dist/scene.js +98 -16
  46. package/dist/unpkg/core/dist/spatial-scene.js +1 -0
  47. package/dist/unpkg/core/dist/types.d.ts +3 -0
  48. package/dist/unpkg/editor/dist/editor.js +55 -12
  49. package/dist/unpkg/obsidian-plugin/dist/diagnostics.d.ts +3 -0
  50. package/dist/unpkg/obsidian-plugin/dist/diagnostics.js +23 -0
  51. package/dist/unpkg/obsidian-plugin/dist/examples.d.ts +3 -0
  52. package/dist/unpkg/obsidian-plugin/dist/examples.js +77 -0
  53. package/dist/unpkg/obsidian-plugin/dist/index.d.ts +9 -0
  54. package/dist/unpkg/obsidian-plugin/dist/index.js +8 -0
  55. package/dist/unpkg/obsidian-plugin/dist/main.d.ts +2 -0
  56. package/dist/unpkg/obsidian-plugin/dist/main.js +2 -0
  57. package/dist/unpkg/obsidian-plugin/dist/navigation.d.ts +6 -0
  58. package/dist/unpkg/obsidian-plugin/dist/navigation.js +44 -0
  59. package/dist/unpkg/obsidian-plugin/dist/plugin.d.ts +8 -0
  60. package/dist/unpkg/obsidian-plugin/dist/plugin.js +508 -0
  61. package/dist/unpkg/obsidian-plugin/dist/positions.d.ts +7 -0
  62. package/dist/unpkg/obsidian-plugin/dist/positions.js +14 -0
  63. package/dist/unpkg/obsidian-plugin/dist/settings.d.ts +2 -0
  64. package/dist/unpkg/obsidian-plugin/dist/settings.js +5 -0
  65. package/dist/unpkg/obsidian-plugin/dist/theme.d.ts +2 -0
  66. package/dist/unpkg/obsidian-plugin/dist/theme.js +31 -0
  67. package/dist/unpkg/obsidian-plugin/dist/types.d.ts +42 -0
  68. package/dist/unpkg/obsidian-plugin/dist/types.js +1 -0
  69. package/dist/unpkg/obsidian-plugin/dist/viewer-host.d.ts +14 -0
  70. package/dist/unpkg/obsidian-plugin/dist/viewer-host.js +110 -0
  71. package/dist/unpkg/viewer/dist/atlas-state.js +3 -0
  72. package/dist/unpkg/viewer/dist/index.d.ts +1 -0
  73. package/dist/unpkg/viewer/dist/index.js +1 -0
  74. package/dist/unpkg/viewer/dist/interactive-2d.d.ts +21 -0
  75. package/dist/unpkg/viewer/dist/interactive-2d.js +201 -0
  76. package/dist/unpkg/viewer/dist/render.d.ts +1 -1
  77. package/dist/unpkg/viewer/dist/render.js +2 -1
  78. package/dist/unpkg/viewer/dist/types.d.ts +1 -0
  79. package/dist/unpkg/viewer/dist/viewer-state.d.ts +1 -1
  80. package/dist/unpkg/viewer/dist/viewer-state.js +1 -1
  81. package/dist/unpkg/viewer/dist/viewer.js +2 -0
  82. package/dist/unpkg/worldorbit-core.min.js +12 -12
  83. package/dist/unpkg/worldorbit-editor.min.js +342 -342
  84. package/dist/unpkg/worldorbit-markdown.min.js +23 -23
  85. package/dist/unpkg/worldorbit-viewer.min.js +197 -197
  86. package/dist/unpkg/worldorbit.js +297 -21
  87. package/dist/unpkg/worldorbit.min.js +201 -201
  88. package/package.json +18 -1
  89. package/packages/core/dist/scene.js +98 -16
  90. package/packages/core/dist/spatial-scene.js +1 -0
  91. package/packages/core/dist/types.d.ts +3 -0
  92. package/packages/editor/dist/editor.js +55 -12
  93. package/packages/viewer/dist/atlas-state.js +3 -0
  94. package/packages/viewer/dist/index.d.ts +1 -0
  95. package/packages/viewer/dist/index.js +1 -0
  96. package/packages/viewer/dist/interactive-2d.d.ts +21 -0
  97. package/packages/viewer/dist/interactive-2d.js +201 -0
  98. package/packages/viewer/dist/render.d.ts +1 -1
  99. package/packages/viewer/dist/render.js +2 -1
  100. package/packages/viewer/dist/types.d.ts +1 -0
  101. package/packages/viewer/dist/viewer-state.d.ts +1 -1
  102. package/packages/viewer/dist/viewer-state.js +1 -1
  103. package/packages/viewer/dist/viewer.js +2 -0
@@ -17,11 +17,12 @@ export function renderDocumentToScene(document, options = {}) {
17
17
  const schemaProjection = resolveProjection(document, options.projection);
18
18
  const camera = normalizeViewCamera(options.camera ?? null);
19
19
  const renderProjection = resolveRenderProjection(schemaProjection, camera);
20
- const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
21
20
  const spacingFactor = layoutPresetSpacing(layoutPreset);
21
+ const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel, options.bodyScaleMode);
22
22
  const systemId = document.system?.id ?? null;
23
23
  const activeEventId = options.activeEventId ?? null;
24
24
  const effectiveObjects = createEffectiveObjects(document.objects, document.events ?? [], activeEventId);
25
+ const sceneMetricScale = resolveSceneMetricScale(effectiveObjects, width, height, padding, spacingFactor, scaleModel);
25
26
  const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
26
27
  const relationships = buildSceneRelationships(effectiveObjects, objectMap);
27
28
  const positions = new Map();
@@ -61,6 +62,7 @@ export function renderDocumentToScene(document, options = {}) {
61
62
  spacingFactor,
62
63
  projection: renderProjection,
63
64
  scaleModel,
65
+ sceneMetricScale,
64
66
  };
65
67
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
66
68
  if (primaryRoot) {
@@ -82,14 +84,14 @@ export function renderDocumentToScene(document, options = {}) {
82
84
  const x = width -
83
85
  padding -
84
86
  140 -
85
- freePlacementOffsetPx(object.placement?.mode === "free" ? object.placement.distance : undefined, scaleModel);
87
+ freePlacementOffsetPx(object.placement?.mode === "free" ? object.placement.distance : undefined, scaleModel, sceneMetricScale);
86
88
  const rowStep = Math.max(76, ((height - padding * 2 - 180) / Math.max(1, freeObjects.length)) * spacingFactor) * scaleModel.freePlacementMultiplier;
87
89
  const y = padding + 92 + index * rowStep;
88
90
  positions.set(object.id, {
89
91
  object,
90
92
  x,
91
93
  y,
92
- radius: visualRadiusFor(object, 0, scaleModel),
94
+ radius: visualRadiusFor(object, 0, scaleModel, sceneMetricScale),
93
95
  sortKey: computeSortKey(x, y, 0),
94
96
  });
95
97
  leaderDrafts.push({
@@ -112,7 +114,7 @@ export function renderDocumentToScene(document, options = {}) {
112
114
  object,
113
115
  x: resolved.x,
114
116
  y: resolved.y,
115
- radius: visualRadiusFor(object, 2, scaleModel),
117
+ radius: visualRadiusFor(object, 2, scaleModel, sceneMetricScale),
116
118
  sortKey: computeSortKey(resolved.x, resolved.y, 2),
117
119
  anchorX: resolved.anchorX,
118
120
  anchorY: resolved.anchorY,
@@ -164,6 +166,7 @@ export function renderDocumentToScene(document, options = {}) {
164
166
  scale: String(document.system?.properties.scale ?? layoutPreset),
165
167
  units: String(document.system?.properties.units ?? "mixed"),
166
168
  preset: frame.preset ?? "custom",
169
+ "body.scaleMode": scaleModel.bodyScaleMode,
167
170
  ...(camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {}),
168
171
  ...(camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {}),
169
172
  ...(camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {}),
@@ -344,10 +347,11 @@ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera)
344
347
  }
345
348
  return parts.join(" - ");
346
349
  }
347
- function resolveScaleModel(layoutPreset, overrides) {
350
+ function resolveScaleModel(layoutPreset, overrides, bodyScaleMode) {
348
351
  const defaults = defaultScaleModel(layoutPreset);
349
352
  return {
350
353
  ...defaults,
354
+ ...(bodyScaleMode ? { bodyScaleMode } : {}),
351
355
  ...overrides,
352
356
  };
353
357
  }
@@ -362,6 +366,7 @@ function defaultScaleModel(layoutPreset) {
362
366
  ringThicknessMultiplier: 0.92,
363
367
  minBodyRadius: 4,
364
368
  maxBodyRadius: 36,
369
+ bodyScaleMode: "readable",
365
370
  };
366
371
  case "presentation":
367
372
  return {
@@ -372,6 +377,7 @@ function defaultScaleModel(layoutPreset) {
372
377
  ringThicknessMultiplier: 1.16,
373
378
  minBodyRadius: 5,
374
379
  maxBodyRadius: 48,
380
+ bodyScaleMode: "readable",
375
381
  };
376
382
  default:
377
383
  return {
@@ -382,6 +388,7 @@ function defaultScaleModel(layoutPreset) {
382
388
  ringThicknessMultiplier: 1,
383
389
  minBodyRadius: 4,
384
390
  maxBodyRadius: 40,
391
+ bodyScaleMode: "readable",
385
392
  };
386
393
  }
387
394
  }
@@ -395,6 +402,51 @@ function layoutPresetSpacing(layoutPreset) {
395
402
  return 1;
396
403
  }
397
404
  }
405
+ function resolveSceneMetricScale(objects, width, height, padding, spacingFactor, scaleModel) {
406
+ const orbitMetrics = [];
407
+ const bodyMetrics = [];
408
+ for (const object of objects) {
409
+ const radiusMetric = objectRadiusMetric(object);
410
+ if (radiusMetric !== null && radiusMetric > 0) {
411
+ bodyMetrics.push(radiusMetric);
412
+ }
413
+ const placement = object.placement;
414
+ if (!placement) {
415
+ continue;
416
+ }
417
+ if (placement.mode === "orbit") {
418
+ const orbitMetricValue = toDistanceMetric(placement.semiMajor ?? placement.distance ?? null);
419
+ if (orbitMetricValue !== null && orbitMetricValue > 0) {
420
+ orbitMetrics.push(orbitMetricValue);
421
+ }
422
+ continue;
423
+ }
424
+ if (placement.mode === "free") {
425
+ const freeMetric = toDistanceMetric(placement.distance ?? null);
426
+ if (freeMetric !== null && freeMetric > 0) {
427
+ orbitMetrics.push(freeMetric);
428
+ }
429
+ }
430
+ }
431
+ const maxDistanceMetric = Math.max(...orbitMetrics, 0);
432
+ const maxBodyMetric = Math.max(...bodyMetrics, 0);
433
+ const baseMetric = Math.max(maxDistanceMetric, maxBodyMetric * 6, 0);
434
+ const referenceMetric = baseMetric +
435
+ Math.max(Math.sqrt(baseMetric), maxBodyMetric * 2, maxDistanceMetric > 0 ? 0.25 : 0);
436
+ if (referenceMetric <= 0) {
437
+ return {
438
+ pixelsPerMetric: null,
439
+ hasExplicitScale: false,
440
+ };
441
+ }
442
+ const availableRadius = Math.max(Math.min(width, height) / 2 - padding - 24, 120) *
443
+ spacingFactor *
444
+ scaleModel.orbitDistanceMultiplier;
445
+ return {
446
+ pixelsPerMetric: availableRadius / referenceMetric,
447
+ hasExplicitScale: true,
448
+ };
449
+ }
398
450
  function createSceneObject(position, scaleModel, relationships) {
399
451
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
400
452
  const renderPriority = object.renderHints?.renderPriority ?? 0;
@@ -1198,7 +1250,7 @@ function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts,
1198
1250
  object,
1199
1251
  x,
1200
1252
  y,
1201
- radius: visualRadiusFor(object, depth, context.scaleModel),
1253
+ radius: visualRadiusFor(object, depth, context.scaleModel, context.sceneMetricScale),
1202
1254
  sortKey: computeSortKey(x, y, depth),
1203
1255
  });
1204
1256
  placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, depth + 1);
@@ -1209,7 +1261,7 @@ function placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, con
1209
1261
  return;
1210
1262
  }
1211
1263
  const orbiting = [...(context.orbitChildren.get(object.id) ?? [])].sort(compareOrbiting);
1212
- const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1264
+ const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel, context.sceneMetricScale);
1213
1265
  const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1214
1266
  orbiting.forEach((child, index) => {
1215
1267
  const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
@@ -1244,7 +1296,7 @@ function placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, con
1244
1296
  object: child,
1245
1297
  x,
1246
1298
  y,
1247
- radius: visualRadiusFor(child, depth + 1, context.scaleModel),
1299
+ radius: visualRadiusFor(child, depth + 1, context.scaleModel, context.sceneMetricScale),
1248
1300
  sortKey: computeSortKey(x, y, depth + 1),
1249
1301
  anchorX,
1250
1302
  anchorY,
@@ -1273,10 +1325,13 @@ function compareOrbiting(left, right) {
1273
1325
  return 1;
1274
1326
  return left.id.localeCompare(right.id);
1275
1327
  }
1276
- function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleModel) {
1328
+ function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleModel, sceneMetricScale) {
1277
1329
  const metrics = objects.map((object) => orbitMetric(object));
1278
1330
  const presentMetrics = metrics.filter((value) => value !== null);
1279
- const innerPx = parentRadius + 56 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1331
+ const minimumGapPx = scaleModel.bodyScaleMode === "strict"
1332
+ ? Math.max(2, 8 * spacingFactor * scaleModel.orbitDistanceMultiplier)
1333
+ : (objects.length > 2 ? 54 : 64) * spacingFactor * scaleModel.orbitDistanceMultiplier * 0.42;
1334
+ const innerPx = parentRadius + Math.max(minimumGapPx * 1.2, 24 * spacingFactor);
1280
1335
  const stepPx = (objects.length > 2 ? 54 : 64) * spacingFactor * scaleModel.orbitDistanceMultiplier;
1281
1336
  if (presentMetrics.length === 0) {
1282
1337
  return {
@@ -1287,7 +1342,8 @@ function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleMo
1287
1342
  innerPx,
1288
1343
  stepPx,
1289
1344
  pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1290
- minimumGapPx: stepPx * 0.42,
1345
+ minimumGapPx,
1346
+ pixelsPerMetric: sceneMetricScale.pixelsPerMetric,
1291
1347
  };
1292
1348
  }
1293
1349
  const minMetric = Math.min(...presentMetrics);
@@ -1301,7 +1357,8 @@ function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleMo
1301
1357
  innerPx,
1302
1358
  stepPx,
1303
1359
  pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1304
- minimumGapPx: stepPx * 0.42,
1360
+ minimumGapPx,
1361
+ pixelsPerMetric: sceneMetricScale.pixelsPerMetric,
1305
1362
  };
1306
1363
  }
1307
1364
  function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
@@ -1366,6 +1423,9 @@ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbit
1366
1423
  };
1367
1424
  }
1368
1425
  function resolveOrbitRadiusPx(metric, metricContext) {
1426
+ if (metricContext.pixelsPerMetric !== null) {
1427
+ return metric * metricContext.pixelsPerMetric;
1428
+ }
1369
1429
  return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
1370
1430
  }
1371
1431
  function resolveOrbitRadiiPx(objects, metricContext) {
@@ -1404,6 +1464,12 @@ function resolveBandThickness(object, orbitRadius, metricContext, scaleModel) {
1404
1464
  const outerMetric = toDistanceMetric(toUnitValue(object.properties.outer));
1405
1465
  if (innerMetric !== null && outerMetric !== null) {
1406
1466
  const thicknessMetric = Math.abs(outerMetric - innerMetric);
1467
+ if (metricContext.pixelsPerMetric !== null) {
1468
+ const thicknessPx = thicknessMetric * metricContext.pixelsPerMetric;
1469
+ return scaleModel.bodyScaleMode === "strict"
1470
+ ? Math.max(thicknessPx * scaleModel.ringThicknessMultiplier, 1)
1471
+ : clampNumber(Math.max(thicknessPx * scaleModel.ringThicknessMultiplier, 8), 8, 54);
1472
+ }
1407
1473
  if (metricContext.metricSpread > 0) {
1408
1474
  return clampNumber((thicknessMetric / metricContext.metricSpread) *
1409
1475
  metricContext.pixelSpread *
@@ -1698,8 +1764,8 @@ function deriveParentAnchor(objectId, positions, objectMap) {
1698
1764
  }
1699
1765
  return positions.get(object.placement.target);
1700
1766
  }
1701
- function visualRadiusFor(object, depth, scaleModel) {
1702
- const explicitRadius = toVisualSizeMetric(object.properties.radius, scaleModel);
1767
+ function visualRadiusFor(object, depth, scaleModel, sceneMetricScale) {
1768
+ const explicitRadius = toVisualSizeMetric(object.properties.radius, scaleModel, sceneMetricScale);
1703
1769
  if (explicitRadius !== null) {
1704
1770
  return explicitRadius;
1705
1771
  }
@@ -1766,18 +1832,31 @@ function toDistanceMetric(value) {
1766
1832
  return value.value;
1767
1833
  }
1768
1834
  }
1769
- function freePlacementOffsetPx(distance, scaleModel) {
1835
+ function freePlacementOffsetPx(distance, scaleModel, sceneMetricScale) {
1770
1836
  const metric = toDistanceMetric(distance ?? null);
1771
1837
  if (metric === null || metric <= 0) {
1772
1838
  return 0;
1773
1839
  }
1840
+ if (sceneMetricScale.pixelsPerMetric !== null) {
1841
+ const scaled = metric * sceneMetricScale.pixelsPerMetric * scaleModel.freePlacementMultiplier;
1842
+ return scaleModel.bodyScaleMode === "strict"
1843
+ ? Math.max(scaled, 0)
1844
+ : clampNumber(scaled, 0, 420);
1845
+ }
1774
1846
  return clampNumber(metric * 96 * scaleModel.freePlacementMultiplier, 0, 420);
1775
1847
  }
1776
- function toVisualSizeMetric(value, scaleModel) {
1848
+ function toVisualSizeMetric(value, scaleModel, sceneMetricScale) {
1777
1849
  const unitValue = toUnitValue(value);
1778
1850
  if (!unitValue) {
1779
1851
  return null;
1780
1852
  }
1853
+ const physicalMetric = toDistanceMetric(unitValue);
1854
+ if (sceneMetricScale.pixelsPerMetric !== null && physicalMetric !== null && physicalMetric > 0) {
1855
+ const scaled = physicalMetric * sceneMetricScale.pixelsPerMetric * scaleModel.bodyRadiusMultiplier;
1856
+ return scaleModel.bodyScaleMode === "strict"
1857
+ ? Math.max(scaled, 0.1)
1858
+ : clampNumber(Math.max(scaled, scaleModel.minBodyRadius), scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1859
+ }
1781
1860
  let size;
1782
1861
  switch (unitValue.unit) {
1783
1862
  case "sol":
@@ -1795,6 +1874,9 @@ function toVisualSizeMetric(value, scaleModel) {
1795
1874
  }
1796
1875
  return clampNumber(size * scaleModel.bodyRadiusMultiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1797
1876
  }
1877
+ function objectRadiusMetric(object) {
1878
+ return toDistanceMetric(toUnitValue(object.properties.radius));
1879
+ }
1798
1880
  function toUnitValue(value) {
1799
1881
  if (!value || typeof value !== "object" || !("value" in value)) {
1800
1882
  return null;
@@ -12,6 +12,7 @@ export function renderDocumentToSpatialScene(document, options = {}) {
12
12
  projection: options.projection,
13
13
  camera: options.camera,
14
14
  scaleModel: options.scaleModel,
15
+ bodyScaleMode: options.bodyScaleMode,
15
16
  activeEventId: options.activeEventId,
16
17
  };
17
18
  const scene = renderDocumentToScene(document, renderOptions);
@@ -8,6 +8,7 @@ export type WorldOrbitAnyDocumentVersion = WorldOrbitDocumentVersion | WorldOrbi
8
8
  export type ViewProjection = "topdown" | "isometric" | "orthographic" | "perspective";
9
9
  export type RenderProjectionFallback = "topdown" | "isometric";
10
10
  export type RenderPresetName = "diagram" | "presentation" | "atlas-card" | "markdown";
11
+ export type BodyScaleMode = "readable" | "strict";
11
12
  export interface CoordinatePoint {
12
13
  x: number;
13
14
  y: number;
@@ -263,6 +264,7 @@ export interface RenderScaleModel {
263
264
  ringThicknessMultiplier: number;
264
265
  minBodyRadius: number;
265
266
  maxBodyRadius: number;
267
+ bodyScaleMode: BodyScaleMode;
266
268
  }
267
269
  export interface SceneRenderOptions {
268
270
  width?: number;
@@ -272,6 +274,7 @@ export interface SceneRenderOptions {
272
274
  projection?: "document" | ViewProjection;
273
275
  camera?: WorldOrbitViewCamera | null;
274
276
  scaleModel?: Partial<RenderScaleModel>;
277
+ bodyScaleMode?: BodyScaleMode;
275
278
  activeEventId?: string | null;
276
279
  }
277
280
  export interface SpatialScaleModel {
@@ -1,4 +1,4 @@
1
- import { cloneAtlasDocument, createEmptyAtlasDocument, formatDocument, getAtlasDocumentNode, loadWorldOrbitSourceWithDiagnostics, materializeAtlasDocument, removeAtlasDocumentNode, resolveAtlasDiagnostics, rotatePoint, upgradeDocumentToV2, validateAtlasDocumentWithDiagnostics, } from "@worldorbit/core";
1
+ import { cloneAtlasDocument, createEmptyAtlasDocument, formatDocument, getAtlasDocumentNode, loadWorldOrbitSourceWithDiagnostics, materializeAtlasDocument, removeAtlasDocumentNode, renderDocumentToScene, resolveAtlasDiagnostics, rotatePoint, upgradeDocumentToV2, validateAtlasDocumentWithDiagnostics, } from "@worldorbit/core";
2
2
  import { renderWorldOrbitBlock } from "@worldorbit/markdown";
3
3
  import { createInteractiveViewer, } from "@worldorbit/viewer";
4
4
  import { getViewerVisibleBounds, invertViewerPoint } from "@worldorbit/viewer/viewer-state";
@@ -1250,7 +1250,7 @@ export function createWorldOrbitEditor(container, options = {}) {
1250
1250
  break;
1251
1251
  case "orbit-radius":
1252
1252
  if (details.object.placement?.mode === "orbit" && details.orbit) {
1253
- nextDocument = updateOrbitRadius(atlasDocument, dragState.path, dragState.objectId, details, pointer, dragState.orbitRadiusContext ?? null);
1253
+ nextDocument = updateOrbitRadius(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), details, pointer, dragState.orbitRadiusContext ?? null);
1254
1254
  }
1255
1255
  break;
1256
1256
  case "at-reference":
@@ -2468,7 +2468,7 @@ function updateOrbitPhase(document, path, objectId, details, pointer) {
2468
2468
  };
2469
2469
  return next;
2470
2470
  }
2471
- function updateOrbitRadius(document, path, objectId, details, pointer, dragContext) {
2471
+ function updateOrbitRadius(document, path, objectId, scene, details, pointer, dragContext) {
2472
2472
  const orbit = details.orbit;
2473
2473
  if (!orbit || details.object.placement?.mode !== "orbit" || !dragContext) {
2474
2474
  return document;
@@ -2476,25 +2476,54 @@ function updateOrbitRadius(document, path, objectId, details, pointer, dragConte
2476
2476
  const unrotated = rotatePoint(pointer, { x: orbit.cx, y: orbit.cy }, -orbit.rotationDeg);
2477
2477
  const nextDisplayedRadius = Math.max(Math.abs(unrotated.x - orbit.cx), 24);
2478
2478
  const nextBaseRadius = Math.max(nextDisplayedRadius - dragContext.radiusOffsetPx, dragContext.innerPx);
2479
- const nextMetric = orbitRadiusPxToMetric(nextBaseRadius, dragContext.innerPx, dragContext.stepPx);
2479
+ const nextMetric = orbitRadiusPxToMetric(nextBaseRadius, dragContext.innerPx, dragContext.stepPx, dragContext.mode, dragContext.pixelsPerMetric);
2480
2480
  const next = cloneAtlasDocument(document);
2481
2481
  const placementOwner = findEditablePlacementOwner(next, path, objectId);
2482
2482
  if (!placementOwner || placementOwner.placement.mode !== "orbit") {
2483
2483
  return document;
2484
2484
  }
2485
- const currentValue = placementOwner.placement.semiMajor ??
2486
- placementOwner.placement.distance ?? {
2485
+ const orbitPlacementOwner = placementOwner;
2486
+ const currentValue = orbitPlacementOwner.placement.semiMajor ??
2487
+ orbitPlacementOwner.placement.distance ?? {
2487
2488
  value: 1,
2488
2489
  unit: "au",
2489
2490
  };
2490
2491
  const scaled = distanceMetricToUnitValue(Math.max(nextMetric, 0), dragContext.preferredUnit ?? currentValue.unit);
2492
+ applyOrbitDistanceValue(orbitPlacementOwner, scaled);
2493
+ const targetDisplayedRadius = nextDisplayedRadius;
2494
+ let correctedMetric = nextMetric;
2495
+ for (let iteration = 0; iteration < 3; iteration += 1) {
2496
+ const candidateScene = renderDocumentToScene(materializeAtlasDocument(next), {
2497
+ width: scene.width,
2498
+ height: scene.height,
2499
+ padding: scene.padding,
2500
+ preset: scene.renderPreset ?? undefined,
2501
+ projection: scene.projection,
2502
+ camera: scene.camera,
2503
+ scaleModel: { ...scene.scaleModel },
2504
+ bodyScaleMode: scene.scaleModel.bodyScaleMode,
2505
+ activeEventId: scene.activeEventId,
2506
+ });
2507
+ const renderedOrbit = candidateScene.orbitVisuals.find((entry) => entry.objectId === objectId);
2508
+ const renderedRadius = renderedOrbit?.kind === "circle"
2509
+ ? renderedOrbit.radius ?? 0
2510
+ : renderedOrbit?.rx ?? 0;
2511
+ if (renderedRadius >= targetDisplayedRadius - 1) {
2512
+ break;
2513
+ }
2514
+ const correctionFactor = targetDisplayedRadius / Math.max(renderedRadius, 1);
2515
+ correctedMetric *= Math.max(correctionFactor, 1.02);
2516
+ applyOrbitDistanceValue(orbitPlacementOwner, distanceMetricToUnitValue(Math.max(correctedMetric, 0), dragContext.preferredUnit ?? currentValue.unit));
2517
+ }
2518
+ return next;
2519
+ }
2520
+ function applyOrbitDistanceValue(placementOwner, value) {
2491
2521
  if (placementOwner.placement.semiMajor) {
2492
- placementOwner.placement.semiMajor = scaled;
2522
+ placementOwner.placement.semiMajor = value;
2493
2523
  }
2494
2524
  else {
2495
- placementOwner.placement.distance = scaled;
2525
+ placementOwner.placement.distance = value;
2496
2526
  }
2497
- return next;
2498
2527
  }
2499
2528
  function updateAtReference(document, path, objectId, scene, pointer) {
2500
2529
  const candidate = findNearestAtCandidate(scene, objectId, pointer);
@@ -2533,15 +2562,26 @@ function createOrbitRadiusDragContext(document, scene, details) {
2533
2562
  !entry.hidden).length;
2534
2563
  const spacingFactor = layoutPresetSpacingForScene(scene.layoutPreset);
2535
2564
  const stepPx = (siblingCount > 2 ? 54 : 64) * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
2565
+ const minimumGapPx = scene.scaleModel.bodyScaleMode === "strict"
2566
+ ? Math.max(2, 8 * spacingFactor * scene.scaleModel.orbitDistanceMultiplier)
2567
+ : stepPx * 0.42;
2536
2568
  const innerPx = details.parent.radius +
2537
- 56 * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
2569
+ Math.max(minimumGapPx * 1.2, 24 * spacingFactor);
2538
2570
  const currentValue = details.object.placement.semiMajor ?? details.object.placement.distance ?? null;
2539
2571
  const currentMetric = unitValueToDistanceMetric(currentValue);
2540
2572
  const displayedRadius = details.orbit.kind === "circle" ? details.orbit.radius ?? 1 : details.orbit.rx ?? 1;
2541
- const baseRadius = orbitMetricToRadiusPx(currentMetric ?? 0, innerPx, stepPx);
2573
+ const usesLinearScale = currentMetric !== null && currentMetric > 0;
2574
+ const pixelsPerMetric = usesLinearScale
2575
+ ? displayedRadius / Math.max(currentMetric, 0.0001)
2576
+ : null;
2577
+ const baseRadius = usesLinearScale
2578
+ ? currentMetric * Math.max(pixelsPerMetric ?? 0, 0)
2579
+ : orbitMetricToRadiusPx(currentMetric ?? 0, innerPx, stepPx);
2542
2580
  return {
2581
+ mode: usesLinearScale ? "linear" : "log",
2543
2582
  innerPx,
2544
2583
  stepPx,
2584
+ pixelsPerMetric,
2545
2585
  radiusOffsetPx: displayedRadius - baseRadius,
2546
2586
  preferredUnit: currentValue?.unit ?? null,
2547
2587
  };
@@ -3191,7 +3231,10 @@ function distanceMetricToUnitValue(metric, unit) {
3191
3231
  function orbitMetricToRadiusPx(metric, innerPx, stepPx) {
3192
3232
  return innerPx + stepPx * log2(Math.max(metric, 0) + 1);
3193
3233
  }
3194
- function orbitRadiusPxToMetric(radiusPx, innerPx, stepPx) {
3234
+ function orbitRadiusPxToMetric(radiusPx, innerPx, stepPx, mode, pixelsPerMetric) {
3235
+ if (mode === "linear" && pixelsPerMetric !== null && pixelsPerMetric > 0) {
3236
+ return Math.max(radiusPx / pixelsPerMetric, 0);
3237
+ }
3195
3238
  if (radiusPx <= innerPx) {
3196
3239
  return 0;
3197
3240
  }
@@ -0,0 +1,3 @@
1
+ import type { WorldOrbitDiagnostic } from "@worldorbit/core/types";
2
+ export declare function formatDiagnosticLocation(diagnostic: WorldOrbitDiagnostic): string;
3
+ export declare function summarizeDiagnostics(diagnostics: WorldOrbitDiagnostic[]): string;
@@ -0,0 +1,23 @@
1
+ export function formatDiagnosticLocation(diagnostic) {
2
+ if (!diagnostic.line) {
3
+ return "No position";
4
+ }
5
+ if (!diagnostic.column) {
6
+ return `Line ${diagnostic.line}`;
7
+ }
8
+ return `Line ${diagnostic.line}, Column ${diagnostic.column}`;
9
+ }
10
+ export function summarizeDiagnostics(diagnostics) {
11
+ const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
12
+ const warnings = diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
13
+ if (!errors && !warnings) {
14
+ return "Ready";
15
+ }
16
+ if (errors && warnings) {
17
+ return `${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}`;
18
+ }
19
+ if (errors) {
20
+ return `${errors} error${errors === 1 ? "" : "s"}`;
21
+ }
22
+ return `${warnings} warning${warnings === 1 ? "" : "s"}`;
23
+ }
@@ -0,0 +1,3 @@
1
+ export declare const WORLDORBIT_HELP_TITLE = "WorldOrbit Quick Start";
2
+ export declare function buildSolarSystemExampleBlock(): string;
3
+ export declare function getQuickStartMarkdown(): string;
@@ -0,0 +1,77 @@
1
+ export const WORLDORBIT_HELP_TITLE = "WorldOrbit Quick Start";
2
+ export function buildSolarSystemExampleBlock() {
3
+ return [
4
+ "```worldorbit",
5
+ "schema 2.5",
6
+ "",
7
+ "system Sol",
8
+ ' title "Solar System"',
9
+ " referencePlane ecliptic",
10
+ "",
11
+ "defaults",
12
+ " view orthographic",
13
+ " preset atlas-card",
14
+ "",
15
+ 'group inner-system',
16
+ ' label "Inner System"',
17
+ "",
18
+ "object star Sun",
19
+ " mass 1sol",
20
+ "",
21
+ "object planet Mercury",
22
+ " orbit Sun",
23
+ " semiMajor 0.39au",
24
+ " color #b7b1a7",
25
+ " groups inner-system",
26
+ "",
27
+ "object planet Venus",
28
+ " orbit Sun",
29
+ " semiMajor 0.72au",
30
+ " color #d9b37a",
31
+ " groups inner-system",
32
+ "",
33
+ "object planet Earth",
34
+ " orbit Sun",
35
+ " semiMajor 1au",
36
+ " color #6fa8ff",
37
+ " atmosphere nitrogen-oxygen",
38
+ " groups inner-system",
39
+ "",
40
+ "object moon Luna",
41
+ " orbit Earth",
42
+ " distance 384400km",
43
+ "",
44
+ "object planet Mars",
45
+ " orbit Sun",
46
+ " semiMajor 1.52au",
47
+ " color #d97c52",
48
+ " groups inner-system",
49
+ "```",
50
+ ].join("\n");
51
+ }
52
+ export function getQuickStartMarkdown() {
53
+ return [
54
+ "Paste this into a note:",
55
+ "",
56
+ "```worldorbit",
57
+ "schema 2.5",
58
+ "",
59
+ "system Sol",
60
+ "",
61
+ "object star Sun",
62
+ "",
63
+ "object planet Earth",
64
+ " orbit Sun",
65
+ " semiMajor 1au",
66
+ "```",
67
+ "",
68
+ "Useful fields:",
69
+ "- `object planet NAME` creates a body",
70
+ "- `orbit TARGET` places it around another object",
71
+ "- `semiMajor 1au` or `distance 384400km` controls placement",
72
+ "- `color #6fa8ff` sets a display color hint",
73
+ "- `radius`, `mass`, `kind`, and `atmosphere` add more detail",
74
+ "",
75
+ "Locked mode keeps scrolling safe in notes. Activate interaction only when you want to pan or zoom the diagram.",
76
+ ].join("\n");
77
+ }
@@ -0,0 +1,9 @@
1
+ export { WorldOrbitObsidianPlugin } from "./plugin.js";
2
+ export { DEFAULT_SETTINGS } from "./settings.js";
3
+ export { formatDiagnosticLocation, summarizeDiagnostics } from "./diagnostics.js";
4
+ export { buildSolarSystemExampleBlock, getQuickStartMarkdown, WORLDORBIT_HELP_TITLE, } from "./examples.js";
5
+ export { inferFenceContentStartLine, resolveDiagnosticEditorPosition, } from "./positions.js";
6
+ export { createDiagnosticNavigationTarget, navigateToWorldOrbitDiagnostic, resolveFenceNavigationContext, } from "./navigation.js";
7
+ export { createObsidianViewerTheme } from "./theme.js";
8
+ export { WorldOrbitEmbeddedView } from "./viewer-host.js";
9
+ export type * from "./types.js";
@@ -0,0 +1,8 @@
1
+ export { WorldOrbitObsidianPlugin } from "./plugin.js";
2
+ export { DEFAULT_SETTINGS } from "./settings.js";
3
+ export { formatDiagnosticLocation, summarizeDiagnostics } from "./diagnostics.js";
4
+ export { buildSolarSystemExampleBlock, getQuickStartMarkdown, WORLDORBIT_HELP_TITLE, } from "./examples.js";
5
+ export { inferFenceContentStartLine, resolveDiagnosticEditorPosition, } from "./positions.js";
6
+ export { createDiagnosticNavigationTarget, navigateToWorldOrbitDiagnostic, resolveFenceNavigationContext, } from "./navigation.js";
7
+ export { createObsidianViewerTheme } from "./theme.js";
8
+ export { WorldOrbitEmbeddedView } from "./viewer-host.js";
@@ -0,0 +1,2 @@
1
+ import { WorldOrbitObsidianPlugin } from "./plugin.js";
2
+ export default WorldOrbitObsidianPlugin;
@@ -0,0 +1,2 @@
1
+ import { WorldOrbitObsidianPlugin } from "./plugin.js";
2
+ export default WorldOrbitObsidianPlugin;
@@ -0,0 +1,6 @@
1
+ import type { WorldOrbitDiagnostic } from "@worldorbit/core/types";
2
+ import { type App, type MarkdownPostProcessorContext } from "obsidian";
3
+ import type { BlockNavigationContext, DiagnosticNavigationTarget } from "./types.js";
4
+ export declare function resolveFenceNavigationContext(ctx: MarkdownPostProcessorContext, el: HTMLElement): BlockNavigationContext | null;
5
+ export declare function createDiagnosticNavigationTarget(contentStartLine: number | null, diagnostic: WorldOrbitDiagnostic): DiagnosticNavigationTarget;
6
+ export declare function navigateToWorldOrbitDiagnostic(app: App, sourcePath: string, contentStartLine: number, diagnostic: WorldOrbitDiagnostic): Promise<boolean>;
@@ -0,0 +1,44 @@
1
+ import { MarkdownView, TFile, } from "obsidian";
2
+ import { inferFenceContentStartLine, resolveDiagnosticEditorPosition, } from "./positions.js";
3
+ export function resolveFenceNavigationContext(ctx, el) {
4
+ if (!ctx.sourcePath) {
5
+ return null;
6
+ }
7
+ const sectionInfo = ctx.getSectionInfo(el);
8
+ if (!sectionInfo) {
9
+ return null;
10
+ }
11
+ return {
12
+ sourcePath: ctx.sourcePath,
13
+ contentStartLine: inferFenceContentStartLine(sectionInfo),
14
+ };
15
+ }
16
+ export function createDiagnosticNavigationTarget(contentStartLine, diagnostic) {
17
+ return {
18
+ diagnostic,
19
+ position: contentStartLine === null
20
+ ? null
21
+ : resolveDiagnosticEditorPosition(contentStartLine, diagnostic),
22
+ };
23
+ }
24
+ export async function navigateToWorldOrbitDiagnostic(app, sourcePath, contentStartLine, diagnostic) {
25
+ const position = resolveDiagnosticEditorPosition(contentStartLine, diagnostic);
26
+ if (!position) {
27
+ return false;
28
+ }
29
+ const file = app.vault.getAbstractFileByPath(sourcePath);
30
+ if (!(file instanceof TFile)) {
31
+ return false;
32
+ }
33
+ const leaf = app.workspace.getMostRecentLeaf() ?? app.workspace.getLeaf(true);
34
+ await leaf.openFile(file);
35
+ const view = leaf.view instanceof MarkdownView
36
+ ? leaf.view
37
+ : app.workspace.getActiveViewOfType(MarkdownView);
38
+ if (!view) {
39
+ return false;
40
+ }
41
+ view.editor.setCursor(position);
42
+ view.editor.focus();
43
+ return true;
44
+ }
@@ -0,0 +1,8 @@
1
+ import { Plugin } from "obsidian";
2
+ import type { WorldOrbitObsidianPluginSettings } from "./types.js";
3
+ export declare class WorldOrbitObsidianPlugin extends Plugin {
4
+ settings: WorldOrbitObsidianPluginSettings;
5
+ onload(): Promise<void>;
6
+ loadSettings(): Promise<void>;
7
+ saveSettings(): Promise<void>;
8
+ }