worldorbit 2.6.0 → 3.0.1

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 (196) hide show
  1. package/README.md +20 -9
  2. package/dist/browser/core/dist/atlas-edit.d.ts +11 -0
  3. package/dist/browser/core/dist/atlas-edit.js +347 -0
  4. package/dist/browser/core/dist/atlas-utils.d.ts +22 -0
  5. package/dist/browser/core/dist/atlas-utils.js +189 -0
  6. package/dist/browser/core/dist/atlas-validate.d.ts +2 -0
  7. package/dist/browser/core/dist/atlas-validate.js +488 -0
  8. package/dist/browser/core/dist/diagnostics.d.ts +10 -0
  9. package/dist/browser/core/dist/diagnostics.js +109 -0
  10. package/dist/browser/core/dist/draft-parse.d.ts +3 -0
  11. package/dist/browser/core/dist/draft-parse.js +1654 -0
  12. package/dist/browser/core/dist/draft.d.ts +21 -0
  13. package/dist/browser/core/dist/draft.js +482 -0
  14. package/dist/browser/core/dist/errors.d.ts +7 -0
  15. package/dist/browser/core/dist/errors.js +16 -0
  16. package/dist/browser/core/dist/format.d.ts +4 -0
  17. package/dist/browser/core/dist/format.js +613 -0
  18. package/dist/browser/core/dist/index.d.ts +29 -0
  19. package/dist/browser/core/dist/index.js +35 -6542
  20. package/dist/browser/core/dist/load.d.ts +4 -0
  21. package/dist/browser/core/dist/load.js +182 -0
  22. package/dist/browser/core/dist/markdown.d.ts +2 -0
  23. package/dist/browser/core/dist/markdown.js +37 -0
  24. package/dist/browser/core/dist/normalize.d.ts +2 -0
  25. package/dist/browser/core/dist/normalize.js +312 -0
  26. package/dist/browser/core/dist/parse.d.ts +2 -0
  27. package/dist/browser/core/dist/parse.js +133 -0
  28. package/dist/browser/core/dist/scene.d.ts +3 -0
  29. package/dist/browser/core/dist/scene.js +1901 -0
  30. package/dist/browser/core/dist/schema.d.ts +8 -0
  31. package/dist/browser/core/dist/schema.js +298 -0
  32. package/dist/browser/core/dist/spatial-scene.d.ts +3 -0
  33. package/dist/browser/core/dist/spatial-scene.js +420 -0
  34. package/dist/browser/core/dist/tokenize.d.ts +4 -0
  35. package/dist/browser/core/dist/tokenize.js +68 -0
  36. package/dist/browser/core/dist/types.d.ts +637 -0
  37. package/dist/browser/core/dist/types.js +1 -0
  38. package/dist/browser/core/dist/validate.d.ts +2 -0
  39. package/dist/browser/core/dist/validate.js +56 -0
  40. package/dist/browser/editor/dist/editor.d.ts +2 -0
  41. package/dist/browser/editor/dist/editor.js +3700 -0
  42. package/dist/browser/editor/dist/index.d.ts +2 -0
  43. package/dist/browser/editor/dist/index.js +1 -12250
  44. package/dist/browser/editor/dist/types.d.ts +59 -0
  45. package/dist/browser/editor/dist/types.js +1 -0
  46. package/dist/browser/markdown/dist/html.d.ts +3 -0
  47. package/dist/browser/markdown/dist/html.js +64 -0
  48. package/dist/browser/markdown/dist/index.d.ts +4 -0
  49. package/dist/browser/markdown/dist/index.js +3 -6179
  50. package/dist/browser/markdown/dist/rehype.d.ts +10 -0
  51. package/dist/browser/markdown/dist/rehype.js +49 -0
  52. package/dist/browser/markdown/dist/remark.d.ts +9 -0
  53. package/dist/browser/markdown/dist/remark.js +28 -0
  54. package/dist/browser/markdown/dist/types.d.ts +11 -0
  55. package/dist/browser/markdown/dist/types.js +1 -0
  56. package/dist/browser/viewer/dist/atlas-state.d.ts +12 -0
  57. package/dist/browser/viewer/dist/atlas-state.js +269 -0
  58. package/dist/browser/viewer/dist/atlas-viewer.d.ts +2 -0
  59. package/dist/browser/viewer/dist/atlas-viewer.js +495 -0
  60. package/dist/browser/viewer/dist/custom-element.d.ts +1 -0
  61. package/dist/browser/viewer/dist/custom-element.js +78 -0
  62. package/dist/browser/viewer/dist/embed.d.ts +24 -0
  63. package/dist/browser/viewer/dist/embed.js +172 -0
  64. package/dist/browser/viewer/dist/errors.d.ts +6 -0
  65. package/dist/browser/viewer/dist/errors.js +12 -0
  66. package/dist/browser/viewer/dist/index.d.ts +10 -0
  67. package/dist/browser/viewer/dist/index.js +9 -8334
  68. package/dist/browser/viewer/dist/minimap.d.ts +3 -0
  69. package/dist/browser/viewer/dist/minimap.js +63 -0
  70. package/dist/browser/viewer/dist/render.d.ts +6 -0
  71. package/dist/browser/viewer/dist/render.js +670 -0
  72. package/dist/browser/viewer/dist/runtime-3d.d.ts +19 -0
  73. package/dist/browser/viewer/dist/runtime-3d.js +494 -0
  74. package/dist/browser/viewer/dist/theme.d.ts +4 -0
  75. package/dist/browser/viewer/dist/theme.js +103 -0
  76. package/dist/browser/viewer/dist/tooltip.d.ts +3 -0
  77. package/dist/browser/viewer/dist/tooltip.js +198 -0
  78. package/dist/browser/viewer/dist/types.d.ts +292 -0
  79. package/dist/browser/viewer/dist/types.js +1 -0
  80. package/dist/browser/viewer/dist/vendor/three.module.js +53032 -0
  81. package/dist/browser/viewer/dist/viewer-state.d.ts +19 -0
  82. package/dist/browser/viewer/dist/viewer-state.js +162 -0
  83. package/dist/browser/viewer/dist/viewer.d.ts +2 -0
  84. package/dist/browser/viewer/dist/viewer.js +1662 -0
  85. package/dist/unpkg/core/dist/atlas-edit.d.ts +11 -0
  86. package/dist/unpkg/core/dist/atlas-edit.js +347 -0
  87. package/dist/unpkg/core/dist/atlas-utils.d.ts +22 -0
  88. package/dist/unpkg/core/dist/atlas-utils.js +189 -0
  89. package/dist/unpkg/core/dist/atlas-validate.d.ts +2 -0
  90. package/dist/unpkg/core/dist/atlas-validate.js +488 -0
  91. package/dist/unpkg/core/dist/diagnostics.d.ts +10 -0
  92. package/dist/unpkg/core/dist/diagnostics.js +109 -0
  93. package/dist/unpkg/core/dist/draft-parse.d.ts +3 -0
  94. package/dist/unpkg/core/dist/draft-parse.js +1654 -0
  95. package/dist/unpkg/core/dist/draft.d.ts +21 -0
  96. package/dist/unpkg/core/dist/draft.js +482 -0
  97. package/dist/unpkg/core/dist/errors.d.ts +7 -0
  98. package/dist/unpkg/core/dist/errors.js +16 -0
  99. package/dist/unpkg/core/dist/format.d.ts +4 -0
  100. package/dist/unpkg/core/dist/format.js +613 -0
  101. package/dist/unpkg/core/dist/index.d.ts +29 -0
  102. package/dist/unpkg/core/dist/index.js +35 -6614
  103. package/dist/unpkg/core/dist/load.d.ts +4 -0
  104. package/dist/unpkg/core/dist/load.js +182 -0
  105. package/dist/unpkg/core/dist/markdown.d.ts +2 -0
  106. package/dist/unpkg/core/dist/markdown.js +37 -0
  107. package/dist/unpkg/core/dist/normalize.d.ts +2 -0
  108. package/dist/unpkg/core/dist/normalize.js +312 -0
  109. package/dist/unpkg/core/dist/parse.d.ts +2 -0
  110. package/dist/unpkg/core/dist/parse.js +133 -0
  111. package/dist/unpkg/core/dist/scene.d.ts +3 -0
  112. package/dist/unpkg/core/dist/scene.js +1901 -0
  113. package/dist/unpkg/core/dist/schema.d.ts +8 -0
  114. package/dist/unpkg/core/dist/schema.js +298 -0
  115. package/dist/unpkg/core/dist/spatial-scene.d.ts +3 -0
  116. package/dist/unpkg/core/dist/spatial-scene.js +420 -0
  117. package/dist/unpkg/core/dist/tokenize.d.ts +4 -0
  118. package/dist/unpkg/core/dist/tokenize.js +68 -0
  119. package/dist/unpkg/core/dist/types.d.ts +637 -0
  120. package/dist/unpkg/core/dist/types.js +1 -0
  121. package/dist/unpkg/core/dist/validate.d.ts +2 -0
  122. package/dist/unpkg/core/dist/validate.js +56 -0
  123. package/dist/unpkg/editor/dist/editor.d.ts +2 -0
  124. package/dist/unpkg/editor/dist/editor.js +3700 -0
  125. package/dist/unpkg/editor/dist/index.d.ts +2 -0
  126. package/dist/unpkg/editor/dist/index.js +1 -12275
  127. package/dist/unpkg/editor/dist/types.d.ts +59 -0
  128. package/dist/unpkg/editor/dist/types.js +1 -0
  129. package/dist/unpkg/markdown/dist/html.d.ts +3 -0
  130. package/dist/unpkg/markdown/dist/html.js +64 -0
  131. package/dist/unpkg/markdown/dist/index.d.ts +4 -0
  132. package/dist/unpkg/markdown/dist/index.js +3 -6207
  133. package/dist/unpkg/markdown/dist/rehype.d.ts +10 -0
  134. package/dist/unpkg/markdown/dist/rehype.js +49 -0
  135. package/dist/unpkg/markdown/dist/remark.d.ts +9 -0
  136. package/dist/unpkg/markdown/dist/remark.js +28 -0
  137. package/dist/unpkg/markdown/dist/types.d.ts +11 -0
  138. package/dist/unpkg/markdown/dist/types.js +1 -0
  139. package/dist/unpkg/viewer/dist/atlas-state.d.ts +12 -0
  140. package/dist/unpkg/viewer/dist/atlas-state.js +269 -0
  141. package/dist/unpkg/viewer/dist/atlas-viewer.d.ts +2 -0
  142. package/dist/unpkg/viewer/dist/atlas-viewer.js +495 -0
  143. package/dist/unpkg/viewer/dist/custom-element.d.ts +1 -0
  144. package/dist/unpkg/viewer/dist/custom-element.js +78 -0
  145. package/dist/unpkg/viewer/dist/embed.d.ts +24 -0
  146. package/dist/unpkg/viewer/dist/embed.js +172 -0
  147. package/dist/unpkg/viewer/dist/errors.d.ts +6 -0
  148. package/dist/unpkg/viewer/dist/errors.js +12 -0
  149. package/dist/unpkg/viewer/dist/index.d.ts +10 -0
  150. package/dist/unpkg/viewer/dist/index.js +9 -8391
  151. package/dist/unpkg/viewer/dist/minimap.d.ts +3 -0
  152. package/dist/unpkg/viewer/dist/minimap.js +63 -0
  153. package/dist/unpkg/viewer/dist/render.d.ts +6 -0
  154. package/dist/unpkg/viewer/dist/render.js +670 -0
  155. package/dist/unpkg/viewer/dist/runtime-3d.d.ts +19 -0
  156. package/dist/unpkg/viewer/dist/runtime-3d.js +494 -0
  157. package/dist/unpkg/viewer/dist/theme.d.ts +4 -0
  158. package/dist/unpkg/viewer/dist/theme.js +103 -0
  159. package/dist/unpkg/viewer/dist/tooltip.d.ts +3 -0
  160. package/dist/unpkg/viewer/dist/tooltip.js +198 -0
  161. package/dist/unpkg/viewer/dist/types.d.ts +292 -0
  162. package/dist/unpkg/viewer/dist/types.js +1 -0
  163. package/dist/unpkg/viewer/dist/vendor/three.module.js +53032 -0
  164. package/dist/unpkg/viewer/dist/viewer-state.d.ts +19 -0
  165. package/dist/unpkg/viewer/dist/viewer-state.js +162 -0
  166. package/dist/unpkg/viewer/dist/viewer.d.ts +2 -0
  167. package/dist/unpkg/viewer/dist/viewer.js +1662 -0
  168. package/dist/unpkg/worldorbit-core.min.js +10 -10
  169. package/dist/unpkg/worldorbit-editor.min.js +4109 -256
  170. package/dist/unpkg/worldorbit-markdown.min.js +26 -26
  171. package/dist/unpkg/worldorbit-viewer.min.js +3945 -92
  172. package/dist/unpkg/worldorbit.js +32219 -199
  173. package/dist/unpkg/worldorbit.min.js +3949 -96
  174. package/package.json +1 -1
  175. package/packages/core/dist/index.d.ts +1 -0
  176. package/packages/core/dist/index.js +1 -0
  177. package/packages/core/dist/spatial-scene.d.ts +3 -0
  178. package/packages/core/dist/spatial-scene.js +420 -0
  179. package/packages/core/dist/types.d.ts +105 -0
  180. package/packages/editor/dist/editor.js +25 -4
  181. package/packages/editor/dist/types.d.ts +4 -0
  182. package/packages/markdown/dist/html.js +10 -3
  183. package/packages/viewer/dist/atlas-state.js +3 -0
  184. package/packages/viewer/dist/atlas-viewer.js +1 -0
  185. package/packages/viewer/dist/custom-element.js +18 -4
  186. package/packages/viewer/dist/embed.d.ts +5 -1
  187. package/packages/viewer/dist/embed.js +58 -24
  188. package/packages/viewer/dist/errors.d.ts +6 -0
  189. package/packages/viewer/dist/errors.js +12 -0
  190. package/packages/viewer/dist/index.d.ts +1 -0
  191. package/packages/viewer/dist/index.js +1 -0
  192. package/packages/viewer/dist/runtime-3d.d.ts +19 -0
  193. package/packages/viewer/dist/runtime-3d.js +494 -0
  194. package/packages/viewer/dist/types.d.ts +21 -2
  195. package/packages/viewer/dist/vendor/three.module.js +53032 -0
  196. package/packages/viewer/dist/viewer.js +501 -41
@@ -0,0 +1,1901 @@
1
+ const AU_IN_KM = 149_597_870.7;
2
+ const EARTH_RADIUS_IN_KM = 6_371;
3
+ const JUPITER_RADIUS_IN_KM = 71_492;
4
+ const SOLAR_RADIUS_IN_KM = 695_700;
5
+ const LY_IN_AU = 63_241.077;
6
+ const PC_IN_AU = 206_264.806;
7
+ const KPC_IN_AU = 206_264_806;
8
+ const ISO_FLATTENING = 0.68;
9
+ const MIN_ISO_MINOR_SCALE = 0.2;
10
+ const ARC_SAMPLE_COUNT = 28;
11
+ export function renderDocumentToScene(document, options = {}) {
12
+ const frame = resolveSceneFrame(options);
13
+ const width = frame.width;
14
+ const height = frame.height;
15
+ const padding = frame.padding;
16
+ const layoutPreset = resolveLayoutPreset(document);
17
+ const schemaProjection = resolveProjection(document, options.projection);
18
+ const camera = normalizeViewCamera(options.camera ?? null);
19
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
20
+ const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
21
+ const spacingFactor = layoutPresetSpacing(layoutPreset);
22
+ const systemId = document.system?.id ?? null;
23
+ const activeEventId = options.activeEventId ?? null;
24
+ const effectiveObjects = createEffectiveObjects(document.objects, document.events ?? [], activeEventId);
25
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
26
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
27
+ const positions = new Map();
28
+ const orbitDrafts = [];
29
+ const leaderDrafts = [];
30
+ const rootObjects = [];
31
+ const freeObjects = [];
32
+ const atObjects = [];
33
+ const surfaceChildren = new Map();
34
+ const orbitChildren = new Map();
35
+ for (const object of effectiveObjects) {
36
+ const placement = object.placement;
37
+ if (!placement) {
38
+ rootObjects.push(object);
39
+ continue;
40
+ }
41
+ if (placement.mode === "orbit") {
42
+ pushGrouped(orbitChildren, placement.target, object);
43
+ continue;
44
+ }
45
+ if (placement.mode === "surface") {
46
+ pushGrouped(surfaceChildren, placement.target, object);
47
+ continue;
48
+ }
49
+ if (placement.mode === "at") {
50
+ atObjects.push(object);
51
+ continue;
52
+ }
53
+ freeObjects.push(object);
54
+ }
55
+ const centerX = freeObjects.length > 0 ? width * 0.42 : width / 2;
56
+ const centerY = height / 2;
57
+ const context = {
58
+ orbitChildren,
59
+ surfaceChildren,
60
+ objectMap,
61
+ spacingFactor,
62
+ projection: renderProjection,
63
+ scaleModel,
64
+ };
65
+ const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
66
+ if (primaryRoot) {
67
+ placeObject(primaryRoot, centerX, centerY, 0, positions, orbitDrafts, leaderDrafts, context);
68
+ }
69
+ const secondaryRoots = rootObjects.filter((object) => object.id !== primaryRoot?.id);
70
+ if (secondaryRoots.length > 0) {
71
+ const rootRingRadius = Math.min(width, height) *
72
+ 0.28 *
73
+ spacingFactor *
74
+ scaleModel.orbitDistanceMultiplier;
75
+ secondaryRoots.forEach((object, index) => {
76
+ const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
77
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
78
+ placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
79
+ });
80
+ }
81
+ freeObjects.forEach((object, index) => {
82
+ const x = width -
83
+ padding -
84
+ 140 -
85
+ freePlacementOffsetPx(object.placement?.mode === "free" ? object.placement.distance : undefined, scaleModel);
86
+ const rowStep = Math.max(76, ((height - padding * 2 - 180) / Math.max(1, freeObjects.length)) * spacingFactor) * scaleModel.freePlacementMultiplier;
87
+ const y = padding + 92 + index * rowStep;
88
+ positions.set(object.id, {
89
+ object,
90
+ x,
91
+ y,
92
+ radius: visualRadiusFor(object, 0, scaleModel),
93
+ sortKey: computeSortKey(x, y, 0),
94
+ });
95
+ leaderDrafts.push({
96
+ object,
97
+ groupId: relationships.groupIds.get(object.id) ?? null,
98
+ x1: x - 60,
99
+ y1: y,
100
+ x2: x - 18,
101
+ y2: y,
102
+ mode: "free",
103
+ });
104
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, 1);
105
+ });
106
+ atObjects.forEach((object, index) => {
107
+ if (positions.has(object.id) || !object.placement || object.placement.mode !== "at") {
108
+ return;
109
+ }
110
+ const resolved = resolveAtPosition(object.placement.reference, positions, objectMap, index, atObjects.length, width, height, padding, context);
111
+ positions.set(object.id, {
112
+ object,
113
+ x: resolved.x,
114
+ y: resolved.y,
115
+ radius: visualRadiusFor(object, 2, scaleModel),
116
+ sortKey: computeSortKey(resolved.x, resolved.y, 2),
117
+ anchorX: resolved.anchorX,
118
+ anchorY: resolved.anchorY,
119
+ });
120
+ if (resolved.anchorX !== undefined && resolved.anchorY !== undefined) {
121
+ leaderDrafts.push({
122
+ object,
123
+ groupId: relationships.groupIds.get(object.id) ?? null,
124
+ x1: resolved.anchorX,
125
+ y1: resolved.anchorY,
126
+ x2: resolved.x,
127
+ y2: resolved.y,
128
+ mode: "at",
129
+ });
130
+ }
131
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, 2);
132
+ });
133
+ const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
134
+ const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
135
+ const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
136
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
137
+ const relations = createSceneRelations(document, objects);
138
+ const events = createSceneEvents(document.events ?? [], objects, activeEventId);
139
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
140
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
141
+ const semanticGroups = createSceneSemanticGroups(document, objects);
142
+ const viewpoints = createSceneViewpoints(document, schemaProjection, frame.preset, relationships, objectMap);
143
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
144
+ return {
145
+ width,
146
+ height,
147
+ padding,
148
+ renderPreset: frame.preset,
149
+ projection: schemaProjection,
150
+ renderProjection,
151
+ camera,
152
+ scaleModel,
153
+ title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") ||
154
+ "WorldOrbit",
155
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
156
+ systemId,
157
+ viewMode: schemaProjection,
158
+ layoutPreset,
159
+ metadata: {
160
+ format: document.format,
161
+ version: document.version,
162
+ view: schemaProjection,
163
+ renderProjection,
164
+ scale: String(document.system?.properties.scale ?? layoutPreset),
165
+ units: String(document.system?.properties.units ?? "mixed"),
166
+ preset: frame.preset ?? "custom",
167
+ ...(camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {}),
168
+ ...(camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {}),
169
+ ...(camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {}),
170
+ ...(camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}),
171
+ },
172
+ contentBounds,
173
+ layers,
174
+ groups,
175
+ semanticGroups,
176
+ viewpoints,
177
+ events,
178
+ activeEventId,
179
+ objects,
180
+ orbitVisuals,
181
+ relations,
182
+ leaders,
183
+ labels,
184
+ };
185
+ }
186
+ export function rotatePoint(point, center, rotationDeg) {
187
+ const radians = degreesToRadians(rotationDeg);
188
+ const cos = Math.cos(radians);
189
+ const sin = Math.sin(radians);
190
+ const dx = point.x - center.x;
191
+ const dy = point.y - center.y;
192
+ return {
193
+ x: center.x + dx * cos - dy * sin,
194
+ y: center.y + dx * sin + dy * cos,
195
+ };
196
+ }
197
+ function createEffectiveObjects(objects, events, activeEventId) {
198
+ const cloned = objects.map((object) => structuredClone(object));
199
+ if (!activeEventId) {
200
+ return cloned;
201
+ }
202
+ const activeEvent = events.find((event) => event.id === activeEventId);
203
+ if (!activeEvent) {
204
+ return cloned;
205
+ }
206
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
207
+ const referencedIds = new Set([
208
+ ...(activeEvent.targetObjectId ? [activeEvent.targetObjectId] : []),
209
+ ...activeEvent.participantObjectIds,
210
+ ...activeEvent.positions.map((pose) => pose.objectId),
211
+ ]);
212
+ for (const objectId of referencedIds) {
213
+ const object = objectMap.get(objectId);
214
+ if (!object) {
215
+ continue;
216
+ }
217
+ if (activeEvent.epoch) {
218
+ object.epoch = activeEvent.epoch;
219
+ }
220
+ if (activeEvent.referencePlane) {
221
+ object.referencePlane = activeEvent.referencePlane;
222
+ }
223
+ }
224
+ for (const pose of activeEvent.positions) {
225
+ const object = objectMap.get(pose.objectId);
226
+ if (!object) {
227
+ continue;
228
+ }
229
+ if (pose.placement) {
230
+ object.placement = structuredClone(pose.placement);
231
+ }
232
+ if (pose.inner) {
233
+ object.properties.inner = { ...pose.inner };
234
+ }
235
+ if (pose.outer) {
236
+ object.properties.outer = { ...pose.outer };
237
+ }
238
+ if (pose.epoch) {
239
+ object.epoch = pose.epoch;
240
+ }
241
+ if (pose.referencePlane) {
242
+ object.referencePlane = pose.referencePlane;
243
+ }
244
+ }
245
+ return cloned;
246
+ }
247
+ function resolveLayoutPreset(document) {
248
+ const rawScale = String(document.system?.properties.scale ?? "balanced").toLowerCase();
249
+ switch (rawScale) {
250
+ case "compressed":
251
+ case "compact":
252
+ return "compact";
253
+ case "expanded":
254
+ case "presentation":
255
+ return "presentation";
256
+ default:
257
+ return "balanced";
258
+ }
259
+ }
260
+ function resolveSceneFrame(options) {
261
+ const defaults = scenePresetDefaults(options.preset);
262
+ return {
263
+ width: options.width ?? defaults.width,
264
+ height: options.height ?? defaults.height,
265
+ padding: options.padding ?? defaults.padding,
266
+ preset: options.preset ?? null,
267
+ };
268
+ }
269
+ function scenePresetDefaults(preset) {
270
+ switch (preset) {
271
+ case "presentation":
272
+ return { width: 1440, height: 900, padding: 88 };
273
+ case "atlas-card":
274
+ return { width: 960, height: 560, padding: 56 };
275
+ case "markdown":
276
+ return { width: 920, height: 540, padding: 48 };
277
+ case "diagram":
278
+ default:
279
+ return { width: 1200, height: 780, padding: 72 };
280
+ }
281
+ }
282
+ function resolveProjection(document, projection) {
283
+ if (projection === "topdown" ||
284
+ projection === "isometric" ||
285
+ projection === "orthographic" ||
286
+ projection === "perspective") {
287
+ return projection;
288
+ }
289
+ const documentView = String(document.system?.properties.view ?? "topdown").toLowerCase();
290
+ return parseViewProjection(documentView) ?? "topdown";
291
+ }
292
+ function resolveRenderProjection(projection, camera) {
293
+ switch (projection) {
294
+ case "topdown":
295
+ return "topdown";
296
+ case "isometric":
297
+ return "isometric";
298
+ case "orthographic":
299
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null)
300
+ ? "isometric"
301
+ : "topdown";
302
+ case "perspective":
303
+ return "isometric";
304
+ }
305
+ }
306
+ function normalizeViewCamera(camera) {
307
+ if (!camera) {
308
+ return null;
309
+ }
310
+ const normalized = {
311
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
312
+ elevation: normalizeFiniteCameraValue(camera.elevation),
313
+ roll: normalizeFiniteCameraValue(camera.roll),
314
+ distance: normalizePositiveCameraDistance(camera.distance),
315
+ };
316
+ return normalized.azimuth !== null ||
317
+ normalized.elevation !== null ||
318
+ normalized.roll !== null ||
319
+ normalized.distance !== null
320
+ ? normalized
321
+ : null;
322
+ }
323
+ function normalizeFiniteCameraValue(value) {
324
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
325
+ }
326
+ function normalizePositiveCameraDistance(value) {
327
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
328
+ }
329
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
330
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
331
+ if (projection !== renderProjection) {
332
+ parts.push(`2D ${renderProjection} fallback`);
333
+ }
334
+ if (camera) {
335
+ const cameraParts = [
336
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
337
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
338
+ camera.roll !== null ? `roll ${camera.roll}` : null,
339
+ camera.distance !== null ? `dist ${camera.distance}` : null,
340
+ ].filter(Boolean);
341
+ if (cameraParts.length > 0) {
342
+ parts.push(`camera ${cameraParts.join(" / ")}`);
343
+ }
344
+ }
345
+ return parts.join(" - ");
346
+ }
347
+ function resolveScaleModel(layoutPreset, overrides) {
348
+ const defaults = defaultScaleModel(layoutPreset);
349
+ return {
350
+ ...defaults,
351
+ ...overrides,
352
+ };
353
+ }
354
+ function defaultScaleModel(layoutPreset) {
355
+ switch (layoutPreset) {
356
+ case "compact":
357
+ return {
358
+ orbitDistanceMultiplier: 0.84,
359
+ bodyRadiusMultiplier: 0.92,
360
+ labelMultiplier: 0.9,
361
+ freePlacementMultiplier: 0.9,
362
+ ringThicknessMultiplier: 0.92,
363
+ minBodyRadius: 4,
364
+ maxBodyRadius: 36,
365
+ };
366
+ case "presentation":
367
+ return {
368
+ orbitDistanceMultiplier: 1.2,
369
+ bodyRadiusMultiplier: 1.18,
370
+ labelMultiplier: 1.08,
371
+ freePlacementMultiplier: 1.05,
372
+ ringThicknessMultiplier: 1.16,
373
+ minBodyRadius: 5,
374
+ maxBodyRadius: 48,
375
+ };
376
+ default:
377
+ return {
378
+ orbitDistanceMultiplier: 1,
379
+ bodyRadiusMultiplier: 1,
380
+ labelMultiplier: 1,
381
+ freePlacementMultiplier: 1,
382
+ ringThicknessMultiplier: 1,
383
+ minBodyRadius: 4,
384
+ maxBodyRadius: 40,
385
+ };
386
+ }
387
+ }
388
+ function layoutPresetSpacing(layoutPreset) {
389
+ switch (layoutPreset) {
390
+ case "compact":
391
+ return 0.84;
392
+ case "presentation":
393
+ return 1.2;
394
+ default:
395
+ return 1;
396
+ }
397
+ }
398
+ function createSceneObject(position, scaleModel, relationships) {
399
+ const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
400
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
401
+ return {
402
+ renderId: createRenderId(object.id),
403
+ objectId: object.id,
404
+ object,
405
+ parentId: relationships.parentIds.get(object.id) ?? null,
406
+ ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
407
+ childIds: relationships.childIds.get(object.id) ?? [],
408
+ groupId: relationships.groupIds.get(object.id) ?? null,
409
+ semanticGroupIds: [...(object.groups ?? [])],
410
+ x,
411
+ y,
412
+ radius,
413
+ visualRadius: visualExtentForObject(object, radius, scaleModel),
414
+ sortKey: sortKey + renderPriority * 0.001,
415
+ anchorX,
416
+ anchorY,
417
+ label: object.id,
418
+ secondaryLabel: object.type === "structure" ? String(object.properties.kind ?? object.type) : object.type,
419
+ fillColor: customColorFor(object.properties.color),
420
+ imageHref: typeof object.properties.image === "string" && object.properties.image.trim()
421
+ ? object.properties.image
422
+ : undefined,
423
+ hidden: object.properties.hidden === true,
424
+ };
425
+ }
426
+ function createOrbitVisual(draft, groupId) {
427
+ return {
428
+ renderId: `${createRenderId(draft.object.id)}-orbit`,
429
+ objectId: draft.object.id,
430
+ object: draft.object,
431
+ parentId: draft.parentId,
432
+ groupId,
433
+ semanticGroupIds: [...(draft.object.groups ?? [])],
434
+ kind: draft.kind,
435
+ cx: draft.cx,
436
+ cy: draft.cy,
437
+ radius: draft.radius,
438
+ rx: draft.rx,
439
+ ry: draft.ry,
440
+ rotationDeg: draft.rotationDeg,
441
+ band: draft.band,
442
+ bandThickness: draft.bandThickness,
443
+ frontArcPath: draft.frontArcPath,
444
+ backArcPath: draft.backArcPath,
445
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false,
446
+ };
447
+ }
448
+ function createLeaderLine(draft) {
449
+ return {
450
+ renderId: `${createRenderId(draft.object.id)}-leader-${draft.mode}`,
451
+ objectId: draft.object.id,
452
+ object: draft.object,
453
+ groupId: draft.groupId,
454
+ semanticGroupIds: [...(draft.object.groups ?? [])],
455
+ x1: draft.x1,
456
+ y1: draft.y1,
457
+ x2: draft.x2,
458
+ y2: draft.y2,
459
+ mode: draft.mode,
460
+ hidden: draft.object.properties.hidden === true,
461
+ };
462
+ }
463
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
464
+ const labels = [];
465
+ const occupied = [];
466
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
467
+ const visibleObjects = [...objects]
468
+ .filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false)
469
+ .sort(compareLabelPlacementOrder);
470
+ for (const object of visibleObjects) {
471
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ??
472
+ createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
473
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
474
+ labels.push({
475
+ renderId: `${object.renderId}-label`,
476
+ objectId: object.objectId,
477
+ object: object.object,
478
+ groupId: object.groupId,
479
+ semanticGroupIds: [...object.semanticGroupIds],
480
+ label: object.label,
481
+ secondaryLabel: object.secondaryLabel,
482
+ x: placement.x,
483
+ y: placement.labelY,
484
+ secondaryY: placement.secondaryY,
485
+ textAnchor: placement.textAnchor,
486
+ direction: placement.direction,
487
+ hidden: object.hidden,
488
+ });
489
+ }
490
+ return labels;
491
+ }
492
+ function compareLabelPlacementOrder(left, right) {
493
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
494
+ if (priorityDiff !== 0) {
495
+ return priorityDiff;
496
+ }
497
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
498
+ if (renderPriorityDiff !== 0) {
499
+ return renderPriorityDiff;
500
+ }
501
+ return left.sortKey - right.sortKey;
502
+ }
503
+ function labelPlacementPriority(object) {
504
+ switch (object.object.type) {
505
+ case "star":
506
+ return 0;
507
+ case "planet":
508
+ return 1;
509
+ case "moon":
510
+ return 2;
511
+ case "belt":
512
+ case "ring":
513
+ return 3;
514
+ case "asteroid":
515
+ case "comet":
516
+ return 4;
517
+ case "structure":
518
+ case "phenomenon":
519
+ return 5;
520
+ }
521
+ }
522
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
523
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
524
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
525
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
526
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
527
+ const rect = createLabelRect(object, placement, labelMultiplier);
528
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
529
+ return placement;
530
+ }
531
+ }
532
+ }
533
+ return null;
534
+ }
535
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
536
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
537
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
538
+ const oppositeVertical = vertical === "below" ? "above" : "below";
539
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
540
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
541
+ const preferHorizontal = object.object.type === "structure" ||
542
+ object.object.type === "phenomenon" ||
543
+ object.object.placement?.mode === "at" ||
544
+ object.object.placement?.mode === "surface" ||
545
+ object.object.placement?.mode === "free";
546
+ return preferHorizontal
547
+ ? [horizontal, vertical, oppositeHorizontal, oppositeVertical]
548
+ : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
549
+ }
550
+ function defaultVerticalDirection(object, parent, sceneHeight) {
551
+ if (parent && Math.abs(object.y - parent.y) > 6) {
552
+ return object.y >= parent.y ? "below" : "above";
553
+ }
554
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
555
+ }
556
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
557
+ if (parent && Math.abs(object.x - parent.x) > 6) {
558
+ return object.x >= parent.x ? "right" : "left";
559
+ }
560
+ return object.x >= sceneWidth / 2 ? "right" : "left";
561
+ }
562
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
563
+ const step = 14 * labelMultiplier;
564
+ switch (direction) {
565
+ case "above": {
566
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
567
+ return {
568
+ x: object.x,
569
+ labelY,
570
+ secondaryY: labelY - 16 * labelMultiplier,
571
+ textAnchor: "middle",
572
+ direction,
573
+ };
574
+ }
575
+ case "below": {
576
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
577
+ return {
578
+ x: object.x,
579
+ labelY,
580
+ secondaryY: labelY + 16 * labelMultiplier,
581
+ textAnchor: "middle",
582
+ direction,
583
+ };
584
+ }
585
+ case "left": {
586
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
587
+ const labelY = object.y - 4 * labelMultiplier;
588
+ return {
589
+ x,
590
+ labelY,
591
+ secondaryY: labelY + 16 * labelMultiplier,
592
+ textAnchor: "end",
593
+ direction,
594
+ };
595
+ }
596
+ case "right": {
597
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
598
+ const labelY = object.y - 4 * labelMultiplier;
599
+ return {
600
+ x,
601
+ labelY,
602
+ secondaryY: labelY + 16 * labelMultiplier,
603
+ textAnchor: "start",
604
+ direction,
605
+ };
606
+ }
607
+ }
608
+ }
609
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
610
+ const backOrbitIds = orbitVisuals
611
+ .filter((visual) => !visual.hidden && Boolean(visual.backArcPath))
612
+ .map((visual) => visual.renderId);
613
+ const frontOrbitIds = orbitVisuals
614
+ .filter((visual) => !visual.hidden)
615
+ .map((visual) => visual.renderId);
616
+ return [
617
+ { id: "background", renderIds: ["wo-bg", "wo-bg-glow", "wo-grid"] },
618
+ {
619
+ id: "guides",
620
+ renderIds: leaders.filter((leader) => !leader.hidden).map((leader) => leader.renderId),
621
+ },
622
+ { id: "orbits-back", renderIds: backOrbitIds },
623
+ { id: "orbits-front", renderIds: frontOrbitIds },
624
+ {
625
+ id: "relations",
626
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId),
627
+ },
628
+ {
629
+ id: "events",
630
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId),
631
+ },
632
+ {
633
+ id: "objects",
634
+ renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId),
635
+ },
636
+ {
637
+ id: "labels",
638
+ renderIds: labels.filter((label) => !label.hidden).map((label) => label.renderId),
639
+ },
640
+ { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] },
641
+ ];
642
+ }
643
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
644
+ const groups = new Map();
645
+ const ensureGroup = (groupId) => {
646
+ if (!groupId) {
647
+ return null;
648
+ }
649
+ const existing = groups.get(groupId);
650
+ if (existing) {
651
+ return existing;
652
+ }
653
+ const rootObjectId = relationships.groupRoots.get(groupId) ?? null;
654
+ const created = {
655
+ renderId: groupId,
656
+ rootObjectId,
657
+ label: rootObjectId ?? groupId,
658
+ objectIds: [],
659
+ orbitIds: [],
660
+ labelIds: [],
661
+ leaderIds: [],
662
+ contentBounds: createBounds(0, 0, 0, 0),
663
+ };
664
+ groups.set(groupId, created);
665
+ return created;
666
+ };
667
+ for (const object of objects) {
668
+ const group = ensureGroup(object.groupId);
669
+ if (group && !object.hidden) {
670
+ group.objectIds.push(object.objectId);
671
+ }
672
+ }
673
+ for (const orbit of orbitVisuals) {
674
+ const group = ensureGroup(orbit.groupId);
675
+ if (group && !orbit.hidden) {
676
+ group.orbitIds.push(orbit.objectId);
677
+ }
678
+ }
679
+ for (const leader of leaders) {
680
+ const group = ensureGroup(leader.groupId);
681
+ if (group && !leader.hidden) {
682
+ group.leaderIds.push(leader.objectId);
683
+ }
684
+ }
685
+ for (const label of labels) {
686
+ const group = ensureGroup(label.groupId);
687
+ if (group && !label.hidden) {
688
+ group.labelIds.push(label.objectId);
689
+ }
690
+ }
691
+ for (const group of groups.values()) {
692
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
693
+ }
694
+ return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
695
+ }
696
+ function createSceneSemanticGroups(document, objects) {
697
+ return [...document.groups]
698
+ .map((group) => ({
699
+ id: group.id,
700
+ label: group.label,
701
+ summary: group.summary,
702
+ color: group.color,
703
+ tags: [...group.tags],
704
+ hidden: group.hidden,
705
+ objectIds: objects
706
+ .filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id))
707
+ .map((object) => object.objectId),
708
+ }))
709
+ .sort((left, right) => left.label.localeCompare(right.label));
710
+ }
711
+ function createSceneRelations(document, objects) {
712
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
713
+ return document.relations
714
+ .map((relation) => {
715
+ const from = objectMap.get(relation.from);
716
+ const to = objectMap.get(relation.to);
717
+ return {
718
+ renderId: `${createRenderId(relation.id)}-relation`,
719
+ relationId: relation.id,
720
+ relation,
721
+ fromObjectId: relation.from,
722
+ toObjectId: relation.to,
723
+ x1: from?.x ?? 0,
724
+ y1: from?.y ?? 0,
725
+ x2: to?.x ?? 0,
726
+ y2: to?.y ?? 0,
727
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden,
728
+ };
729
+ })
730
+ .sort((left, right) => left.relation.id.localeCompare(right.relation.id));
731
+ }
732
+ function createSceneEvents(events, objects, activeEventId) {
733
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
734
+ return events
735
+ .map((event) => {
736
+ const objectIds = [...new Set([
737
+ ...(event.targetObjectId ? [event.targetObjectId] : []),
738
+ ...event.participantObjectIds,
739
+ ])];
740
+ const positions = objectIds
741
+ .map((objectId) => objectMap.get(objectId))
742
+ .filter(Boolean);
743
+ const centroidX = positions.length > 0
744
+ ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length
745
+ : 0;
746
+ const centroidY = positions.length > 0
747
+ ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length
748
+ : 0;
749
+ return {
750
+ renderId: `${createRenderId(event.id)}-event`,
751
+ eventId: event.id,
752
+ event,
753
+ objectIds,
754
+ participantIds: [...event.participantObjectIds],
755
+ targetObjectId: event.targetObjectId,
756
+ x: centroidX,
757
+ y: centroidY,
758
+ hidden: event.hidden ||
759
+ positions.length === 0 ||
760
+ positions.every((object) => object.hidden) ||
761
+ (activeEventId !== null && event.id !== activeEventId),
762
+ };
763
+ })
764
+ .sort((left, right) => left.event.id.localeCompare(right.event.id));
765
+ }
766
+ function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
767
+ const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
768
+ const drafts = new Map();
769
+ for (const [key, value] of Object.entries(document.system?.info ?? {})) {
770
+ if (!key.startsWith("viewpoint.")) {
771
+ continue;
772
+ }
773
+ const [prefix, rawId, ...fieldParts] = key.split(".");
774
+ if (prefix !== "viewpoint" || !rawId || fieldParts.length === 0) {
775
+ continue;
776
+ }
777
+ const id = normalizeViewpointId(rawId);
778
+ if (!id) {
779
+ continue;
780
+ }
781
+ const field = fieldParts.join(".").toLowerCase();
782
+ const draft = drafts.get(id) ?? { id };
783
+ applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap);
784
+ drafts.set(id, draft);
785
+ }
786
+ const viewpoints = [...drafts.values()]
787
+ .map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap))
788
+ .filter(Boolean);
789
+ const overviewIndex = viewpoints.findIndex((viewpoint) => viewpoint.id === generatedOverview.id);
790
+ if (overviewIndex >= 0) {
791
+ viewpoints.splice(overviewIndex, 1, {
792
+ ...generatedOverview,
793
+ ...viewpoints[overviewIndex],
794
+ layers: {
795
+ ...generatedOverview.layers,
796
+ ...viewpoints[overviewIndex].layers,
797
+ },
798
+ filter: viewpoints[overviewIndex].filter ?? generatedOverview.filter,
799
+ generated: false,
800
+ });
801
+ }
802
+ else {
803
+ viewpoints.unshift(generatedOverview);
804
+ }
805
+ return viewpoints.sort((left, right) => {
806
+ if (left.id === "overview")
807
+ return -1;
808
+ if (right.id === "overview")
809
+ return 1;
810
+ return left.label.localeCompare(right.label);
811
+ });
812
+ }
813
+ function createGeneratedOverviewViewpoint(document, projection, preset) {
814
+ const title = document.system?.title ?? document.system?.properties.title;
815
+ const label = title ? `${String(title)} Overview` : "Overview";
816
+ const camera = normalizeViewCamera(null);
817
+ const renderProjection = resolveRenderProjection(projection, camera);
818
+ return {
819
+ id: "overview",
820
+ label,
821
+ summary: "Fit the whole system with the current atlas defaults.",
822
+ objectId: null,
823
+ selectedObjectId: null,
824
+ eventIds: [],
825
+ projection,
826
+ renderProjection,
827
+ camera,
828
+ preset,
829
+ rotationDeg: 0,
830
+ scale: null,
831
+ layers: {},
832
+ filter: null,
833
+ generated: true,
834
+ };
835
+ }
836
+ function applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap) {
837
+ const normalizedValue = value.trim();
838
+ switch (field) {
839
+ case "label":
840
+ case "title":
841
+ if (normalizedValue) {
842
+ draft.label = normalizedValue;
843
+ }
844
+ return;
845
+ case "summary":
846
+ case "description":
847
+ if (normalizedValue) {
848
+ draft.summary = normalizedValue;
849
+ }
850
+ return;
851
+ case "focus":
852
+ case "object":
853
+ if (normalizedValue) {
854
+ draft.focus = normalizedValue;
855
+ }
856
+ return;
857
+ case "select":
858
+ case "selection":
859
+ if (normalizedValue) {
860
+ draft.select = normalizedValue;
861
+ }
862
+ return;
863
+ case "events":
864
+ draft.eventIds = splitListValue(normalizedValue);
865
+ return;
866
+ case "projection":
867
+ case "view":
868
+ draft.projection = parseViewProjection(normalizedValue) ?? projection;
869
+ return;
870
+ case "preset":
871
+ draft.preset = parseRenderPreset(normalizedValue) ?? preset;
872
+ return;
873
+ case "rotation":
874
+ case "angle":
875
+ draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
876
+ return;
877
+ case "camera.azimuth":
878
+ draft.camera = {
879
+ ...(draft.camera ?? createEmptyViewCamera()),
880
+ azimuth: parseFiniteNumber(normalizedValue),
881
+ };
882
+ return;
883
+ case "camera.elevation":
884
+ draft.camera = {
885
+ ...(draft.camera ?? createEmptyViewCamera()),
886
+ elevation: parseFiniteNumber(normalizedValue),
887
+ };
888
+ return;
889
+ case "camera.roll":
890
+ draft.camera = {
891
+ ...(draft.camera ?? createEmptyViewCamera()),
892
+ roll: parseFiniteNumber(normalizedValue),
893
+ };
894
+ return;
895
+ case "camera.distance":
896
+ draft.camera = {
897
+ ...(draft.camera ?? createEmptyViewCamera()),
898
+ distance: parsePositiveNumber(normalizedValue),
899
+ };
900
+ return;
901
+ case "zoom":
902
+ case "scale":
903
+ draft.scale = parsePositiveNumber(normalizedValue);
904
+ return;
905
+ case "layers":
906
+ draft.layers = parseViewpointLayers(normalizedValue);
907
+ return;
908
+ case "query":
909
+ draft.filter = {
910
+ ...(draft.filter ?? createEmptyViewpointFilter()),
911
+ query: normalizedValue || null,
912
+ };
913
+ return;
914
+ case "types":
915
+ case "objecttypes":
916
+ draft.filter = {
917
+ ...(draft.filter ?? createEmptyViewpointFilter()),
918
+ objectTypes: parseViewpointObjectTypes(normalizedValue),
919
+ };
920
+ return;
921
+ case "tags":
922
+ draft.filter = {
923
+ ...(draft.filter ?? createEmptyViewpointFilter()),
924
+ tags: splitListValue(normalizedValue),
925
+ };
926
+ return;
927
+ case "groups":
928
+ draft.filter = {
929
+ ...(draft.filter ?? createEmptyViewpointFilter()),
930
+ groupIds: parseViewpointGroups(normalizedValue, document, relationships, objectMap),
931
+ };
932
+ return;
933
+ }
934
+ }
935
+ function finalizeViewpointDraft(draft, projection, preset, objectMap) {
936
+ const objectId = draft.focus && objectMap.has(draft.focus) ? draft.focus : null;
937
+ const selectedObjectId = draft.select && objectMap.has(draft.select)
938
+ ? draft.select
939
+ : objectId;
940
+ const filter = normalizeViewpointFilter(draft.filter);
941
+ const label = draft.label?.trim() || humanizeIdentifier(draft.id);
942
+ const resolvedProjection = draft.projection ?? projection;
943
+ const camera = normalizeViewCamera(draft.camera ?? null);
944
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
945
+ return {
946
+ id: draft.id,
947
+ label,
948
+ summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
949
+ objectId,
950
+ selectedObjectId,
951
+ eventIds: [...new Set(draft.eventIds ?? [])],
952
+ projection: resolvedProjection,
953
+ renderProjection,
954
+ camera,
955
+ preset: draft.preset ?? preset,
956
+ rotationDeg: draft.rotationDeg ?? 0,
957
+ scale: draft.scale ?? null,
958
+ layers: draft.layers ?? {},
959
+ filter,
960
+ generated: false,
961
+ };
962
+ }
963
+ function createEmptyViewpointFilter() {
964
+ return {
965
+ query: null,
966
+ objectTypes: [],
967
+ tags: [],
968
+ groupIds: [],
969
+ };
970
+ }
971
+ function createEmptyViewCamera() {
972
+ return {
973
+ azimuth: null,
974
+ elevation: null,
975
+ roll: null,
976
+ distance: null,
977
+ };
978
+ }
979
+ function normalizeViewpointFilter(filter) {
980
+ if (!filter) {
981
+ return null;
982
+ }
983
+ const normalized = {
984
+ query: filter.query?.trim() || null,
985
+ objectTypes: [...new Set(filter.objectTypes)],
986
+ tags: [...new Set(filter.tags)],
987
+ groupIds: [...new Set(filter.groupIds)],
988
+ };
989
+ return normalized.query ||
990
+ normalized.objectTypes.length > 0 ||
991
+ normalized.tags.length > 0 ||
992
+ normalized.groupIds.length > 0
993
+ ? normalized
994
+ : null;
995
+ }
996
+ function parseViewProjection(value) {
997
+ switch (value.toLowerCase()) {
998
+ case "topdown":
999
+ return "topdown";
1000
+ case "isometric":
1001
+ return "isometric";
1002
+ case "orthographic":
1003
+ return "orthographic";
1004
+ case "perspective":
1005
+ return "perspective";
1006
+ default:
1007
+ return null;
1008
+ }
1009
+ }
1010
+ function parseRenderPreset(value) {
1011
+ const normalized = value.toLowerCase();
1012
+ if (normalized === "diagram" ||
1013
+ normalized === "presentation" ||
1014
+ normalized === "atlas-card" ||
1015
+ normalized === "markdown") {
1016
+ return normalized;
1017
+ }
1018
+ return null;
1019
+ }
1020
+ function parseFiniteNumber(value) {
1021
+ const parsed = Number(value);
1022
+ return Number.isFinite(parsed) ? parsed : null;
1023
+ }
1024
+ function parsePositiveNumber(value) {
1025
+ const parsed = parseFiniteNumber(value);
1026
+ return parsed !== null && parsed > 0 ? parsed : null;
1027
+ }
1028
+ function parseViewpointLayers(value) {
1029
+ const next = {};
1030
+ for (const token of splitListValue(value)) {
1031
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
1032
+ const rawLayer = token.replace(/^[-!]+/, "").toLowerCase();
1033
+ if (rawLayer === "orbits") {
1034
+ next["orbits-back"] = enabled;
1035
+ next["orbits-front"] = enabled;
1036
+ continue;
1037
+ }
1038
+ if (rawLayer === "background" ||
1039
+ rawLayer === "guides" ||
1040
+ rawLayer === "orbits-back" ||
1041
+ rawLayer === "orbits-front" ||
1042
+ rawLayer === "relations" ||
1043
+ rawLayer === "events" ||
1044
+ rawLayer === "objects" ||
1045
+ rawLayer === "labels" ||
1046
+ rawLayer === "metadata") {
1047
+ next[rawLayer] = enabled;
1048
+ }
1049
+ }
1050
+ return next;
1051
+ }
1052
+ function parseViewpointObjectTypes(value) {
1053
+ return splitListValue(value).filter((entry) => entry === "star" ||
1054
+ entry === "planet" ||
1055
+ entry === "moon" ||
1056
+ entry === "belt" ||
1057
+ entry === "asteroid" ||
1058
+ entry === "comet" ||
1059
+ entry === "ring" ||
1060
+ entry === "structure" ||
1061
+ entry === "phenomenon");
1062
+ }
1063
+ function parseViewpointGroups(value, document, relationships, objectMap) {
1064
+ return splitListValue(value).map((entry) => {
1065
+ if (document.schemaVersion === "2.1" ||
1066
+ document.schemaVersion === "2.5" ||
1067
+ document.groups.some((group) => group.id === entry)) {
1068
+ return entry;
1069
+ }
1070
+ if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1071
+ return entry;
1072
+ }
1073
+ if (relationships.groupIds.has(entry)) {
1074
+ return relationships.groupIds.get(entry) ?? createGroupId(entry);
1075
+ }
1076
+ return objectMap.has(entry) ? createGroupId(entry) : createGroupId(entry);
1077
+ });
1078
+ }
1079
+ function splitListValue(value) {
1080
+ return value
1081
+ .split(/[\s,]+/)
1082
+ .map((entry) => entry.trim())
1083
+ .filter(Boolean);
1084
+ }
1085
+ function normalizeViewpointId(value) {
1086
+ return value
1087
+ .trim()
1088
+ .toLowerCase()
1089
+ .replace(/[^a-z0-9_-]+/g, "-")
1090
+ .replace(/^-+|-+$/g, "");
1091
+ }
1092
+ function humanizeIdentifier(value) {
1093
+ return value
1094
+ .split(/[-_]+/)
1095
+ .filter(Boolean)
1096
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
1097
+ .join(" ");
1098
+ }
1099
+ function createViewpointSummary(label, objectId, filter) {
1100
+ const parts = [label];
1101
+ if (objectId) {
1102
+ parts.push(`focus ${objectId}`);
1103
+ }
1104
+ if (filter?.objectTypes.length) {
1105
+ parts.push(filter.objectTypes.join("/"));
1106
+ }
1107
+ if (filter?.tags.length) {
1108
+ parts.push(`tags ${filter.tags.join(", ")}`);
1109
+ }
1110
+ if (filter?.query) {
1111
+ parts.push(`query "${filter.query}"`);
1112
+ }
1113
+ return parts.join(" - ");
1114
+ }
1115
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1116
+ let minX = Number.POSITIVE_INFINITY;
1117
+ let minY = Number.POSITIVE_INFINITY;
1118
+ let maxX = Number.NEGATIVE_INFINITY;
1119
+ let maxY = Number.NEGATIVE_INFINITY;
1120
+ const include = (x, y) => {
1121
+ minX = Math.min(minX, x);
1122
+ minY = Math.min(minY, y);
1123
+ maxX = Math.max(maxX, x);
1124
+ maxY = Math.max(maxY, y);
1125
+ };
1126
+ for (const orbit of orbitVisuals) {
1127
+ if (orbit.hidden)
1128
+ continue;
1129
+ includeOrbitBounds(orbit, include);
1130
+ }
1131
+ for (const leader of leaders) {
1132
+ if (leader.hidden)
1133
+ continue;
1134
+ include(leader.x1, leader.y1);
1135
+ include(leader.x2, leader.y2);
1136
+ }
1137
+ for (const object of objects) {
1138
+ if (object.hidden)
1139
+ continue;
1140
+ includeObjectBounds(object, include);
1141
+ }
1142
+ for (const label of labels) {
1143
+ if (label.hidden)
1144
+ continue;
1145
+ includeLabelBounds(label, include, labelMultiplier);
1146
+ }
1147
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1148
+ return createBounds(0, 0, width, height);
1149
+ }
1150
+ return createBounds(minX, minY, maxX, maxY);
1151
+ }
1152
+ function includeOrbitBounds(orbit, include) {
1153
+ const strokePadding = orbit.bandThickness !== undefined
1154
+ ? orbit.bandThickness / 2 + 4
1155
+ : orbit.band
1156
+ ? 10
1157
+ : 3;
1158
+ if (orbit.kind === "circle" && orbit.radius !== undefined) {
1159
+ include(orbit.cx - orbit.radius - strokePadding, orbit.cy - orbit.radius - strokePadding);
1160
+ include(orbit.cx + orbit.radius + strokePadding, orbit.cy + orbit.radius + strokePadding);
1161
+ return;
1162
+ }
1163
+ const rx = orbit.rx ?? orbit.radius ?? 0;
1164
+ const ry = orbit.ry ?? orbit.radius ?? 0;
1165
+ const points = sampleEllipseArcPoints(orbit.cx, orbit.cy, rx, ry, orbit.rotationDeg, 0, Math.PI * 2, ARC_SAMPLE_COUNT * 2);
1166
+ for (const point of points) {
1167
+ include(point.x - strokePadding, point.y - strokePadding);
1168
+ include(point.x + strokePadding, point.y + strokePadding);
1169
+ }
1170
+ }
1171
+ function createBounds(minX, minY, maxX, maxY) {
1172
+ return {
1173
+ minX,
1174
+ minY,
1175
+ maxX,
1176
+ maxY,
1177
+ width: maxX - minX,
1178
+ height: maxY - minY,
1179
+ centerX: minX + (maxX - minX) / 2,
1180
+ centerY: minY + (maxY - minY) / 2,
1181
+ };
1182
+ }
1183
+ function includeObjectBounds(object, include) {
1184
+ include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1185
+ include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1186
+ }
1187
+ function includeLabelBounds(label, include, labelMultiplier) {
1188
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1189
+ include(bounds.left, bounds.top);
1190
+ include(bounds.right, bounds.bottom);
1191
+ }
1192
+ function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1193
+ if (positions.has(object.id)) {
1194
+ return;
1195
+ }
1196
+ positions.set(object.id, {
1197
+ object,
1198
+ x,
1199
+ y,
1200
+ radius: visualRadiusFor(object, depth, context.scaleModel),
1201
+ sortKey: computeSortKey(x, y, depth),
1202
+ });
1203
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, depth + 1);
1204
+ }
1205
+ function placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, depth) {
1206
+ const parent = positions.get(object.id);
1207
+ if (!parent) {
1208
+ return;
1209
+ }
1210
+ const orbiting = [...(context.orbitChildren.get(object.id) ?? [])].sort(compareOrbiting);
1211
+ const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1212
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1213
+ orbiting.forEach((child, index) => {
1214
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1215
+ orbitDrafts.push({
1216
+ object: child,
1217
+ parentId: object.id,
1218
+ kind: orbitGeometry.kind,
1219
+ cx: orbitGeometry.cx,
1220
+ cy: orbitGeometry.cy,
1221
+ radius: orbitGeometry.radius,
1222
+ rx: orbitGeometry.rx,
1223
+ ry: orbitGeometry.ry,
1224
+ rotationDeg: orbitGeometry.rotationDeg,
1225
+ band: orbitGeometry.band,
1226
+ bandThickness: orbitGeometry.bandThickness,
1227
+ frontArcPath: orbitGeometry.frontArcPath,
1228
+ backArcPath: orbitGeometry.backArcPath,
1229
+ });
1230
+ placeObject(child, orbitGeometry.objectX, orbitGeometry.objectY, depth, positions, orbitDrafts, leaderDrafts, context);
1231
+ });
1232
+ const surfaceObjects = [...(context.surfaceChildren.get(object.id) ?? [])];
1233
+ surfaceObjects.forEach((child, index) => {
1234
+ const angle = angleForIndex(index, surfaceObjects.length, -Math.PI / 3);
1235
+ const leaderDistance = 28 * context.spacingFactor;
1236
+ const anchorOffset = projectPolarOffset(angle, parent.radius, context.projection, context.projection === "isometric" ? 0.9 : 1);
1237
+ const bodyOffset = projectPolarOffset(angle, parent.radius + leaderDistance, context.projection, context.projection === "isometric" ? 0.9 : 1);
1238
+ const anchorX = parent.x + anchorOffset.x;
1239
+ const anchorY = parent.y + anchorOffset.y;
1240
+ const x = parent.x + bodyOffset.x;
1241
+ const y = parent.y + bodyOffset.y;
1242
+ positions.set(child.id, {
1243
+ object: child,
1244
+ x,
1245
+ y,
1246
+ radius: visualRadiusFor(child, depth + 1, context.scaleModel),
1247
+ sortKey: computeSortKey(x, y, depth + 1),
1248
+ anchorX,
1249
+ anchorY,
1250
+ });
1251
+ leaderDrafts.push({
1252
+ object: child,
1253
+ groupId: context.objectMap.has(child.id) ? createGroupId(resolveGroupRootObjectId(child, context.objectMap)) : null,
1254
+ x1: anchorX,
1255
+ y1: anchorY,
1256
+ x2: x,
1257
+ y2: y,
1258
+ mode: "surface",
1259
+ });
1260
+ placeOrbitingChildren(child, positions, orbitDrafts, leaderDrafts, context, depth + 1);
1261
+ });
1262
+ }
1263
+ function compareOrbiting(left, right) {
1264
+ const leftMetric = orbitMetric(left);
1265
+ const rightMetric = orbitMetric(right);
1266
+ if (leftMetric !== null && rightMetric !== null && leftMetric !== rightMetric) {
1267
+ return leftMetric - rightMetric;
1268
+ }
1269
+ if (leftMetric !== null && rightMetric === null)
1270
+ return -1;
1271
+ if (leftMetric === null && rightMetric !== null)
1272
+ return 1;
1273
+ return left.id.localeCompare(right.id);
1274
+ }
1275
+ function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleModel) {
1276
+ const metrics = objects.map((object) => orbitMetric(object));
1277
+ const presentMetrics = metrics.filter((value) => value !== null);
1278
+ const innerPx = parentRadius + 56 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1279
+ const stepPx = (objects.length > 2 ? 54 : 64) * spacingFactor * scaleModel.orbitDistanceMultiplier;
1280
+ if (presentMetrics.length === 0) {
1281
+ return {
1282
+ metrics,
1283
+ minMetric: 0,
1284
+ maxMetric: 0,
1285
+ metricSpread: 0,
1286
+ innerPx,
1287
+ stepPx,
1288
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1289
+ minimumGapPx: stepPx * 0.42,
1290
+ };
1291
+ }
1292
+ const minMetric = Math.min(...presentMetrics);
1293
+ const maxMetric = Math.max(...presentMetrics);
1294
+ const metricSpread = maxMetric - minMetric;
1295
+ return {
1296
+ metrics,
1297
+ minMetric,
1298
+ maxMetric,
1299
+ metricSpread,
1300
+ innerPx,
1301
+ stepPx,
1302
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1303
+ minimumGapPx: stepPx * 0.42,
1304
+ };
1305
+ }
1306
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1307
+ const placement = object.placement;
1308
+ const band = object.type === "belt" || object.type === "ring";
1309
+ if (!placement || placement.mode !== "orbit") {
1310
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
1311
+ return {
1312
+ kind: "circle",
1313
+ cx: parent.x,
1314
+ cy: parent.y,
1315
+ radius: fallbackRadius,
1316
+ rotationDeg: 0,
1317
+ band,
1318
+ bandThickness: band
1319
+ ? 12 * context.scaleModel.ringThicknessMultiplier
1320
+ : undefined,
1321
+ objectX: parent.x,
1322
+ objectY: parent.y - fallbackRadius,
1323
+ };
1324
+ }
1325
+ const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1326
+ const semiMajor = orbitRadiusPx;
1327
+ const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1328
+ const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1329
+ const inclinationScale = context.projection === "isometric"
1330
+ ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) *
1331
+ ISO_FLATTENING
1332
+ : 1;
1333
+ const semiMinor = Math.max(baseMinor * inclinationScale, semiMajor * 0.14);
1334
+ const rotationDeg = unitValueToDegrees(placement.angle) ?? 0;
1335
+ const focusOffset = semiMajor * eccentricity;
1336
+ const centerOffset = rotateOffset(-focusOffset, 0, rotationDeg);
1337
+ const cx = parent.x + centerOffset.x;
1338
+ const cy = parent.y + centerOffset.y;
1339
+ const phase = resolveOrbitPhase(placement.phase, index, count);
1340
+ const objectPoint = ellipsePoint(cx, cy, semiMajor, semiMinor, rotationDeg, phase);
1341
+ const useCircle = context.projection === "topdown" &&
1342
+ eccentricity <= 0.0001 &&
1343
+ Math.abs(rotationDeg) <= 0.0001;
1344
+ const bandThickness = band
1345
+ ? resolveBandThickness(object, semiMajor, metricContext, context.scaleModel)
1346
+ : undefined;
1347
+ return {
1348
+ kind: useCircle ? "circle" : "ellipse",
1349
+ cx: useCircle ? parent.x : cx,
1350
+ cy: useCircle ? parent.y : cy,
1351
+ radius: useCircle ? semiMajor : undefined,
1352
+ rx: useCircle ? undefined : semiMajor,
1353
+ ry: useCircle ? undefined : semiMinor,
1354
+ rotationDeg,
1355
+ band,
1356
+ bandThickness,
1357
+ frontArcPath: context.projection === "isometric" || band
1358
+ ? buildEllipseArcPath(cx, cy, semiMajor, semiMinor, rotationDeg, 0, Math.PI)
1359
+ : undefined,
1360
+ backArcPath: context.projection === "isometric" || band
1361
+ ? buildEllipseArcPath(cx, cy, semiMajor, semiMinor, rotationDeg, Math.PI, Math.PI * 2)
1362
+ : undefined,
1363
+ objectX: objectPoint.x,
1364
+ objectY: objectPoint.y,
1365
+ };
1366
+ }
1367
+ function resolveOrbitRadiusPx(metric, metricContext) {
1368
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
1369
+ }
1370
+ function resolveOrbitRadiiPx(objects, metricContext) {
1371
+ const radii = [];
1372
+ objects.forEach((object, index) => {
1373
+ const metric = orbitMetric(object);
1374
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
1375
+ const baseRadius = metric === null
1376
+ ? fallbackRadius
1377
+ : resolveOrbitRadiusPx(metric, metricContext);
1378
+ const minimumRadius = index === 0
1379
+ ? metricContext.innerPx
1380
+ : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
1381
+ radii.push(Math.max(baseRadius, minimumRadius));
1382
+ });
1383
+ return radii;
1384
+ }
1385
+ function orbitMetric(object) {
1386
+ if (!object.placement || object.placement.mode !== "orbit") {
1387
+ return null;
1388
+ }
1389
+ return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1390
+ }
1391
+ function log2(value) {
1392
+ return Math.log(value) / Math.log(2);
1393
+ }
1394
+ function resolveOrbitPhase(phase, index, count) {
1395
+ const degreeValue = phase ? unitValueToDegrees(phase) : null;
1396
+ if (degreeValue !== null) {
1397
+ return degreesToRadians(degreeValue - 90);
1398
+ }
1399
+ return angleForIndex(index, count, -Math.PI / 2);
1400
+ }
1401
+ function resolveBandThickness(object, orbitRadius, metricContext, scaleModel) {
1402
+ const innerMetric = toDistanceMetric(toUnitValue(object.properties.inner));
1403
+ const outerMetric = toDistanceMetric(toUnitValue(object.properties.outer));
1404
+ if (innerMetric !== null && outerMetric !== null) {
1405
+ const thicknessMetric = Math.abs(outerMetric - innerMetric);
1406
+ if (metricContext.metricSpread > 0) {
1407
+ return clampNumber((thicknessMetric / metricContext.metricSpread) *
1408
+ metricContext.pixelSpread *
1409
+ scaleModel.ringThicknessMultiplier, 8, 54);
1410
+ }
1411
+ const referenceMetric = Math.max(Math.max(innerMetric, outerMetric), 0.0001);
1412
+ return clampNumber((thicknessMetric / referenceMetric) *
1413
+ orbitRadius *
1414
+ 0.75 *
1415
+ scaleModel.ringThicknessMultiplier, 8, 48);
1416
+ }
1417
+ const fallbackBase = object.type === "belt" ? 18 : 12;
1418
+ return fallbackBase * scaleModel.ringThicknessMultiplier;
1419
+ }
1420
+ function resolveAtPosition(reference, positions, objectMap, index, count, width, height, padding, context) {
1421
+ if (reference.kind === "lagrange") {
1422
+ return resolveLagrangePosition(reference, positions, objectMap, width, height);
1423
+ }
1424
+ if (reference.kind === "anchor") {
1425
+ const anchor = positions.get(reference.objectId);
1426
+ if (anchor) {
1427
+ const angle = angleForIndex(index, count, Math.PI / 5);
1428
+ const distance = (anchor.radius + 36) * context.scaleModel.labelMultiplier;
1429
+ const offset = projectPolarOffset(angle, distance, context.projection, context.projection === "isometric" ? 0.92 : 1);
1430
+ return {
1431
+ x: anchor.x + offset.x,
1432
+ y: anchor.y + offset.y,
1433
+ anchorX: anchor.x,
1434
+ anchorY: anchor.y,
1435
+ };
1436
+ }
1437
+ }
1438
+ if (reference.kind === "named") {
1439
+ const anchor = positions.get(reference.name);
1440
+ if (anchor) {
1441
+ const angle = angleForIndex(index, count, Math.PI / 6);
1442
+ const distance = (anchor.radius + 36) * context.scaleModel.labelMultiplier;
1443
+ const offset = projectPolarOffset(angle, distance, context.projection, context.projection === "isometric" ? 0.92 : 1);
1444
+ return {
1445
+ x: anchor.x + offset.x,
1446
+ y: anchor.y + offset.y,
1447
+ anchorX: anchor.x,
1448
+ anchorY: anchor.y,
1449
+ };
1450
+ }
1451
+ }
1452
+ return {
1453
+ x: width - padding - 170,
1454
+ y: height -
1455
+ padding -
1456
+ 86 -
1457
+ index * 58 * context.scaleModel.freePlacementMultiplier,
1458
+ };
1459
+ }
1460
+ function resolveLagrangePosition(reference, positions, objectMap, width, height) {
1461
+ const primary = reference.secondary
1462
+ ? positions.get(reference.primary)
1463
+ : deriveParentAnchor(reference.primary, positions, objectMap);
1464
+ const secondary = positions.get(reference.secondary ?? reference.primary);
1465
+ if (!primary || !secondary) {
1466
+ return {
1467
+ x: width * 0.7,
1468
+ y: height * 0.25,
1469
+ };
1470
+ }
1471
+ const dx = secondary.x - primary.x;
1472
+ const dy = secondary.y - primary.y;
1473
+ const distance = Math.hypot(dx, dy) || 1;
1474
+ const ux = dx / distance;
1475
+ const uy = dy / distance;
1476
+ const nx = -uy;
1477
+ const ny = ux;
1478
+ const offset = clampNumber(distance * 0.25, 24, 68);
1479
+ switch (reference.point) {
1480
+ case "L1":
1481
+ return {
1482
+ x: secondary.x - ux * offset,
1483
+ y: secondary.y - uy * offset,
1484
+ anchorX: secondary.x,
1485
+ anchorY: secondary.y,
1486
+ };
1487
+ case "L2":
1488
+ return {
1489
+ x: secondary.x + ux * offset,
1490
+ y: secondary.y + uy * offset,
1491
+ anchorX: secondary.x,
1492
+ anchorY: secondary.y,
1493
+ };
1494
+ case "L3":
1495
+ return {
1496
+ x: primary.x - ux * offset,
1497
+ y: primary.y - uy * offset,
1498
+ anchorX: primary.x,
1499
+ anchorY: primary.y,
1500
+ };
1501
+ case "L4":
1502
+ return {
1503
+ x: secondary.x + (ux * 0.5 - nx * 0.8660254) * offset,
1504
+ y: secondary.y + (uy * 0.5 - ny * 0.8660254) * offset,
1505
+ anchorX: secondary.x,
1506
+ anchorY: secondary.y,
1507
+ };
1508
+ case "L5":
1509
+ return {
1510
+ x: secondary.x + (ux * 0.5 + nx * 0.8660254) * offset,
1511
+ y: secondary.y + (uy * 0.5 + ny * 0.8660254) * offset,
1512
+ anchorX: secondary.x,
1513
+ anchorY: secondary.y,
1514
+ };
1515
+ }
1516
+ }
1517
+ function buildSceneRelationships(objects, objectMap) {
1518
+ const parentIds = new Map();
1519
+ const childIds = new Map();
1520
+ for (const object of objects) {
1521
+ const parentId = resolveParentId(object, objectMap);
1522
+ parentIds.set(object.id, parentId);
1523
+ if (parentId) {
1524
+ const existing = childIds.get(parentId);
1525
+ if (existing) {
1526
+ existing.push(object.id);
1527
+ }
1528
+ else {
1529
+ childIds.set(parentId, [object.id]);
1530
+ }
1531
+ }
1532
+ if (!childIds.has(object.id)) {
1533
+ childIds.set(object.id, []);
1534
+ }
1535
+ }
1536
+ const ancestorIds = new Map();
1537
+ const groupIds = new Map();
1538
+ const groupRoots = new Map();
1539
+ const buildAncestors = (objectId) => {
1540
+ const cached = ancestorIds.get(objectId);
1541
+ if (cached) {
1542
+ return cached;
1543
+ }
1544
+ const seen = new Set();
1545
+ const results = [];
1546
+ let cursor = parentIds.get(objectId) ?? null;
1547
+ while (cursor && !seen.has(cursor)) {
1548
+ results.push(cursor);
1549
+ seen.add(cursor);
1550
+ cursor = parentIds.get(cursor) ?? null;
1551
+ }
1552
+ ancestorIds.set(objectId, results);
1553
+ return results;
1554
+ };
1555
+ const resolveGroupRootObjectId = (objectId) => {
1556
+ const cached = groupRoots.get(groupIds.get(objectId) ?? "");
1557
+ if (cached) {
1558
+ return cached;
1559
+ }
1560
+ const parentId = parentIds.get(objectId) ?? null;
1561
+ const object = objectMap.get(objectId);
1562
+ let rootObjectId = objectId;
1563
+ if (object?.placement && object.placement.mode !== "free" && parentId) {
1564
+ rootObjectId = resolveGroupRootObjectId(parentId);
1565
+ }
1566
+ return rootObjectId;
1567
+ };
1568
+ for (const object of objects) {
1569
+ buildAncestors(object.id);
1570
+ const rootObjectId = resolveGroupRootObjectId(object.id);
1571
+ const groupId = createGroupId(rootObjectId);
1572
+ groupIds.set(object.id, groupId);
1573
+ groupRoots.set(groupId, rootObjectId);
1574
+ }
1575
+ return {
1576
+ parentIds,
1577
+ childIds,
1578
+ ancestorIds,
1579
+ groupIds,
1580
+ groupRoots,
1581
+ };
1582
+ }
1583
+ function resolveParentId(object, objectMap) {
1584
+ const placement = object.placement;
1585
+ if (!placement) {
1586
+ return null;
1587
+ }
1588
+ switch (placement.mode) {
1589
+ case "orbit":
1590
+ case "surface":
1591
+ return objectMap.has(placement.target) ? placement.target : null;
1592
+ case "at":
1593
+ switch (placement.reference.kind) {
1594
+ case "anchor":
1595
+ return objectMap.has(placement.reference.objectId) ? placement.reference.objectId : null;
1596
+ case "named":
1597
+ return objectMap.has(placement.reference.name) ? placement.reference.name : null;
1598
+ case "lagrange":
1599
+ if (placement.reference.secondary && objectMap.has(placement.reference.secondary)) {
1600
+ return placement.reference.secondary;
1601
+ }
1602
+ return objectMap.has(placement.reference.primary) ? placement.reference.primary : null;
1603
+ }
1604
+ case "free":
1605
+ return null;
1606
+ }
1607
+ }
1608
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1609
+ let minX = Number.POSITIVE_INFINITY;
1610
+ let minY = Number.POSITIVE_INFINITY;
1611
+ let maxX = Number.NEGATIVE_INFINITY;
1612
+ let maxY = Number.NEGATIVE_INFINITY;
1613
+ const include = (x, y) => {
1614
+ minX = Math.min(minX, x);
1615
+ minY = Math.min(minY, y);
1616
+ maxX = Math.max(maxX, x);
1617
+ maxY = Math.max(maxY, y);
1618
+ };
1619
+ for (const object of objects) {
1620
+ if (!object.hidden && group.objectIds.includes(object.objectId)) {
1621
+ includeObjectBounds(object, include);
1622
+ }
1623
+ }
1624
+ for (const orbit of orbitVisuals) {
1625
+ if (!orbit.hidden && group.orbitIds.includes(orbit.objectId)) {
1626
+ includeOrbitBounds(orbit, include);
1627
+ }
1628
+ }
1629
+ for (const leader of leaders) {
1630
+ if (!leader.hidden && group.leaderIds.includes(leader.objectId)) {
1631
+ include(leader.x1, leader.y1);
1632
+ include(leader.x2, leader.y2);
1633
+ }
1634
+ }
1635
+ for (const label of labels) {
1636
+ if (!label.hidden && group.labelIds.includes(label.objectId)) {
1637
+ includeLabelBounds(label, include, labelMultiplier);
1638
+ }
1639
+ }
1640
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1641
+ return createBounds(0, 0, 0, 0);
1642
+ }
1643
+ return createBounds(minX, minY, maxX, maxY);
1644
+ }
1645
+ function resolveGroupRootObjectId(object, objectMap) {
1646
+ let current = object;
1647
+ const seen = new Set();
1648
+ while (current.placement && current.placement.mode !== "free" && !seen.has(current.id)) {
1649
+ seen.add(current.id);
1650
+ const parentId = resolveParentId(current, objectMap);
1651
+ if (!parentId) {
1652
+ break;
1653
+ }
1654
+ const parent = objectMap.get(parentId);
1655
+ if (!parent) {
1656
+ break;
1657
+ }
1658
+ current = parent;
1659
+ }
1660
+ return current.id;
1661
+ }
1662
+ function createLabelRect(object, placement, labelMultiplier) {
1663
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
1664
+ }
1665
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
1666
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
1667
+ const labelWidth = labelHalfWidth * 2;
1668
+ const topPadding = direction === "above" ? 18 : 12;
1669
+ const bottomPadding = direction === "above" ? 8 : 12;
1670
+ let left = x - labelHalfWidth;
1671
+ let right = x + labelHalfWidth;
1672
+ if (textAnchor === "start") {
1673
+ left = x;
1674
+ right = x + labelWidth;
1675
+ }
1676
+ else if (textAnchor === "end") {
1677
+ left = x - labelWidth;
1678
+ right = x;
1679
+ }
1680
+ return {
1681
+ left,
1682
+ right,
1683
+ top: Math.min(labelY, secondaryY) - topPadding,
1684
+ bottom: Math.max(labelY, secondaryY) + bottomPadding,
1685
+ };
1686
+ }
1687
+ function rectsOverlap(left, right) {
1688
+ return !(left.right < right.left ||
1689
+ right.right < left.left ||
1690
+ left.bottom < right.top ||
1691
+ right.bottom < left.top);
1692
+ }
1693
+ function deriveParentAnchor(objectId, positions, objectMap) {
1694
+ const object = objectMap.get(objectId);
1695
+ if (!object?.placement || object.placement.mode !== "orbit") {
1696
+ return positions.get(objectId);
1697
+ }
1698
+ return positions.get(object.placement.target);
1699
+ }
1700
+ function visualRadiusFor(object, depth, scaleModel) {
1701
+ const explicitRadius = toVisualSizeMetric(object.properties.radius, scaleModel);
1702
+ if (explicitRadius !== null) {
1703
+ return explicitRadius;
1704
+ }
1705
+ const multiplier = scaleModel.bodyRadiusMultiplier;
1706
+ switch (object.type) {
1707
+ case "star":
1708
+ return clampNumber((depth === 0 ? 28 : 20) * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1709
+ case "planet":
1710
+ return clampNumber(12 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1711
+ case "moon":
1712
+ return clampNumber(7 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1713
+ case "belt":
1714
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1715
+ case "asteroid":
1716
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1717
+ case "comet":
1718
+ return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1719
+ case "ring":
1720
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1721
+ case "structure":
1722
+ return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1723
+ case "phenomenon":
1724
+ return clampNumber(8 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1725
+ }
1726
+ }
1727
+ function visualExtentForObject(object, radius, scaleModel) {
1728
+ const atmosphereBoost = typeof object.properties.atmosphere === "string" ? 4 : 0;
1729
+ switch (object.type) {
1730
+ case "star":
1731
+ return radius * 2.4;
1732
+ case "phenomenon":
1733
+ return radius * 1.25;
1734
+ case "structure":
1735
+ return radius + 2;
1736
+ default:
1737
+ return Math.min(radius + atmosphereBoost, scaleModel.maxBodyRadius + 10);
1738
+ }
1739
+ }
1740
+ function toDistanceMetric(value) {
1741
+ if (!value)
1742
+ return null;
1743
+ switch (value.unit) {
1744
+ case "au":
1745
+ return value.value;
1746
+ case "km":
1747
+ return value.value / AU_IN_KM;
1748
+ case "m":
1749
+ return (value.value / 1_000) / AU_IN_KM;
1750
+ case "ly":
1751
+ return value.value * LY_IN_AU;
1752
+ case "pc":
1753
+ return value.value * PC_IN_AU;
1754
+ case "kpc":
1755
+ return value.value * KPC_IN_AU;
1756
+ case "re":
1757
+ return (value.value * EARTH_RADIUS_IN_KM) / AU_IN_KM;
1758
+ case "rj":
1759
+ return (value.value * JUPITER_RADIUS_IN_KM) / AU_IN_KM;
1760
+ case "sol":
1761
+ return (value.value * SOLAR_RADIUS_IN_KM) / AU_IN_KM;
1762
+ default:
1763
+ // Unitless or non-distance units (me, mj, s, min, h, d, y, ky, my, gy, K, deg):
1764
+ // return raw value — renderer treats it as an abstract metric
1765
+ return value.value;
1766
+ }
1767
+ }
1768
+ function freePlacementOffsetPx(distance, scaleModel) {
1769
+ const metric = toDistanceMetric(distance ?? null);
1770
+ if (metric === null || metric <= 0) {
1771
+ return 0;
1772
+ }
1773
+ return clampNumber(metric * 96 * scaleModel.freePlacementMultiplier, 0, 420);
1774
+ }
1775
+ function toVisualSizeMetric(value, scaleModel) {
1776
+ const unitValue = toUnitValue(value);
1777
+ if (!unitValue) {
1778
+ return null;
1779
+ }
1780
+ let size;
1781
+ switch (unitValue.unit) {
1782
+ case "sol":
1783
+ size = clampNumber(unitValue.value * 22, 14, 40);
1784
+ break;
1785
+ case "re":
1786
+ size = clampNumber(unitValue.value * 10, 6, 18);
1787
+ break;
1788
+ case "km":
1789
+ size = clampNumber(Math.log10(Math.max(unitValue.value, 1)) * 2.6, 4, 16);
1790
+ break;
1791
+ default:
1792
+ size = clampNumber(unitValue.value * 4, 4, 20);
1793
+ break;
1794
+ }
1795
+ return clampNumber(size * scaleModel.bodyRadiusMultiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1796
+ }
1797
+ function toUnitValue(value) {
1798
+ if (!value || typeof value !== "object" || !("value" in value)) {
1799
+ return null;
1800
+ }
1801
+ return value;
1802
+ }
1803
+ function unitValueToDegrees(value) {
1804
+ if (!value) {
1805
+ return null;
1806
+ }
1807
+ return value.unit === "deg" || value.unit === null ? value.value : null;
1808
+ }
1809
+ function angleForIndex(index, count, startAngle) {
1810
+ if (count <= 1)
1811
+ return startAngle;
1812
+ return startAngle + (index * Math.PI * 2) / count;
1813
+ }
1814
+ function buildEllipseArcPath(cx, cy, rx, ry, rotationDeg, start, end) {
1815
+ const points = sampleEllipseArcPoints(cx, cy, rx, ry, rotationDeg, start, end, ARC_SAMPLE_COUNT);
1816
+ if (points.length === 0) {
1817
+ return "";
1818
+ }
1819
+ return points
1820
+ .map((point, index) => `${index === 0 ? "M" : "L"} ${formatNumber(point.x)} ${formatNumber(point.y)}`)
1821
+ .join(" ");
1822
+ }
1823
+ function sampleEllipseArcPoints(cx, cy, rx, ry, rotationDeg, start, end, segments) {
1824
+ const points = [];
1825
+ for (let index = 0; index <= segments; index += 1) {
1826
+ const t = start + ((end - start) * index) / segments;
1827
+ points.push(ellipsePoint(cx, cy, rx, ry, rotationDeg, t));
1828
+ }
1829
+ return points;
1830
+ }
1831
+ function ellipsePoint(cx, cy, rx, ry, rotationDeg, angle) {
1832
+ const localX = rx * Math.cos(angle);
1833
+ const localY = ry * Math.sin(angle);
1834
+ const rotated = rotateOffset(localX, localY, rotationDeg);
1835
+ return {
1836
+ x: cx + rotated.x,
1837
+ y: cy + rotated.y,
1838
+ };
1839
+ }
1840
+ function rotateOffset(x, y, rotationDeg) {
1841
+ const radians = degreesToRadians(rotationDeg);
1842
+ return {
1843
+ x: x * Math.cos(radians) - y * Math.sin(radians),
1844
+ y: x * Math.sin(radians) + y * Math.cos(radians),
1845
+ };
1846
+ }
1847
+ function projectPolarOffset(angle, distance, projection, verticalFactor) {
1848
+ const yScale = projection === "isometric" ? ISO_FLATTENING * verticalFactor : verticalFactor;
1849
+ return {
1850
+ x: Math.cos(angle) * distance,
1851
+ y: Math.sin(angle) * distance * yScale,
1852
+ };
1853
+ }
1854
+ function computeSortKey(x, y, depth) {
1855
+ return y * 1_000 + x + depth * 0.01;
1856
+ }
1857
+ function clampNumber(value, min, max) {
1858
+ return Math.min(Math.max(value, min), max);
1859
+ }
1860
+ function pushGrouped(map, key, value) {
1861
+ const existing = map.get(key);
1862
+ if (existing) {
1863
+ existing.push(value);
1864
+ }
1865
+ else {
1866
+ map.set(key, [value]);
1867
+ }
1868
+ }
1869
+ function createRenderId(objectId) {
1870
+ const normalized = objectId
1871
+ .trim()
1872
+ .toLowerCase()
1873
+ .replace(/[^a-z0-9_-]+/g, "-")
1874
+ .replace(/^-+|-+$/g, "") || "object";
1875
+ return `wo-${normalized}`;
1876
+ }
1877
+ function createGroupId(objectId) {
1878
+ return `${createRenderId(objectId)}-group`;
1879
+ }
1880
+ function customColorFor(value) {
1881
+ return typeof value === "string" && value.trim() ? value : undefined;
1882
+ }
1883
+ function estimateLabelHalfWidth(object, labelMultiplier) {
1884
+ const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
1885
+ const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
1886
+ return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
1887
+ }
1888
+ function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
1889
+ const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
1890
+ const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
1891
+ return Math.max(primaryWidth, secondaryWidth, 24);
1892
+ }
1893
+ function capitalizeLabel(value) {
1894
+ return value.length > 0 ? value[0].toUpperCase() + value.slice(1) : value;
1895
+ }
1896
+ function degreesToRadians(value) {
1897
+ return (value * Math.PI) / 180;
1898
+ }
1899
+ function formatNumber(value) {
1900
+ return Number.isInteger(value) ? String(value) : value.toFixed(2);
1901
+ }