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,1484 @@
1
+ const AU_IN_KM = 149_597_870.7;
2
+ const EARTH_RADIUS_IN_KM = 6_371;
3
+ const SOLAR_RADIUS_IN_KM = 695_700;
4
+ const ISO_FLATTENING = 0.68;
5
+ const MIN_ISO_MINOR_SCALE = 0.2;
6
+ const ARC_SAMPLE_COUNT = 28;
7
+ export function renderDocumentToScene(document, options = {}) {
8
+ const frame = resolveSceneFrame(options);
9
+ const width = frame.width;
10
+ const height = frame.height;
11
+ const padding = frame.padding;
12
+ const layoutPreset = resolveLayoutPreset(document);
13
+ const projection = resolveProjection(document, options.projection);
14
+ const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
15
+ const spacingFactor = layoutPresetSpacing(layoutPreset);
16
+ const systemId = document.system?.id ?? null;
17
+ const objectMap = new Map(document.objects.map((object) => [object.id, object]));
18
+ const relationships = buildSceneRelationships(document.objects, objectMap);
19
+ const positions = new Map();
20
+ const orbitDrafts = [];
21
+ const leaderDrafts = [];
22
+ const rootObjects = [];
23
+ const freeObjects = [];
24
+ const atObjects = [];
25
+ const surfaceChildren = new Map();
26
+ const orbitChildren = new Map();
27
+ for (const object of document.objects) {
28
+ const placement = object.placement;
29
+ if (!placement) {
30
+ rootObjects.push(object);
31
+ continue;
32
+ }
33
+ if (placement.mode === "orbit") {
34
+ pushGrouped(orbitChildren, placement.target, object);
35
+ continue;
36
+ }
37
+ if (placement.mode === "surface") {
38
+ pushGrouped(surfaceChildren, placement.target, object);
39
+ continue;
40
+ }
41
+ if (placement.mode === "at") {
42
+ atObjects.push(object);
43
+ continue;
44
+ }
45
+ freeObjects.push(object);
46
+ }
47
+ const centerX = freeObjects.length > 0 ? width * 0.42 : width / 2;
48
+ const centerY = height / 2;
49
+ const context = {
50
+ orbitChildren,
51
+ surfaceChildren,
52
+ objectMap,
53
+ spacingFactor,
54
+ projection,
55
+ scaleModel,
56
+ };
57
+ const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
58
+ if (primaryRoot) {
59
+ placeObject(primaryRoot, centerX, centerY, 0, positions, orbitDrafts, leaderDrafts, context);
60
+ }
61
+ const secondaryRoots = rootObjects.filter((object) => object.id !== primaryRoot?.id);
62
+ if (secondaryRoots.length > 0) {
63
+ const rootRingRadius = Math.min(width, height) *
64
+ 0.28 *
65
+ spacingFactor *
66
+ scaleModel.orbitDistanceMultiplier;
67
+ secondaryRoots.forEach((object, index) => {
68
+ const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
69
+ const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
70
+ placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
71
+ });
72
+ }
73
+ freeObjects.forEach((object, index) => {
74
+ const x = width -
75
+ padding -
76
+ 140 -
77
+ freePlacementOffsetPx(object.placement?.mode === "free" ? object.placement.distance : undefined, scaleModel);
78
+ const rowStep = Math.max(76, ((height - padding * 2 - 180) / Math.max(1, freeObjects.length)) * spacingFactor) * scaleModel.freePlacementMultiplier;
79
+ const y = padding + 92 + index * rowStep;
80
+ positions.set(object.id, {
81
+ object,
82
+ x,
83
+ y,
84
+ radius: visualRadiusFor(object, 0, scaleModel),
85
+ sortKey: computeSortKey(x, y, 0),
86
+ });
87
+ leaderDrafts.push({
88
+ object,
89
+ groupId: relationships.groupIds.get(object.id) ?? null,
90
+ x1: x - 60,
91
+ y1: y,
92
+ x2: x - 18,
93
+ y2: y,
94
+ mode: "free",
95
+ });
96
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, 1);
97
+ });
98
+ atObjects.forEach((object, index) => {
99
+ if (positions.has(object.id) || !object.placement || object.placement.mode !== "at") {
100
+ return;
101
+ }
102
+ const resolved = resolveAtPosition(object.placement.reference, positions, objectMap, index, atObjects.length, width, height, padding, context);
103
+ positions.set(object.id, {
104
+ object,
105
+ x: resolved.x,
106
+ y: resolved.y,
107
+ radius: visualRadiusFor(object, 2, scaleModel),
108
+ sortKey: computeSortKey(resolved.x, resolved.y, 2),
109
+ anchorX: resolved.anchorX,
110
+ anchorY: resolved.anchorY,
111
+ });
112
+ if (resolved.anchorX !== undefined && resolved.anchorY !== undefined) {
113
+ leaderDrafts.push({
114
+ object,
115
+ groupId: relationships.groupIds.get(object.id) ?? null,
116
+ x1: resolved.anchorX,
117
+ y1: resolved.anchorY,
118
+ x2: resolved.x,
119
+ y2: resolved.y,
120
+ mode: "at",
121
+ });
122
+ }
123
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, 2);
124
+ });
125
+ const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
126
+ const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
127
+ const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
128
+ const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
129
+ const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
130
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
131
+ const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
132
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
133
+ return {
134
+ width,
135
+ height,
136
+ padding,
137
+ renderPreset: frame.preset,
138
+ projection,
139
+ scaleModel,
140
+ title: String(document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") ||
141
+ "WorldOrbit",
142
+ subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
143
+ systemId,
144
+ viewMode: projection,
145
+ layoutPreset,
146
+ metadata: {
147
+ format: document.format,
148
+ version: document.version,
149
+ view: projection,
150
+ scale: String(document.system?.properties.scale ?? layoutPreset),
151
+ units: String(document.system?.properties.units ?? "mixed"),
152
+ preset: frame.preset ?? "custom",
153
+ },
154
+ contentBounds,
155
+ layers,
156
+ groups,
157
+ viewpoints,
158
+ objects,
159
+ orbitVisuals,
160
+ leaders,
161
+ labels,
162
+ };
163
+ }
164
+ export function rotatePoint(point, center, rotationDeg) {
165
+ const radians = degreesToRadians(rotationDeg);
166
+ const cos = Math.cos(radians);
167
+ const sin = Math.sin(radians);
168
+ const dx = point.x - center.x;
169
+ const dy = point.y - center.y;
170
+ return {
171
+ x: center.x + dx * cos - dy * sin,
172
+ y: center.y + dx * sin + dy * cos,
173
+ };
174
+ }
175
+ function resolveLayoutPreset(document) {
176
+ const rawScale = String(document.system?.properties.scale ?? "balanced").toLowerCase();
177
+ switch (rawScale) {
178
+ case "compressed":
179
+ case "compact":
180
+ return "compact";
181
+ case "expanded":
182
+ case "presentation":
183
+ return "presentation";
184
+ default:
185
+ return "balanced";
186
+ }
187
+ }
188
+ function resolveSceneFrame(options) {
189
+ const defaults = scenePresetDefaults(options.preset);
190
+ return {
191
+ width: options.width ?? defaults.width,
192
+ height: options.height ?? defaults.height,
193
+ padding: options.padding ?? defaults.padding,
194
+ preset: options.preset ?? null,
195
+ };
196
+ }
197
+ function scenePresetDefaults(preset) {
198
+ switch (preset) {
199
+ case "presentation":
200
+ return { width: 1440, height: 900, padding: 88 };
201
+ case "atlas-card":
202
+ return { width: 960, height: 560, padding: 56 };
203
+ case "markdown":
204
+ return { width: 920, height: 540, padding: 48 };
205
+ case "diagram":
206
+ default:
207
+ return { width: 1200, height: 780, padding: 72 };
208
+ }
209
+ }
210
+ function resolveProjection(document, projection) {
211
+ if (projection === "topdown" || projection === "isometric") {
212
+ return projection;
213
+ }
214
+ return String(document.system?.properties.view ?? "topdown").toLowerCase() === "isometric"
215
+ ? "isometric"
216
+ : "topdown";
217
+ }
218
+ function resolveScaleModel(layoutPreset, overrides) {
219
+ const defaults = defaultScaleModel(layoutPreset);
220
+ return {
221
+ ...defaults,
222
+ ...overrides,
223
+ };
224
+ }
225
+ function defaultScaleModel(layoutPreset) {
226
+ switch (layoutPreset) {
227
+ case "compact":
228
+ return {
229
+ orbitDistanceMultiplier: 0.84,
230
+ bodyRadiusMultiplier: 0.92,
231
+ labelMultiplier: 0.9,
232
+ freePlacementMultiplier: 0.9,
233
+ ringThicknessMultiplier: 0.92,
234
+ minBodyRadius: 4,
235
+ maxBodyRadius: 36,
236
+ };
237
+ case "presentation":
238
+ return {
239
+ orbitDistanceMultiplier: 1.2,
240
+ bodyRadiusMultiplier: 1.18,
241
+ labelMultiplier: 1.08,
242
+ freePlacementMultiplier: 1.05,
243
+ ringThicknessMultiplier: 1.16,
244
+ minBodyRadius: 5,
245
+ maxBodyRadius: 48,
246
+ };
247
+ default:
248
+ return {
249
+ orbitDistanceMultiplier: 1,
250
+ bodyRadiusMultiplier: 1,
251
+ labelMultiplier: 1,
252
+ freePlacementMultiplier: 1,
253
+ ringThicknessMultiplier: 1,
254
+ minBodyRadius: 4,
255
+ maxBodyRadius: 40,
256
+ };
257
+ }
258
+ }
259
+ function layoutPresetSpacing(layoutPreset) {
260
+ switch (layoutPreset) {
261
+ case "compact":
262
+ return 0.84;
263
+ case "presentation":
264
+ return 1.2;
265
+ default:
266
+ return 1;
267
+ }
268
+ }
269
+ function createSceneObject(position, scaleModel, relationships) {
270
+ const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
271
+ return {
272
+ renderId: createRenderId(object.id),
273
+ objectId: object.id,
274
+ object,
275
+ parentId: relationships.parentIds.get(object.id) ?? null,
276
+ ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
277
+ childIds: relationships.childIds.get(object.id) ?? [],
278
+ groupId: relationships.groupIds.get(object.id) ?? null,
279
+ x,
280
+ y,
281
+ radius,
282
+ visualRadius: visualExtentForObject(object, radius, scaleModel),
283
+ sortKey,
284
+ anchorX,
285
+ anchorY,
286
+ label: object.id,
287
+ secondaryLabel: object.type === "structure" ? String(object.properties.kind ?? object.type) : object.type,
288
+ fillColor: customColorFor(object.properties.color),
289
+ imageHref: typeof object.properties.image === "string" && object.properties.image.trim()
290
+ ? object.properties.image
291
+ : undefined,
292
+ hidden: object.properties.hidden === true,
293
+ };
294
+ }
295
+ function createOrbitVisual(draft, groupId) {
296
+ return {
297
+ renderId: `${createRenderId(draft.object.id)}-orbit`,
298
+ objectId: draft.object.id,
299
+ object: draft.object,
300
+ parentId: draft.parentId,
301
+ groupId,
302
+ kind: draft.kind,
303
+ cx: draft.cx,
304
+ cy: draft.cy,
305
+ radius: draft.radius,
306
+ rx: draft.rx,
307
+ ry: draft.ry,
308
+ rotationDeg: draft.rotationDeg,
309
+ band: draft.band,
310
+ bandThickness: draft.bandThickness,
311
+ frontArcPath: draft.frontArcPath,
312
+ backArcPath: draft.backArcPath,
313
+ hidden: draft.object.properties.hidden === true,
314
+ };
315
+ }
316
+ function createLeaderLine(draft) {
317
+ return {
318
+ renderId: `${createRenderId(draft.object.id)}-leader-${draft.mode}`,
319
+ objectId: draft.object.id,
320
+ object: draft.object,
321
+ groupId: draft.groupId,
322
+ x1: draft.x1,
323
+ y1: draft.y1,
324
+ x2: draft.x2,
325
+ y2: draft.y2,
326
+ mode: draft.mode,
327
+ hidden: draft.object.properties.hidden === true,
328
+ };
329
+ }
330
+ function createSceneLabels(objects, sceneHeight, labelMultiplier) {
331
+ const labels = [];
332
+ const occupied = [];
333
+ const visibleObjects = [...objects]
334
+ .filter((object) => !object.hidden)
335
+ .sort((left, right) => left.sortKey - right.sortKey);
336
+ for (const object of visibleObjects) {
337
+ const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
338
+ const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
339
+ let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
340
+ let secondaryY = labelY + direction * (16 * labelMultiplier);
341
+ let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
342
+ let attempts = 0;
343
+ while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
344
+ labelY += direction * 14 * labelMultiplier;
345
+ secondaryY += direction * 14 * labelMultiplier;
346
+ bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
347
+ attempts += 1;
348
+ }
349
+ occupied.push(bounds);
350
+ labels.push({
351
+ renderId: `${object.renderId}-label`,
352
+ objectId: object.objectId,
353
+ object: object.object,
354
+ groupId: object.groupId,
355
+ label: object.label,
356
+ secondaryLabel: object.secondaryLabel,
357
+ x: object.x,
358
+ y: labelY,
359
+ secondaryY,
360
+ textAnchor: "middle",
361
+ direction: direction < 0 ? "above" : "below",
362
+ hidden: object.hidden,
363
+ });
364
+ }
365
+ return labels;
366
+ }
367
+ function createSceneLayers(orbitVisuals, leaders, objects, labels) {
368
+ const backOrbitIds = orbitVisuals
369
+ .filter((visual) => !visual.hidden && Boolean(visual.backArcPath))
370
+ .map((visual) => visual.renderId);
371
+ const frontOrbitIds = orbitVisuals
372
+ .filter((visual) => !visual.hidden)
373
+ .map((visual) => visual.renderId);
374
+ return [
375
+ { id: "background", renderIds: ["wo-bg", "wo-bg-glow", "wo-grid"] },
376
+ {
377
+ id: "guides",
378
+ renderIds: leaders.filter((leader) => !leader.hidden).map((leader) => leader.renderId),
379
+ },
380
+ { id: "orbits-back", renderIds: backOrbitIds },
381
+ { id: "orbits-front", renderIds: frontOrbitIds },
382
+ {
383
+ id: "objects",
384
+ renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId),
385
+ },
386
+ {
387
+ id: "labels",
388
+ renderIds: labels.filter((label) => !label.hidden).map((label) => label.renderId),
389
+ },
390
+ { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] },
391
+ ];
392
+ }
393
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
394
+ const groups = new Map();
395
+ const ensureGroup = (groupId) => {
396
+ if (!groupId) {
397
+ return null;
398
+ }
399
+ const existing = groups.get(groupId);
400
+ if (existing) {
401
+ return existing;
402
+ }
403
+ const rootObjectId = relationships.groupRoots.get(groupId) ?? null;
404
+ const created = {
405
+ renderId: groupId,
406
+ rootObjectId,
407
+ label: rootObjectId ?? groupId,
408
+ objectIds: [],
409
+ orbitIds: [],
410
+ labelIds: [],
411
+ leaderIds: [],
412
+ contentBounds: createBounds(0, 0, 0, 0),
413
+ };
414
+ groups.set(groupId, created);
415
+ return created;
416
+ };
417
+ for (const object of objects) {
418
+ const group = ensureGroup(object.groupId);
419
+ if (group && !object.hidden) {
420
+ group.objectIds.push(object.objectId);
421
+ }
422
+ }
423
+ for (const orbit of orbitVisuals) {
424
+ const group = ensureGroup(orbit.groupId);
425
+ if (group && !orbit.hidden) {
426
+ group.orbitIds.push(orbit.objectId);
427
+ }
428
+ }
429
+ for (const leader of leaders) {
430
+ const group = ensureGroup(leader.groupId);
431
+ if (group && !leader.hidden) {
432
+ group.leaderIds.push(leader.objectId);
433
+ }
434
+ }
435
+ for (const label of labels) {
436
+ const group = ensureGroup(label.groupId);
437
+ if (group && !label.hidden) {
438
+ group.labelIds.push(label.objectId);
439
+ }
440
+ }
441
+ for (const group of groups.values()) {
442
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
443
+ }
444
+ return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
445
+ }
446
+ function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
447
+ const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
448
+ const drafts = new Map();
449
+ for (const [key, value] of Object.entries(document.system?.info ?? {})) {
450
+ if (!key.startsWith("viewpoint.")) {
451
+ continue;
452
+ }
453
+ const [prefix, rawId, ...fieldParts] = key.split(".");
454
+ if (prefix !== "viewpoint" || !rawId || fieldParts.length === 0) {
455
+ continue;
456
+ }
457
+ const id = normalizeViewpointId(rawId);
458
+ if (!id) {
459
+ continue;
460
+ }
461
+ const field = fieldParts.join(".").toLowerCase();
462
+ const draft = drafts.get(id) ?? { id };
463
+ applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
464
+ drafts.set(id, draft);
465
+ }
466
+ const viewpoints = [...drafts.values()]
467
+ .map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap))
468
+ .filter(Boolean);
469
+ const overviewIndex = viewpoints.findIndex((viewpoint) => viewpoint.id === generatedOverview.id);
470
+ if (overviewIndex >= 0) {
471
+ viewpoints.splice(overviewIndex, 1, {
472
+ ...generatedOverview,
473
+ ...viewpoints[overviewIndex],
474
+ layers: {
475
+ ...generatedOverview.layers,
476
+ ...viewpoints[overviewIndex].layers,
477
+ },
478
+ filter: viewpoints[overviewIndex].filter ?? generatedOverview.filter,
479
+ generated: false,
480
+ });
481
+ }
482
+ else {
483
+ viewpoints.unshift(generatedOverview);
484
+ }
485
+ return viewpoints.sort((left, right) => {
486
+ if (left.id === "overview")
487
+ return -1;
488
+ if (right.id === "overview")
489
+ return 1;
490
+ return left.label.localeCompare(right.label);
491
+ });
492
+ }
493
+ function createGeneratedOverviewViewpoint(document, projection, preset) {
494
+ const label = document.system?.properties.title
495
+ ? `${String(document.system.properties.title)} Overview`
496
+ : "Overview";
497
+ return {
498
+ id: "overview",
499
+ label,
500
+ summary: "Fit the whole system with the current atlas defaults.",
501
+ objectId: null,
502
+ selectedObjectId: null,
503
+ projection,
504
+ preset,
505
+ rotationDeg: 0,
506
+ scale: null,
507
+ layers: {},
508
+ filter: null,
509
+ generated: true,
510
+ };
511
+ }
512
+ function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
513
+ const normalizedValue = value.trim();
514
+ switch (field) {
515
+ case "label":
516
+ case "title":
517
+ if (normalizedValue) {
518
+ draft.label = normalizedValue;
519
+ }
520
+ return;
521
+ case "summary":
522
+ case "description":
523
+ if (normalizedValue) {
524
+ draft.summary = normalizedValue;
525
+ }
526
+ return;
527
+ case "focus":
528
+ case "object":
529
+ if (normalizedValue) {
530
+ draft.focus = normalizedValue;
531
+ }
532
+ return;
533
+ case "select":
534
+ case "selection":
535
+ if (normalizedValue) {
536
+ draft.select = normalizedValue;
537
+ }
538
+ return;
539
+ case "projection":
540
+ case "view":
541
+ draft.projection = parseViewProjection(normalizedValue) ?? projection;
542
+ return;
543
+ case "preset":
544
+ draft.preset = parseRenderPreset(normalizedValue) ?? preset;
545
+ return;
546
+ case "rotation":
547
+ case "angle":
548
+ draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
549
+ return;
550
+ case "zoom":
551
+ case "scale":
552
+ draft.scale = parsePositiveNumber(normalizedValue);
553
+ return;
554
+ case "layers":
555
+ draft.layers = parseViewpointLayers(normalizedValue);
556
+ return;
557
+ case "query":
558
+ draft.filter = {
559
+ ...(draft.filter ?? createEmptyViewpointFilter()),
560
+ query: normalizedValue || null,
561
+ };
562
+ return;
563
+ case "types":
564
+ case "objecttypes":
565
+ draft.filter = {
566
+ ...(draft.filter ?? createEmptyViewpointFilter()),
567
+ objectTypes: parseViewpointObjectTypes(normalizedValue),
568
+ };
569
+ return;
570
+ case "tags":
571
+ draft.filter = {
572
+ ...(draft.filter ?? createEmptyViewpointFilter()),
573
+ tags: splitListValue(normalizedValue),
574
+ };
575
+ return;
576
+ case "groups":
577
+ draft.filter = {
578
+ ...(draft.filter ?? createEmptyViewpointFilter()),
579
+ groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap),
580
+ };
581
+ return;
582
+ }
583
+ }
584
+ function finalizeViewpointDraft(draft, projection, preset, objectMap) {
585
+ const objectId = draft.focus && objectMap.has(draft.focus) ? draft.focus : null;
586
+ const selectedObjectId = draft.select && objectMap.has(draft.select)
587
+ ? draft.select
588
+ : objectId;
589
+ const filter = normalizeViewpointFilter(draft.filter);
590
+ const label = draft.label?.trim() || humanizeIdentifier(draft.id);
591
+ return {
592
+ id: draft.id,
593
+ label,
594
+ summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
595
+ objectId,
596
+ selectedObjectId,
597
+ projection: draft.projection ?? projection,
598
+ preset: draft.preset ?? preset,
599
+ rotationDeg: draft.rotationDeg ?? 0,
600
+ scale: draft.scale ?? null,
601
+ layers: draft.layers ?? {},
602
+ filter,
603
+ generated: false,
604
+ };
605
+ }
606
+ function createEmptyViewpointFilter() {
607
+ return {
608
+ query: null,
609
+ objectTypes: [],
610
+ tags: [],
611
+ groupIds: [],
612
+ };
613
+ }
614
+ function normalizeViewpointFilter(filter) {
615
+ if (!filter) {
616
+ return null;
617
+ }
618
+ const normalized = {
619
+ query: filter.query?.trim() || null,
620
+ objectTypes: [...new Set(filter.objectTypes)],
621
+ tags: [...new Set(filter.tags)],
622
+ groupIds: [...new Set(filter.groupIds)],
623
+ };
624
+ return normalized.query ||
625
+ normalized.objectTypes.length > 0 ||
626
+ normalized.tags.length > 0 ||
627
+ normalized.groupIds.length > 0
628
+ ? normalized
629
+ : null;
630
+ }
631
+ function parseViewProjection(value) {
632
+ return value.toLowerCase() === "isometric"
633
+ ? "isometric"
634
+ : value.toLowerCase() === "topdown"
635
+ ? "topdown"
636
+ : null;
637
+ }
638
+ function parseRenderPreset(value) {
639
+ const normalized = value.toLowerCase();
640
+ if (normalized === "diagram" ||
641
+ normalized === "presentation" ||
642
+ normalized === "atlas-card" ||
643
+ normalized === "markdown") {
644
+ return normalized;
645
+ }
646
+ return null;
647
+ }
648
+ function parseFiniteNumber(value) {
649
+ const parsed = Number(value);
650
+ return Number.isFinite(parsed) ? parsed : null;
651
+ }
652
+ function parsePositiveNumber(value) {
653
+ const parsed = parseFiniteNumber(value);
654
+ return parsed !== null && parsed > 0 ? parsed : null;
655
+ }
656
+ function parseViewpointLayers(value) {
657
+ const next = {};
658
+ for (const token of splitListValue(value)) {
659
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
660
+ const rawLayer = token.replace(/^[-!]+/, "").toLowerCase();
661
+ if (rawLayer === "orbits") {
662
+ next["orbits-back"] = enabled;
663
+ next["orbits-front"] = enabled;
664
+ continue;
665
+ }
666
+ if (rawLayer === "background" ||
667
+ rawLayer === "guides" ||
668
+ rawLayer === "orbits-back" ||
669
+ rawLayer === "orbits-front" ||
670
+ rawLayer === "objects" ||
671
+ rawLayer === "labels" ||
672
+ rawLayer === "metadata") {
673
+ next[rawLayer] = enabled;
674
+ }
675
+ }
676
+ return next;
677
+ }
678
+ function parseViewpointObjectTypes(value) {
679
+ return splitListValue(value).filter((entry) => entry === "star" ||
680
+ entry === "planet" ||
681
+ entry === "moon" ||
682
+ entry === "belt" ||
683
+ entry === "asteroid" ||
684
+ entry === "comet" ||
685
+ entry === "ring" ||
686
+ entry === "structure" ||
687
+ entry === "phenomenon");
688
+ }
689
+ function parseViewpointGroups(value, relationships, objectMap) {
690
+ return splitListValue(value).map((entry) => {
691
+ if (entry.startsWith("wo-") && entry.endsWith("-group")) {
692
+ return entry;
693
+ }
694
+ if (relationships.groupIds.has(entry)) {
695
+ return relationships.groupIds.get(entry) ?? createGroupId(entry);
696
+ }
697
+ return objectMap.has(entry) ? createGroupId(entry) : createGroupId(entry);
698
+ });
699
+ }
700
+ function splitListValue(value) {
701
+ return value
702
+ .split(/[\s,]+/)
703
+ .map((entry) => entry.trim())
704
+ .filter(Boolean);
705
+ }
706
+ function normalizeViewpointId(value) {
707
+ return value
708
+ .trim()
709
+ .toLowerCase()
710
+ .replace(/[^a-z0-9_-]+/g, "-")
711
+ .replace(/^-+|-+$/g, "");
712
+ }
713
+ function humanizeIdentifier(value) {
714
+ return value
715
+ .split(/[-_]+/)
716
+ .filter(Boolean)
717
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
718
+ .join(" ");
719
+ }
720
+ function createViewpointSummary(label, objectId, filter) {
721
+ const parts = [label];
722
+ if (objectId) {
723
+ parts.push(`focus ${objectId}`);
724
+ }
725
+ if (filter?.objectTypes.length) {
726
+ parts.push(filter.objectTypes.join("/"));
727
+ }
728
+ if (filter?.tags.length) {
729
+ parts.push(`tags ${filter.tags.join(", ")}`);
730
+ }
731
+ if (filter?.query) {
732
+ parts.push(`query "${filter.query}"`);
733
+ }
734
+ return parts.join(" - ");
735
+ }
736
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
737
+ let minX = Number.POSITIVE_INFINITY;
738
+ let minY = Number.POSITIVE_INFINITY;
739
+ let maxX = Number.NEGATIVE_INFINITY;
740
+ let maxY = Number.NEGATIVE_INFINITY;
741
+ const include = (x, y) => {
742
+ minX = Math.min(minX, x);
743
+ minY = Math.min(minY, y);
744
+ maxX = Math.max(maxX, x);
745
+ maxY = Math.max(maxY, y);
746
+ };
747
+ for (const orbit of orbitVisuals) {
748
+ if (orbit.hidden)
749
+ continue;
750
+ includeOrbitBounds(orbit, include);
751
+ }
752
+ for (const leader of leaders) {
753
+ if (leader.hidden)
754
+ continue;
755
+ include(leader.x1, leader.y1);
756
+ include(leader.x2, leader.y2);
757
+ }
758
+ for (const object of objects) {
759
+ if (object.hidden)
760
+ continue;
761
+ includeObjectBounds(object, include);
762
+ }
763
+ for (const label of labels) {
764
+ if (label.hidden)
765
+ continue;
766
+ includeLabelBounds(label, include);
767
+ }
768
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
769
+ return createBounds(0, 0, width, height);
770
+ }
771
+ return createBounds(minX, minY, maxX, maxY);
772
+ }
773
+ function includeOrbitBounds(orbit, include) {
774
+ const strokePadding = orbit.bandThickness !== undefined
775
+ ? orbit.bandThickness / 2 + 4
776
+ : orbit.band
777
+ ? 10
778
+ : 3;
779
+ if (orbit.kind === "circle" && orbit.radius !== undefined) {
780
+ include(orbit.cx - orbit.radius - strokePadding, orbit.cy - orbit.radius - strokePadding);
781
+ include(orbit.cx + orbit.radius + strokePadding, orbit.cy + orbit.radius + strokePadding);
782
+ return;
783
+ }
784
+ const rx = orbit.rx ?? orbit.radius ?? 0;
785
+ const ry = orbit.ry ?? orbit.radius ?? 0;
786
+ const points = sampleEllipseArcPoints(orbit.cx, orbit.cy, rx, ry, orbit.rotationDeg, 0, Math.PI * 2, ARC_SAMPLE_COUNT * 2);
787
+ for (const point of points) {
788
+ include(point.x - strokePadding, point.y - strokePadding);
789
+ include(point.x + strokePadding, point.y + strokePadding);
790
+ }
791
+ }
792
+ function createBounds(minX, minY, maxX, maxY) {
793
+ return {
794
+ minX,
795
+ minY,
796
+ maxX,
797
+ maxY,
798
+ width: maxX - minX,
799
+ height: maxY - minY,
800
+ centerX: minX + (maxX - minX) / 2,
801
+ centerY: minY + (maxY - minY) / 2,
802
+ };
803
+ }
804
+ function includeObjectBounds(object, include) {
805
+ include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
806
+ include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
807
+ }
808
+ function includeLabelBounds(label, include) {
809
+ const labelScale = 1;
810
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
811
+ include(label.x - labelHalfWidth, label.y - 18);
812
+ include(label.x + labelHalfWidth, label.y + 8);
813
+ include(label.x - labelHalfWidth, label.secondaryY - 14);
814
+ include(label.x + labelHalfWidth, label.secondaryY + 8);
815
+ }
816
+ function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
817
+ if (positions.has(object.id)) {
818
+ return;
819
+ }
820
+ positions.set(object.id, {
821
+ object,
822
+ x,
823
+ y,
824
+ radius: visualRadiusFor(object, depth, context.scaleModel),
825
+ sortKey: computeSortKey(x, y, depth),
826
+ });
827
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, depth + 1);
828
+ }
829
+ function placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, depth) {
830
+ const parent = positions.get(object.id);
831
+ if (!parent) {
832
+ return;
833
+ }
834
+ const orbiting = [...(context.orbitChildren.get(object.id) ?? [])].sort(compareOrbiting);
835
+ const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
836
+ orbiting.forEach((child, index) => {
837
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
838
+ orbitDrafts.push({
839
+ object: child,
840
+ parentId: object.id,
841
+ kind: orbitGeometry.kind,
842
+ cx: orbitGeometry.cx,
843
+ cy: orbitGeometry.cy,
844
+ radius: orbitGeometry.radius,
845
+ rx: orbitGeometry.rx,
846
+ ry: orbitGeometry.ry,
847
+ rotationDeg: orbitGeometry.rotationDeg,
848
+ band: orbitGeometry.band,
849
+ bandThickness: orbitGeometry.bandThickness,
850
+ frontArcPath: orbitGeometry.frontArcPath,
851
+ backArcPath: orbitGeometry.backArcPath,
852
+ });
853
+ placeObject(child, orbitGeometry.objectX, orbitGeometry.objectY, depth, positions, orbitDrafts, leaderDrafts, context);
854
+ });
855
+ const surfaceObjects = [...(context.surfaceChildren.get(object.id) ?? [])];
856
+ surfaceObjects.forEach((child, index) => {
857
+ const angle = angleForIndex(index, surfaceObjects.length, -Math.PI / 3);
858
+ const leaderDistance = 28 * context.spacingFactor;
859
+ const anchorOffset = projectPolarOffset(angle, parent.radius, context.projection, context.projection === "isometric" ? 0.9 : 1);
860
+ const bodyOffset = projectPolarOffset(angle, parent.radius + leaderDistance, context.projection, context.projection === "isometric" ? 0.9 : 1);
861
+ const anchorX = parent.x + anchorOffset.x;
862
+ const anchorY = parent.y + anchorOffset.y;
863
+ const x = parent.x + bodyOffset.x;
864
+ const y = parent.y + bodyOffset.y;
865
+ positions.set(child.id, {
866
+ object: child,
867
+ x,
868
+ y,
869
+ radius: visualRadiusFor(child, depth + 1, context.scaleModel),
870
+ sortKey: computeSortKey(x, y, depth + 1),
871
+ anchorX,
872
+ anchorY,
873
+ });
874
+ leaderDrafts.push({
875
+ object: child,
876
+ groupId: context.objectMap.has(child.id) ? createGroupId(resolveGroupRootObjectId(child, context.objectMap)) : null,
877
+ x1: anchorX,
878
+ y1: anchorY,
879
+ x2: x,
880
+ y2: y,
881
+ mode: "surface",
882
+ });
883
+ placeOrbitingChildren(child, positions, orbitDrafts, leaderDrafts, context, depth + 1);
884
+ });
885
+ }
886
+ function compareOrbiting(left, right) {
887
+ const leftMetric = orbitMetric(left);
888
+ const rightMetric = orbitMetric(right);
889
+ if (leftMetric !== null && rightMetric !== null && leftMetric !== rightMetric) {
890
+ return leftMetric - rightMetric;
891
+ }
892
+ if (leftMetric !== null && rightMetric === null)
893
+ return -1;
894
+ if (leftMetric === null && rightMetric !== null)
895
+ return 1;
896
+ return left.id.localeCompare(right.id);
897
+ }
898
+ function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleModel) {
899
+ const metrics = objects.map((object) => orbitMetric(object));
900
+ const presentMetrics = metrics.filter((value) => value !== null);
901
+ const innerPx = parentRadius + 56 * spacingFactor * scaleModel.orbitDistanceMultiplier;
902
+ const stepPx = (objects.length > 2 ? 54 : 64) * spacingFactor * scaleModel.orbitDistanceMultiplier;
903
+ if (presentMetrics.length === 0) {
904
+ return {
905
+ metrics,
906
+ minMetric: 0,
907
+ maxMetric: 0,
908
+ metricSpread: 0,
909
+ innerPx,
910
+ stepPx,
911
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
912
+ };
913
+ }
914
+ const minMetric = Math.min(...presentMetrics);
915
+ const maxMetric = Math.max(...presentMetrics);
916
+ const metricSpread = maxMetric - minMetric;
917
+ return {
918
+ metrics,
919
+ minMetric,
920
+ maxMetric,
921
+ metricSpread,
922
+ innerPx,
923
+ stepPx,
924
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
925
+ };
926
+ }
927
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
928
+ const placement = object.placement;
929
+ const band = object.type === "belt" || object.type === "ring";
930
+ if (!placement || placement.mode !== "orbit") {
931
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
932
+ return {
933
+ kind: "circle",
934
+ cx: parent.x,
935
+ cy: parent.y,
936
+ radius: fallbackRadius,
937
+ rotationDeg: 0,
938
+ band,
939
+ bandThickness: band
940
+ ? 12 * context.scaleModel.ringThicknessMultiplier
941
+ : undefined,
942
+ objectX: parent.x,
943
+ objectY: parent.y - fallbackRadius,
944
+ };
945
+ }
946
+ const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
947
+ const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
948
+ const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
949
+ const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
950
+ const inclinationScale = context.projection === "isometric"
951
+ ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) *
952
+ ISO_FLATTENING
953
+ : 1;
954
+ const semiMinor = Math.max(baseMinor * inclinationScale, semiMajor * 0.14);
955
+ const rotationDeg = unitValueToDegrees(placement.angle) ?? 0;
956
+ const focusOffset = semiMajor * eccentricity;
957
+ const centerOffset = rotateOffset(-focusOffset, 0, rotationDeg);
958
+ const cx = parent.x + centerOffset.x;
959
+ const cy = parent.y + centerOffset.y;
960
+ const phase = resolveOrbitPhase(placement.phase, index, count);
961
+ const objectPoint = ellipsePoint(cx, cy, semiMajor, semiMinor, rotationDeg, phase);
962
+ const useCircle = context.projection === "topdown" &&
963
+ eccentricity <= 0.0001 &&
964
+ Math.abs(rotationDeg) <= 0.0001;
965
+ const bandThickness = band
966
+ ? resolveBandThickness(object, semiMajor, metricContext, context.scaleModel)
967
+ : undefined;
968
+ return {
969
+ kind: useCircle ? "circle" : "ellipse",
970
+ cx: useCircle ? parent.x : cx,
971
+ cy: useCircle ? parent.y : cy,
972
+ radius: useCircle ? semiMajor : undefined,
973
+ rx: useCircle ? undefined : semiMajor,
974
+ ry: useCircle ? undefined : semiMinor,
975
+ rotationDeg,
976
+ band,
977
+ bandThickness,
978
+ frontArcPath: context.projection === "isometric" || band
979
+ ? buildEllipseArcPath(cx, cy, semiMajor, semiMinor, rotationDeg, 0, Math.PI)
980
+ : undefined,
981
+ backArcPath: context.projection === "isometric" || band
982
+ ? buildEllipseArcPath(cx, cy, semiMajor, semiMinor, rotationDeg, Math.PI, Math.PI * 2)
983
+ : undefined,
984
+ objectX: objectPoint.x,
985
+ objectY: objectPoint.y,
986
+ };
987
+ }
988
+ function resolveOrbitRadiusPx(object, index, metricContext) {
989
+ const metric = orbitMetric(object);
990
+ if (metric === null) {
991
+ return metricContext.innerPx + index * metricContext.stepPx;
992
+ }
993
+ if (metricContext.metricSpread > 0) {
994
+ return (metricContext.innerPx +
995
+ ((metric - metricContext.minMetric) / metricContext.metricSpread) *
996
+ metricContext.pixelSpread);
997
+ }
998
+ return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
999
+ }
1000
+ function orbitMetric(object) {
1001
+ if (!object.placement || object.placement.mode !== "orbit") {
1002
+ return null;
1003
+ }
1004
+ return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1005
+ }
1006
+ function resolveOrbitPhase(phase, index, count) {
1007
+ const degreeValue = phase ? unitValueToDegrees(phase) : null;
1008
+ if (degreeValue !== null) {
1009
+ return degreesToRadians(degreeValue - 90);
1010
+ }
1011
+ return angleForIndex(index, count, -Math.PI / 2);
1012
+ }
1013
+ function resolveBandThickness(object, orbitRadius, metricContext, scaleModel) {
1014
+ const innerMetric = toDistanceMetric(toUnitValue(object.properties.inner));
1015
+ const outerMetric = toDistanceMetric(toUnitValue(object.properties.outer));
1016
+ if (innerMetric !== null && outerMetric !== null) {
1017
+ const thicknessMetric = Math.abs(outerMetric - innerMetric);
1018
+ if (metricContext.metricSpread > 0) {
1019
+ return clampNumber((thicknessMetric / metricContext.metricSpread) *
1020
+ metricContext.pixelSpread *
1021
+ scaleModel.ringThicknessMultiplier, 8, 54);
1022
+ }
1023
+ const referenceMetric = Math.max(Math.max(innerMetric, outerMetric), 0.0001);
1024
+ return clampNumber((thicknessMetric / referenceMetric) *
1025
+ orbitRadius *
1026
+ 0.75 *
1027
+ scaleModel.ringThicknessMultiplier, 8, 48);
1028
+ }
1029
+ const fallbackBase = object.type === "belt" ? 18 : 12;
1030
+ return fallbackBase * scaleModel.ringThicknessMultiplier;
1031
+ }
1032
+ function resolveAtPosition(reference, positions, objectMap, index, count, width, height, padding, context) {
1033
+ if (reference.kind === "lagrange") {
1034
+ return resolveLagrangePosition(reference, positions, objectMap, width, height);
1035
+ }
1036
+ if (reference.kind === "anchor") {
1037
+ const anchor = positions.get(reference.objectId);
1038
+ if (anchor) {
1039
+ const angle = angleForIndex(index, count, Math.PI / 5);
1040
+ const distance = (anchor.radius + 36) * context.scaleModel.labelMultiplier;
1041
+ const offset = projectPolarOffset(angle, distance, context.projection, context.projection === "isometric" ? 0.92 : 1);
1042
+ return {
1043
+ x: anchor.x + offset.x,
1044
+ y: anchor.y + offset.y,
1045
+ anchorX: anchor.x,
1046
+ anchorY: anchor.y,
1047
+ };
1048
+ }
1049
+ }
1050
+ if (reference.kind === "named") {
1051
+ const anchor = positions.get(reference.name);
1052
+ if (anchor) {
1053
+ const angle = angleForIndex(index, count, Math.PI / 6);
1054
+ const distance = (anchor.radius + 36) * context.scaleModel.labelMultiplier;
1055
+ const offset = projectPolarOffset(angle, distance, context.projection, context.projection === "isometric" ? 0.92 : 1);
1056
+ return {
1057
+ x: anchor.x + offset.x,
1058
+ y: anchor.y + offset.y,
1059
+ anchorX: anchor.x,
1060
+ anchorY: anchor.y,
1061
+ };
1062
+ }
1063
+ }
1064
+ return {
1065
+ x: width - padding - 170,
1066
+ y: height -
1067
+ padding -
1068
+ 86 -
1069
+ index * 58 * context.scaleModel.freePlacementMultiplier,
1070
+ };
1071
+ }
1072
+ function resolveLagrangePosition(reference, positions, objectMap, width, height) {
1073
+ const primary = reference.secondary
1074
+ ? positions.get(reference.primary)
1075
+ : deriveParentAnchor(reference.primary, positions, objectMap);
1076
+ const secondary = positions.get(reference.secondary ?? reference.primary);
1077
+ if (!primary || !secondary) {
1078
+ return {
1079
+ x: width * 0.7,
1080
+ y: height * 0.25,
1081
+ };
1082
+ }
1083
+ const dx = secondary.x - primary.x;
1084
+ const dy = secondary.y - primary.y;
1085
+ const distance = Math.hypot(dx, dy) || 1;
1086
+ const ux = dx / distance;
1087
+ const uy = dy / distance;
1088
+ const nx = -uy;
1089
+ const ny = ux;
1090
+ const offset = clampNumber(distance * 0.25, 24, 68);
1091
+ switch (reference.point) {
1092
+ case "L1":
1093
+ return {
1094
+ x: secondary.x - ux * offset,
1095
+ y: secondary.y - uy * offset,
1096
+ anchorX: secondary.x,
1097
+ anchorY: secondary.y,
1098
+ };
1099
+ case "L2":
1100
+ return {
1101
+ x: secondary.x + ux * offset,
1102
+ y: secondary.y + uy * offset,
1103
+ anchorX: secondary.x,
1104
+ anchorY: secondary.y,
1105
+ };
1106
+ case "L3":
1107
+ return {
1108
+ x: primary.x - ux * offset,
1109
+ y: primary.y - uy * offset,
1110
+ anchorX: primary.x,
1111
+ anchorY: primary.y,
1112
+ };
1113
+ case "L4":
1114
+ return {
1115
+ x: secondary.x + (ux * 0.5 - nx * 0.8660254) * offset,
1116
+ y: secondary.y + (uy * 0.5 - ny * 0.8660254) * offset,
1117
+ anchorX: secondary.x,
1118
+ anchorY: secondary.y,
1119
+ };
1120
+ case "L5":
1121
+ return {
1122
+ x: secondary.x + (ux * 0.5 + nx * 0.8660254) * offset,
1123
+ y: secondary.y + (uy * 0.5 + ny * 0.8660254) * offset,
1124
+ anchorX: secondary.x,
1125
+ anchorY: secondary.y,
1126
+ };
1127
+ }
1128
+ }
1129
+ function buildSceneRelationships(objects, objectMap) {
1130
+ const parentIds = new Map();
1131
+ const childIds = new Map();
1132
+ for (const object of objects) {
1133
+ const parentId = resolveParentId(object, objectMap);
1134
+ parentIds.set(object.id, parentId);
1135
+ if (parentId) {
1136
+ const existing = childIds.get(parentId);
1137
+ if (existing) {
1138
+ existing.push(object.id);
1139
+ }
1140
+ else {
1141
+ childIds.set(parentId, [object.id]);
1142
+ }
1143
+ }
1144
+ if (!childIds.has(object.id)) {
1145
+ childIds.set(object.id, []);
1146
+ }
1147
+ }
1148
+ const ancestorIds = new Map();
1149
+ const groupIds = new Map();
1150
+ const groupRoots = new Map();
1151
+ const buildAncestors = (objectId) => {
1152
+ const cached = ancestorIds.get(objectId);
1153
+ if (cached) {
1154
+ return cached;
1155
+ }
1156
+ const seen = new Set();
1157
+ const results = [];
1158
+ let cursor = parentIds.get(objectId) ?? null;
1159
+ while (cursor && !seen.has(cursor)) {
1160
+ results.push(cursor);
1161
+ seen.add(cursor);
1162
+ cursor = parentIds.get(cursor) ?? null;
1163
+ }
1164
+ ancestorIds.set(objectId, results);
1165
+ return results;
1166
+ };
1167
+ const resolveGroupRootObjectId = (objectId) => {
1168
+ const cached = groupRoots.get(groupIds.get(objectId) ?? "");
1169
+ if (cached) {
1170
+ return cached;
1171
+ }
1172
+ const parentId = parentIds.get(objectId) ?? null;
1173
+ const object = objectMap.get(objectId);
1174
+ let rootObjectId = objectId;
1175
+ if (object?.placement && object.placement.mode !== "free" && parentId) {
1176
+ rootObjectId = resolveGroupRootObjectId(parentId);
1177
+ }
1178
+ return rootObjectId;
1179
+ };
1180
+ for (const object of objects) {
1181
+ buildAncestors(object.id);
1182
+ const rootObjectId = resolveGroupRootObjectId(object.id);
1183
+ const groupId = createGroupId(rootObjectId);
1184
+ groupIds.set(object.id, groupId);
1185
+ groupRoots.set(groupId, rootObjectId);
1186
+ }
1187
+ return {
1188
+ parentIds,
1189
+ childIds,
1190
+ ancestorIds,
1191
+ groupIds,
1192
+ groupRoots,
1193
+ };
1194
+ }
1195
+ function resolveParentId(object, objectMap) {
1196
+ const placement = object.placement;
1197
+ if (!placement) {
1198
+ return null;
1199
+ }
1200
+ switch (placement.mode) {
1201
+ case "orbit":
1202
+ case "surface":
1203
+ return objectMap.has(placement.target) ? placement.target : null;
1204
+ case "at":
1205
+ switch (placement.reference.kind) {
1206
+ case "anchor":
1207
+ return objectMap.has(placement.reference.objectId) ? placement.reference.objectId : null;
1208
+ case "named":
1209
+ return objectMap.has(placement.reference.name) ? placement.reference.name : null;
1210
+ case "lagrange":
1211
+ if (placement.reference.secondary && objectMap.has(placement.reference.secondary)) {
1212
+ return placement.reference.secondary;
1213
+ }
1214
+ return objectMap.has(placement.reference.primary) ? placement.reference.primary : null;
1215
+ }
1216
+ case "free":
1217
+ return null;
1218
+ }
1219
+ }
1220
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
1221
+ let minX = Number.POSITIVE_INFINITY;
1222
+ let minY = Number.POSITIVE_INFINITY;
1223
+ let maxX = Number.NEGATIVE_INFINITY;
1224
+ let maxY = Number.NEGATIVE_INFINITY;
1225
+ const include = (x, y) => {
1226
+ minX = Math.min(minX, x);
1227
+ minY = Math.min(minY, y);
1228
+ maxX = Math.max(maxX, x);
1229
+ maxY = Math.max(maxY, y);
1230
+ };
1231
+ for (const object of objects) {
1232
+ if (!object.hidden && group.objectIds.includes(object.objectId)) {
1233
+ includeObjectBounds(object, include);
1234
+ }
1235
+ }
1236
+ for (const orbit of orbitVisuals) {
1237
+ if (!orbit.hidden && group.orbitIds.includes(orbit.objectId)) {
1238
+ includeOrbitBounds(orbit, include);
1239
+ }
1240
+ }
1241
+ for (const leader of leaders) {
1242
+ if (!leader.hidden && group.leaderIds.includes(leader.objectId)) {
1243
+ include(leader.x1, leader.y1);
1244
+ include(leader.x2, leader.y2);
1245
+ }
1246
+ }
1247
+ for (const label of labels) {
1248
+ if (!label.hidden && group.labelIds.includes(label.objectId)) {
1249
+ includeLabelBounds(label, include);
1250
+ }
1251
+ }
1252
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1253
+ return createBounds(0, 0, 0, 0);
1254
+ }
1255
+ return createBounds(minX, minY, maxX, maxY);
1256
+ }
1257
+ function resolveGroupRootObjectId(object, objectMap) {
1258
+ let current = object;
1259
+ const seen = new Set();
1260
+ while (current.placement && current.placement.mode !== "free" && !seen.has(current.id)) {
1261
+ seen.add(current.id);
1262
+ const parentId = resolveParentId(current, objectMap);
1263
+ if (!parentId) {
1264
+ break;
1265
+ }
1266
+ const parent = objectMap.get(parentId);
1267
+ if (!parent) {
1268
+ break;
1269
+ }
1270
+ current = parent;
1271
+ }
1272
+ return current.id;
1273
+ }
1274
+ function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
1275
+ return {
1276
+ left: x - labelHalfWidth,
1277
+ right: x + labelHalfWidth,
1278
+ top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
1279
+ bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12),
1280
+ };
1281
+ }
1282
+ function rectsOverlap(left, right) {
1283
+ return !(left.right < right.left ||
1284
+ right.right < left.left ||
1285
+ left.bottom < right.top ||
1286
+ right.bottom < left.top);
1287
+ }
1288
+ function deriveParentAnchor(objectId, positions, objectMap) {
1289
+ const object = objectMap.get(objectId);
1290
+ if (!object?.placement || object.placement.mode !== "orbit") {
1291
+ return positions.get(objectId);
1292
+ }
1293
+ return positions.get(object.placement.target);
1294
+ }
1295
+ function visualRadiusFor(object, depth, scaleModel) {
1296
+ const explicitRadius = toVisualSizeMetric(object.properties.radius, scaleModel);
1297
+ if (explicitRadius !== null) {
1298
+ return explicitRadius;
1299
+ }
1300
+ const multiplier = scaleModel.bodyRadiusMultiplier;
1301
+ switch (object.type) {
1302
+ case "star":
1303
+ return clampNumber((depth === 0 ? 28 : 20) * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1304
+ case "planet":
1305
+ return clampNumber(12 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1306
+ case "moon":
1307
+ return clampNumber(7 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1308
+ case "belt":
1309
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1310
+ case "asteroid":
1311
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1312
+ case "comet":
1313
+ return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1314
+ case "ring":
1315
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1316
+ case "structure":
1317
+ return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1318
+ case "phenomenon":
1319
+ return clampNumber(8 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1320
+ }
1321
+ }
1322
+ function visualExtentForObject(object, radius, scaleModel) {
1323
+ const atmosphereBoost = typeof object.properties.atmosphere === "string" ? 4 : 0;
1324
+ switch (object.type) {
1325
+ case "star":
1326
+ return radius * 2.4;
1327
+ case "phenomenon":
1328
+ return radius * 1.25;
1329
+ case "structure":
1330
+ return radius + 2;
1331
+ default:
1332
+ return Math.min(radius + atmosphereBoost, scaleModel.maxBodyRadius + 10);
1333
+ }
1334
+ }
1335
+ function toDistanceMetric(value) {
1336
+ if (!value)
1337
+ return null;
1338
+ switch (value.unit) {
1339
+ case "au":
1340
+ return value.value;
1341
+ case "km":
1342
+ return value.value / AU_IN_KM;
1343
+ case "re":
1344
+ return (value.value * EARTH_RADIUS_IN_KM) / AU_IN_KM;
1345
+ case "sol":
1346
+ return (value.value * SOLAR_RADIUS_IN_KM) / AU_IN_KM;
1347
+ default:
1348
+ return value.value;
1349
+ }
1350
+ }
1351
+ function freePlacementOffsetPx(distance, scaleModel) {
1352
+ const metric = toDistanceMetric(distance ?? null);
1353
+ if (metric === null || metric <= 0) {
1354
+ return 0;
1355
+ }
1356
+ return clampNumber(metric * 96 * scaleModel.freePlacementMultiplier, 0, 420);
1357
+ }
1358
+ function toVisualSizeMetric(value, scaleModel) {
1359
+ const unitValue = toUnitValue(value);
1360
+ if (!unitValue) {
1361
+ return null;
1362
+ }
1363
+ let size;
1364
+ switch (unitValue.unit) {
1365
+ case "sol":
1366
+ size = clampNumber(unitValue.value * 22, 14, 40);
1367
+ break;
1368
+ case "re":
1369
+ size = clampNumber(unitValue.value * 10, 6, 18);
1370
+ break;
1371
+ case "km":
1372
+ size = clampNumber(Math.log10(Math.max(unitValue.value, 1)) * 2.6, 4, 16);
1373
+ break;
1374
+ default:
1375
+ size = clampNumber(unitValue.value * 4, 4, 20);
1376
+ break;
1377
+ }
1378
+ return clampNumber(size * scaleModel.bodyRadiusMultiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1379
+ }
1380
+ function toUnitValue(value) {
1381
+ if (!value || typeof value !== "object" || !("value" in value)) {
1382
+ return null;
1383
+ }
1384
+ return value;
1385
+ }
1386
+ function unitValueToDegrees(value) {
1387
+ if (!value) {
1388
+ return null;
1389
+ }
1390
+ return value.unit === "deg" || value.unit === null ? value.value : null;
1391
+ }
1392
+ function angleForIndex(index, count, startAngle) {
1393
+ if (count <= 1)
1394
+ return startAngle;
1395
+ return startAngle + (index * Math.PI * 2) / count;
1396
+ }
1397
+ function buildEllipseArcPath(cx, cy, rx, ry, rotationDeg, start, end) {
1398
+ const points = sampleEllipseArcPoints(cx, cy, rx, ry, rotationDeg, start, end, ARC_SAMPLE_COUNT);
1399
+ if (points.length === 0) {
1400
+ return "";
1401
+ }
1402
+ return points
1403
+ .map((point, index) => `${index === 0 ? "M" : "L"} ${formatNumber(point.x)} ${formatNumber(point.y)}`)
1404
+ .join(" ");
1405
+ }
1406
+ function sampleEllipseArcPoints(cx, cy, rx, ry, rotationDeg, start, end, segments) {
1407
+ const points = [];
1408
+ for (let index = 0; index <= segments; index += 1) {
1409
+ const t = start + ((end - start) * index) / segments;
1410
+ points.push(ellipsePoint(cx, cy, rx, ry, rotationDeg, t));
1411
+ }
1412
+ return points;
1413
+ }
1414
+ function ellipsePoint(cx, cy, rx, ry, rotationDeg, angle) {
1415
+ const localX = rx * Math.cos(angle);
1416
+ const localY = ry * Math.sin(angle);
1417
+ const rotated = rotateOffset(localX, localY, rotationDeg);
1418
+ return {
1419
+ x: cx + rotated.x,
1420
+ y: cy + rotated.y,
1421
+ };
1422
+ }
1423
+ function rotateOffset(x, y, rotationDeg) {
1424
+ const radians = degreesToRadians(rotationDeg);
1425
+ return {
1426
+ x: x * Math.cos(radians) - y * Math.sin(radians),
1427
+ y: x * Math.sin(radians) + y * Math.cos(radians),
1428
+ };
1429
+ }
1430
+ function projectPolarOffset(angle, distance, projection, verticalFactor) {
1431
+ const yScale = projection === "isometric" ? ISO_FLATTENING * verticalFactor : verticalFactor;
1432
+ return {
1433
+ x: Math.cos(angle) * distance,
1434
+ y: Math.sin(angle) * distance * yScale,
1435
+ };
1436
+ }
1437
+ function computeSortKey(x, y, depth) {
1438
+ return y * 1_000 + x + depth * 0.01;
1439
+ }
1440
+ function clampNumber(value, min, max) {
1441
+ return Math.min(Math.max(value, min), max);
1442
+ }
1443
+ function pushGrouped(map, key, value) {
1444
+ const existing = map.get(key);
1445
+ if (existing) {
1446
+ existing.push(value);
1447
+ }
1448
+ else {
1449
+ map.set(key, [value]);
1450
+ }
1451
+ }
1452
+ function createRenderId(objectId) {
1453
+ const normalized = objectId
1454
+ .trim()
1455
+ .toLowerCase()
1456
+ .replace(/[^a-z0-9_-]+/g, "-")
1457
+ .replace(/^-+|-+$/g, "") || "object";
1458
+ return `wo-${normalized}`;
1459
+ }
1460
+ function createGroupId(objectId) {
1461
+ return `${createRenderId(objectId)}-group`;
1462
+ }
1463
+ function customColorFor(value) {
1464
+ return typeof value === "string" && value.trim() ? value : undefined;
1465
+ }
1466
+ function estimateLabelHalfWidth(object, labelMultiplier) {
1467
+ const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
1468
+ const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
1469
+ return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
1470
+ }
1471
+ function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
1472
+ const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
1473
+ const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
1474
+ return Math.max(primaryWidth, secondaryWidth, 24);
1475
+ }
1476
+ function capitalizeLabel(value) {
1477
+ return value.length > 0 ? value[0].toUpperCase() + value.slice(1) : value;
1478
+ }
1479
+ function degreesToRadians(value) {
1480
+ return (value * Math.PI) / 180;
1481
+ }
1482
+ function formatNumber(value) {
1483
+ return Number.isInteger(value) ? String(value) : value.toFixed(2);
1484
+ }