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,3951 @@
1
+ "use strict";
2
+ (() => {
3
+ // packages/core/dist/errors.js
4
+ var WorldOrbitError = class _WorldOrbitError extends Error {
5
+ line;
6
+ column;
7
+ constructor(message, line, column) {
8
+ const locationSuffix = line === void 0 ? "" : ` (line ${line}${column === void 0 ? "" : `, column ${column}`})`;
9
+ super(`${message}${locationSuffix}`);
10
+ this.name = "WorldOrbitError";
11
+ this.line = line;
12
+ this.column = column;
13
+ }
14
+ static fromLocation(message, location) {
15
+ return new _WorldOrbitError(message, location?.line, location?.column);
16
+ }
17
+ };
18
+
19
+ // packages/core/dist/schema.js
20
+ var ALL_OBJECTS = [
21
+ "system",
22
+ "star",
23
+ "planet",
24
+ "moon",
25
+ "belt",
26
+ "asteroid",
27
+ "comet",
28
+ "ring",
29
+ "structure",
30
+ "phenomenon"
31
+ ];
32
+ var NON_SYSTEM_OBJECTS = ALL_OBJECTS.filter((objectType) => objectType !== "system");
33
+ var IMAGE_OBJECTS = [
34
+ "star",
35
+ "planet",
36
+ "moon",
37
+ "asteroid",
38
+ "comet",
39
+ "structure",
40
+ "phenomenon"
41
+ ];
42
+ var ANCHORED_OBJECTS = ["structure", "phenomenon"];
43
+ var ORBITAL_OBJECTS = [
44
+ "star",
45
+ "planet",
46
+ "moon",
47
+ "belt",
48
+ "asteroid",
49
+ "comet",
50
+ "ring",
51
+ "structure",
52
+ "phenomenon"
53
+ ];
54
+ var FREE_OBJECTS = [
55
+ "star",
56
+ "planet",
57
+ "moon",
58
+ "belt",
59
+ "asteroid",
60
+ "comet",
61
+ "ring",
62
+ "structure",
63
+ "phenomenon"
64
+ ];
65
+ function createField(key, options) {
66
+ return {
67
+ key,
68
+ ...options
69
+ };
70
+ }
71
+ var WORLDORBIT_OBJECT_TYPES = new Set(ALL_OBJECTS);
72
+ var WORLDORBIT_FIELD_SCHEMAS = new Map([
73
+ createField("orbit", {
74
+ kind: "string",
75
+ placement: true,
76
+ arity: "single",
77
+ objectTypes: ORBITAL_OBJECTS
78
+ }),
79
+ createField("distance", {
80
+ kind: "unit",
81
+ placement: true,
82
+ arity: "single",
83
+ objectTypes: ORBITAL_OBJECTS,
84
+ unitFamily: "distance"
85
+ }),
86
+ createField("semiMajor", {
87
+ kind: "unit",
88
+ placement: true,
89
+ arity: "single",
90
+ objectTypes: ORBITAL_OBJECTS,
91
+ unitFamily: "distance"
92
+ }),
93
+ createField("eccentricity", {
94
+ kind: "number",
95
+ placement: true,
96
+ arity: "single",
97
+ objectTypes: ORBITAL_OBJECTS
98
+ }),
99
+ createField("period", {
100
+ kind: "unit",
101
+ placement: true,
102
+ arity: "single",
103
+ objectTypes: ORBITAL_OBJECTS,
104
+ unitFamily: "duration"
105
+ }),
106
+ createField("angle", {
107
+ kind: "unit",
108
+ placement: true,
109
+ arity: "single",
110
+ objectTypes: ORBITAL_OBJECTS,
111
+ unitFamily: "angle"
112
+ }),
113
+ createField("inclination", {
114
+ kind: "unit",
115
+ placement: true,
116
+ arity: "single",
117
+ objectTypes: ORBITAL_OBJECTS,
118
+ unitFamily: "angle"
119
+ }),
120
+ createField("phase", {
121
+ kind: "unit",
122
+ placement: true,
123
+ arity: "single",
124
+ objectTypes: ORBITAL_OBJECTS,
125
+ unitFamily: "angle"
126
+ }),
127
+ createField("at", {
128
+ kind: "string",
129
+ placement: true,
130
+ arity: "single",
131
+ objectTypes: ANCHORED_OBJECTS
132
+ }),
133
+ createField("surface", {
134
+ kind: "string",
135
+ placement: true,
136
+ arity: "single",
137
+ objectTypes: ANCHORED_OBJECTS
138
+ }),
139
+ createField("free", {
140
+ kind: "string",
141
+ placement: true,
142
+ arity: "single",
143
+ objectTypes: FREE_OBJECTS
144
+ }),
145
+ createField("kind", {
146
+ kind: "string",
147
+ placement: false,
148
+ arity: "single",
149
+ objectTypes: NON_SYSTEM_OBJECTS
150
+ }),
151
+ createField("class", {
152
+ kind: "string",
153
+ placement: false,
154
+ arity: "single",
155
+ objectTypes: NON_SYSTEM_OBJECTS
156
+ }),
157
+ createField("culture", {
158
+ kind: "string",
159
+ placement: false,
160
+ arity: "single",
161
+ objectTypes: NON_SYSTEM_OBJECTS
162
+ }),
163
+ createField("tags", {
164
+ kind: "list",
165
+ placement: false,
166
+ arity: "multiple",
167
+ objectTypes: ALL_OBJECTS
168
+ }),
169
+ createField("color", {
170
+ kind: "string",
171
+ placement: false,
172
+ arity: "single",
173
+ objectTypes: ALL_OBJECTS
174
+ }),
175
+ createField("image", {
176
+ kind: "string",
177
+ placement: false,
178
+ arity: "single",
179
+ objectTypes: IMAGE_OBJECTS
180
+ }),
181
+ createField("hidden", {
182
+ kind: "boolean",
183
+ placement: false,
184
+ arity: "single",
185
+ objectTypes: ALL_OBJECTS
186
+ }),
187
+ createField("radius", {
188
+ kind: "unit",
189
+ placement: false,
190
+ arity: "single",
191
+ objectTypes: NON_SYSTEM_OBJECTS,
192
+ unitFamily: "radius"
193
+ }),
194
+ createField("mass", {
195
+ kind: "unit",
196
+ placement: false,
197
+ arity: "single",
198
+ objectTypes: NON_SYSTEM_OBJECTS,
199
+ unitFamily: "mass"
200
+ }),
201
+ createField("density", {
202
+ kind: "unit",
203
+ placement: false,
204
+ arity: "single",
205
+ objectTypes: NON_SYSTEM_OBJECTS,
206
+ unitFamily: "generic"
207
+ }),
208
+ createField("gravity", {
209
+ kind: "unit",
210
+ placement: false,
211
+ arity: "single",
212
+ objectTypes: NON_SYSTEM_OBJECTS,
213
+ unitFamily: "generic"
214
+ }),
215
+ createField("temperature", {
216
+ kind: "unit",
217
+ placement: false,
218
+ arity: "single",
219
+ objectTypes: NON_SYSTEM_OBJECTS,
220
+ unitFamily: "generic"
221
+ }),
222
+ createField("albedo", {
223
+ kind: "number",
224
+ placement: false,
225
+ arity: "single",
226
+ objectTypes: NON_SYSTEM_OBJECTS
227
+ }),
228
+ createField("atmosphere", {
229
+ kind: "string",
230
+ placement: false,
231
+ arity: "single",
232
+ objectTypes: ["planet", "moon", "asteroid", "comet", "phenomenon"]
233
+ }),
234
+ createField("inner", {
235
+ kind: "unit",
236
+ placement: false,
237
+ arity: "single",
238
+ objectTypes: ["belt", "ring", "phenomenon"],
239
+ unitFamily: "distance"
240
+ }),
241
+ createField("outer", {
242
+ kind: "unit",
243
+ placement: false,
244
+ arity: "single",
245
+ objectTypes: ["belt", "ring", "phenomenon"],
246
+ unitFamily: "distance"
247
+ }),
248
+ createField("view", {
249
+ kind: "string",
250
+ placement: false,
251
+ arity: "single",
252
+ objectTypes: ["system"]
253
+ }),
254
+ createField("scale", {
255
+ kind: "string",
256
+ placement: false,
257
+ arity: "single",
258
+ objectTypes: ["system"]
259
+ }),
260
+ createField("units", {
261
+ kind: "string",
262
+ placement: false,
263
+ arity: "single",
264
+ objectTypes: ["system"]
265
+ }),
266
+ createField("title", {
267
+ kind: "string",
268
+ placement: false,
269
+ arity: "single",
270
+ objectTypes: ["system"]
271
+ }),
272
+ createField("on", {
273
+ kind: "string",
274
+ placement: false,
275
+ arity: "single",
276
+ objectTypes: NON_SYSTEM_OBJECTS
277
+ }),
278
+ createField("source", {
279
+ kind: "string",
280
+ placement: false,
281
+ arity: "single",
282
+ objectTypes: NON_SYSTEM_OBJECTS
283
+ }),
284
+ createField("cycle", {
285
+ kind: "unit",
286
+ placement: false,
287
+ arity: "single",
288
+ objectTypes: NON_SYSTEM_OBJECTS,
289
+ unitFamily: "duration"
290
+ })
291
+ ].map((schema) => [schema.key, schema]));
292
+ var WORLDORBIT_FIELD_KEYS = new Set(WORLDORBIT_FIELD_SCHEMAS.keys());
293
+ function getFieldSchema(key) {
294
+ return WORLDORBIT_FIELD_SCHEMAS.get(key);
295
+ }
296
+ function isKnownFieldKey(key) {
297
+ return WORLDORBIT_FIELD_KEYS.has(key);
298
+ }
299
+ function supportsObjectType(schema, objectType) {
300
+ return schema.objectTypes.includes(objectType);
301
+ }
302
+ function unitFamilyAllowsUnit(family, unit) {
303
+ switch (family) {
304
+ case "distance":
305
+ return unit === null || ["au", "km", "re", "sol"].includes(unit);
306
+ case "radius":
307
+ return unit === null || ["km", "re", "sol"].includes(unit);
308
+ case "mass":
309
+ return unit === null || ["me", "sol"].includes(unit);
310
+ case "duration":
311
+ return unit === null || ["h", "d", "y"].includes(unit);
312
+ case "angle":
313
+ return unit === null || unit === "deg";
314
+ case "generic":
315
+ return true;
316
+ }
317
+ }
318
+
319
+ // packages/core/dist/tokenize.js
320
+ function tokenizeLineDetailed(input, options = {}) {
321
+ const tokens = [];
322
+ const columnOffset = options.columnOffset ?? 0;
323
+ let current = "";
324
+ let tokenColumn = null;
325
+ let tokenWasQuoted = false;
326
+ let inQuotes = false;
327
+ let quoteColumn = null;
328
+ const pushToken = () => {
329
+ if (tokenColumn === null) {
330
+ return;
331
+ }
332
+ tokens.push({
333
+ value: current,
334
+ column: tokenColumn,
335
+ quoted: tokenWasQuoted
336
+ });
337
+ current = "";
338
+ tokenColumn = null;
339
+ tokenWasQuoted = false;
340
+ };
341
+ for (let index = 0; index < input.length; index++) {
342
+ const ch = input[index];
343
+ const absoluteColumn = columnOffset + index + 1;
344
+ if (inQuotes && ch === "\\") {
345
+ const next = input[index + 1];
346
+ if (next === '"' || next === "\\") {
347
+ current += next;
348
+ index++;
349
+ continue;
350
+ }
351
+ }
352
+ if (ch === '"') {
353
+ if (!inQuotes) {
354
+ if (tokenColumn === null) {
355
+ tokenColumn = absoluteColumn;
356
+ }
357
+ tokenWasQuoted = true;
358
+ quoteColumn = absoluteColumn;
359
+ inQuotes = true;
360
+ } else {
361
+ inQuotes = false;
362
+ }
363
+ continue;
364
+ }
365
+ if (!inQuotes && /\s/.test(ch)) {
366
+ pushToken();
367
+ continue;
368
+ }
369
+ if (tokenColumn === null) {
370
+ tokenColumn = absoluteColumn;
371
+ }
372
+ current += ch;
373
+ }
374
+ if (inQuotes) {
375
+ throw new WorldOrbitError("Unclosed quote in line", options.line, quoteColumn ?? columnOffset + input.length);
376
+ }
377
+ pushToken();
378
+ return tokens;
379
+ }
380
+ function getIndent(rawLine) {
381
+ return rawLine.match(/^\s*/)?.[0].length ?? 0;
382
+ }
383
+
384
+ // packages/core/dist/parse.js
385
+ function parseWorldOrbit(source) {
386
+ const lines = source.split(/\r?\n/);
387
+ const objects = [];
388
+ let currentObject = null;
389
+ let inInfoBlock = false;
390
+ let infoIndent = null;
391
+ for (let index = 0; index < lines.length; index++) {
392
+ const rawLine = lines[index];
393
+ const lineNumber = index + 1;
394
+ if (!rawLine.trim()) {
395
+ continue;
396
+ }
397
+ const indent = getIndent(rawLine);
398
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
399
+ line: lineNumber,
400
+ columnOffset: indent
401
+ });
402
+ if (tokens.length === 0) {
403
+ continue;
404
+ }
405
+ if (indent === 0) {
406
+ inInfoBlock = false;
407
+ infoIndent = null;
408
+ const objectNode = parseObjectHeader(tokens, lineNumber);
409
+ objects.push(objectNode);
410
+ currentObject = objectNode;
411
+ continue;
412
+ }
413
+ if (!currentObject) {
414
+ throw new WorldOrbitError("Indented line without parent object", lineNumber, indent + 1);
415
+ }
416
+ if (tokens.length === 1 && tokens[0].value === "info") {
417
+ inInfoBlock = true;
418
+ infoIndent = indent;
419
+ continue;
420
+ }
421
+ if (inInfoBlock && indent <= (infoIndent ?? 0)) {
422
+ inInfoBlock = false;
423
+ }
424
+ if (inInfoBlock) {
425
+ currentObject.infoEntries.push(parseInfoEntry(tokens, lineNumber));
426
+ } else {
427
+ currentObject.blockFields.push(parseField(tokens, lineNumber));
428
+ }
429
+ }
430
+ return {
431
+ type: "document",
432
+ objects
433
+ };
434
+ }
435
+ function parseObjectHeader(tokens, line) {
436
+ if (tokens.length < 2) {
437
+ throw new WorldOrbitError("Invalid object declaration", line, tokens[0]?.column ?? 1);
438
+ }
439
+ const [objectTypeToken, nameToken, ...rest] = tokens;
440
+ if (!WORLDORBIT_OBJECT_TYPES.has(objectTypeToken.value)) {
441
+ throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
442
+ }
443
+ return {
444
+ type: "object",
445
+ objectType: objectTypeToken.value,
446
+ name: nameToken.value,
447
+ inlineFields: parseInlineFields(rest, line),
448
+ blockFields: [],
449
+ infoEntries: [],
450
+ location: { line, column: objectTypeToken.column }
451
+ };
452
+ }
453
+ function parseInlineFields(tokens, line) {
454
+ const fields = [];
455
+ let index = 0;
456
+ while (index < tokens.length) {
457
+ const keyToken = tokens[index];
458
+ const schema = getFieldSchema(keyToken.value);
459
+ if (!schema) {
460
+ throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
461
+ }
462
+ index++;
463
+ const valueTokens = [];
464
+ if (schema.arity === "multiple") {
465
+ while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
466
+ valueTokens.push(tokens[index]);
467
+ index++;
468
+ }
469
+ } else {
470
+ const nextToken = tokens[index];
471
+ if (nextToken) {
472
+ valueTokens.push(nextToken);
473
+ index++;
474
+ }
475
+ }
476
+ if (valueTokens.length === 0) {
477
+ throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
478
+ }
479
+ fields.push({
480
+ type: "field",
481
+ key: keyToken.value,
482
+ values: valueTokens.map((token) => token.value),
483
+ location: { line, column: keyToken.column }
484
+ });
485
+ }
486
+ return fields;
487
+ }
488
+ function parseField(tokens, line) {
489
+ if (tokens.length < 2) {
490
+ throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
491
+ }
492
+ if (!getFieldSchema(tokens[0].value)) {
493
+ throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
494
+ }
495
+ return {
496
+ type: "field",
497
+ key: tokens[0].value,
498
+ values: tokens.slice(1).map((token) => token.value),
499
+ location: { line, column: tokens[0].column }
500
+ };
501
+ }
502
+ function parseInfoEntry(tokens, line) {
503
+ if (tokens.length < 2) {
504
+ throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
505
+ }
506
+ return {
507
+ type: "info-entry",
508
+ key: tokens[0].value,
509
+ value: tokens.slice(1).map((token) => token.value).join(" "),
510
+ location: { line, column: tokens[0].column }
511
+ };
512
+ }
513
+
514
+ // packages/core/dist/normalize.js
515
+ var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
516
+ var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
517
+ ["true", true],
518
+ ["false", false],
519
+ ["yes", true],
520
+ ["no", false]
521
+ ]);
522
+ var URL_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/;
523
+ function normalizeDocument(ast) {
524
+ let system = null;
525
+ const objects = [];
526
+ for (const node of ast.objects) {
527
+ const normalized = normalizeObject(node);
528
+ if (node.objectType === "system") {
529
+ if (system) {
530
+ throw WorldOrbitError.fromLocation("Only one system object is allowed", node.location);
531
+ }
532
+ system = normalized;
533
+ } else {
534
+ objects.push(normalized);
535
+ }
536
+ }
537
+ return {
538
+ format: "worldorbit",
539
+ version: "1.0",
540
+ system,
541
+ objects
542
+ };
543
+ }
544
+ function normalizeObject(node) {
545
+ const mergedFields = [...node.inlineFields, ...node.blockFields];
546
+ validateFieldCompatibility(node.objectType, mergedFields);
547
+ const fieldMap = collectFields(mergedFields);
548
+ const placement = extractPlacement(node.objectType, fieldMap);
549
+ const properties = normalizeProperties(fieldMap);
550
+ const info = normalizeInfo(node.infoEntries);
551
+ if (node.objectType === "system") {
552
+ return {
553
+ type: "system",
554
+ id: node.name,
555
+ properties,
556
+ info
557
+ };
558
+ }
559
+ return {
560
+ type: node.objectType,
561
+ id: node.name,
562
+ properties,
563
+ placement,
564
+ info
565
+ };
566
+ }
567
+ function validateFieldCompatibility(objectType, fields) {
568
+ for (const field of fields) {
569
+ const schema = getFieldSchema(field.key);
570
+ if (!schema) {
571
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
572
+ }
573
+ if (!supportsObjectType(schema, objectType)) {
574
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" is not valid on "${objectType}"`, field.location);
575
+ }
576
+ if (schema.arity === "single" && field.values.length !== 1) {
577
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
578
+ }
579
+ }
580
+ }
581
+ function collectFields(fields) {
582
+ const map = /* @__PURE__ */ new Map();
583
+ for (const field of fields) {
584
+ if (map.has(field.key)) {
585
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
586
+ }
587
+ map.set(field.key, field);
588
+ }
589
+ return map;
590
+ }
591
+ function extractPlacement(objectType, fieldMap) {
592
+ const hasOrbit = fieldMap.has("orbit");
593
+ const hasAt = fieldMap.has("at");
594
+ const hasSurface = fieldMap.has("surface");
595
+ const hasFree = fieldMap.has("free");
596
+ const count = [hasOrbit, hasAt, hasSurface, hasFree].filter(Boolean).length;
597
+ if (count > 1) {
598
+ const conflictingField = fieldMap.get("orbit") ?? fieldMap.get("at") ?? fieldMap.get("surface") ?? fieldMap.get("free");
599
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
600
+ }
601
+ if (objectType === "system" && count > 0) {
602
+ throw WorldOrbitError.fromLocation("System objects cannot declare placement", [...fieldMap.values()][0]?.location);
603
+ }
604
+ if (hasOrbit) {
605
+ return {
606
+ mode: "orbit",
607
+ target: singleValue(fieldMap, "orbit"),
608
+ distance: parseOptionalUnitValue(fieldMap, "distance"),
609
+ semiMajor: parseOptionalUnitValue(fieldMap, "semiMajor"),
610
+ eccentricity: parseOptionalNumber(fieldMap, "eccentricity"),
611
+ period: parseOptionalUnitValue(fieldMap, "period"),
612
+ angle: parseOptionalUnitValue(fieldMap, "angle"),
613
+ inclination: parseOptionalUnitValue(fieldMap, "inclination"),
614
+ phase: parseOptionalUnitValue(fieldMap, "phase")
615
+ };
616
+ }
617
+ if (hasAt) {
618
+ const field = getField(fieldMap, "at");
619
+ const target = singleValue(fieldMap, "at");
620
+ return {
621
+ mode: "at",
622
+ target,
623
+ reference: parseAtReference(target, field.location)
624
+ };
625
+ }
626
+ if (hasSurface) {
627
+ return {
628
+ mode: "surface",
629
+ target: singleValue(fieldMap, "surface")
630
+ };
631
+ }
632
+ if (hasFree) {
633
+ const raw = singleValue(fieldMap, "free");
634
+ const distance = tryParseUnitValue(raw);
635
+ return {
636
+ mode: "free",
637
+ distance: distance ?? void 0,
638
+ descriptor: distance ? void 0 : raw
639
+ };
640
+ }
641
+ return null;
642
+ }
643
+ function normalizeProperties(fieldMap) {
644
+ const result = {};
645
+ for (const [key, field] of fieldMap.entries()) {
646
+ const schema = getFieldSchema(key);
647
+ if (!schema || schema.placement) {
648
+ continue;
649
+ }
650
+ switch (schema.kind) {
651
+ case "list":
652
+ result[key] = field.values;
653
+ break;
654
+ case "boolean":
655
+ result[key] = parseBoolean(field);
656
+ break;
657
+ case "number":
658
+ result[key] = parseNumber(singleFieldValue(field), key, field.location);
659
+ break;
660
+ case "unit":
661
+ result[key] = parseUnitValue(singleFieldValue(field), field.location, key);
662
+ break;
663
+ case "string":
664
+ result[key] = normalizeStringValue(key, field);
665
+ break;
666
+ }
667
+ }
668
+ return result;
669
+ }
670
+ function normalizeStringValue(key, field) {
671
+ const value = field.values.join(" ").trim();
672
+ if (key === "image") {
673
+ validateImageSource(value, field.location);
674
+ }
675
+ return value;
676
+ }
677
+ function validateImageSource(value, location) {
678
+ if (!value) {
679
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
680
+ }
681
+ if (value.startsWith("//")) {
682
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
683
+ }
684
+ const schemeMatch = value.match(URL_SCHEME_PATTERN);
685
+ if (!schemeMatch) {
686
+ return;
687
+ }
688
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
689
+ if (scheme !== "http" && scheme !== "https") {
690
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
691
+ }
692
+ }
693
+ function normalizeInfo(entries) {
694
+ const info = {};
695
+ for (const entry of entries) {
696
+ if (entry.key in info) {
697
+ throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
698
+ }
699
+ info[entry.key] = entry.value;
700
+ }
701
+ return info;
702
+ }
703
+ function parseAtReference(target, location) {
704
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
705
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
706
+ }
707
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
708
+ if (pairedMatch) {
709
+ return {
710
+ kind: "lagrange",
711
+ primary: pairedMatch[1],
712
+ secondary: pairedMatch[2],
713
+ point: pairedMatch[3]
714
+ };
715
+ }
716
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
717
+ if (simpleMatch) {
718
+ return {
719
+ kind: "lagrange",
720
+ primary: simpleMatch[1],
721
+ secondary: null,
722
+ point: simpleMatch[2]
723
+ };
724
+ }
725
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
726
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
727
+ }
728
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
729
+ if (anchorMatch) {
730
+ return {
731
+ kind: "anchor",
732
+ objectId: anchorMatch[1],
733
+ anchor: anchorMatch[2]
734
+ };
735
+ }
736
+ return {
737
+ kind: "named",
738
+ name: target
739
+ };
740
+ }
741
+ function parseUnitValue(input, location, fieldKey) {
742
+ const match = input.match(UNIT_PATTERN);
743
+ if (!match) {
744
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
745
+ }
746
+ const unitValue = {
747
+ value: Number(match[1]),
748
+ unit: match[2] ?? null
749
+ };
750
+ if (fieldKey) {
751
+ const schema = getFieldSchema(fieldKey);
752
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
753
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
754
+ }
755
+ }
756
+ return unitValue;
757
+ }
758
+ function tryParseUnitValue(input) {
759
+ const match = input.match(UNIT_PATTERN);
760
+ if (!match) {
761
+ return null;
762
+ }
763
+ return {
764
+ value: Number(match[1]),
765
+ unit: match[2] ?? null
766
+ };
767
+ }
768
+ function parseOptionalUnitValue(fieldMap, key) {
769
+ if (!fieldMap.has(key)) {
770
+ return void 0;
771
+ }
772
+ const field = getField(fieldMap, key);
773
+ return parseUnitValue(singleFieldValue(field), field.location, key);
774
+ }
775
+ function parseOptionalNumber(fieldMap, key) {
776
+ if (!fieldMap.has(key)) {
777
+ return void 0;
778
+ }
779
+ const field = getField(fieldMap, key);
780
+ return parseNumber(singleFieldValue(field), key, field.location);
781
+ }
782
+ function parseNumber(input, key, location) {
783
+ const value = Number(input);
784
+ if (!Number.isFinite(value)) {
785
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
786
+ }
787
+ return value;
788
+ }
789
+ function parseBoolean(field) {
790
+ const rawValue = singleFieldValue(field).toLowerCase();
791
+ const parsed = BOOLEAN_VALUES.get(rawValue);
792
+ if (parsed === void 0) {
793
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${rawValue}" for "${field.key}"`, field.location);
794
+ }
795
+ return parsed;
796
+ }
797
+ function getField(fieldMap, key) {
798
+ const field = fieldMap.get(key);
799
+ if (!field) {
800
+ throw new WorldOrbitError(`Missing value for key "${key}"`);
801
+ }
802
+ return field;
803
+ }
804
+ function singleValue(fieldMap, key) {
805
+ return singleFieldValue(getField(fieldMap, key));
806
+ }
807
+ function singleFieldValue(field) {
808
+ if (field.values.length !== 1) {
809
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
810
+ }
811
+ return field.values[0];
812
+ }
813
+
814
+ // packages/core/dist/validate.js
815
+ var SURFACE_TARGET_TYPES = /* @__PURE__ */ new Set([
816
+ "star",
817
+ "planet",
818
+ "moon",
819
+ "asteroid",
820
+ "comet"
821
+ ]);
822
+ function validateDocument(doc) {
823
+ const knownIds = /* @__PURE__ */ new Set();
824
+ const objectMap = /* @__PURE__ */ new Map();
825
+ for (const obj of doc.objects) {
826
+ if (knownIds.has(obj.id)) {
827
+ throw new WorldOrbitError(`Duplicate object id "${obj.id}"`);
828
+ }
829
+ knownIds.add(obj.id);
830
+ objectMap.set(obj.id, obj);
831
+ }
832
+ for (const obj of doc.objects) {
833
+ if (!obj.placement) {
834
+ continue;
835
+ }
836
+ if (obj.placement.mode === "orbit" || obj.placement.mode === "surface") {
837
+ if (!knownIds.has(obj.placement.target)) {
838
+ throw new WorldOrbitError(`Unknown placement target "${obj.placement.target}" on "${obj.id}"`);
839
+ }
840
+ }
841
+ if (obj.placement.mode === "surface") {
842
+ const target = objectMap.get(obj.placement.target);
843
+ if (!target || !SURFACE_TARGET_TYPES.has(target.type)) {
844
+ throw new WorldOrbitError(`Surface target "${obj.placement.target}" on "${obj.id}" is not surface-capable`);
845
+ }
846
+ }
847
+ if (obj.placement.mode === "at") {
848
+ if (obj.placement.reference.kind === "lagrange") {
849
+ validateLagrangeReference(obj, obj.placement.reference, knownIds);
850
+ }
851
+ if (obj.placement.reference.kind === "anchor") {
852
+ validateAnchorReference(obj, obj.placement.reference, knownIds);
853
+ }
854
+ }
855
+ }
856
+ }
857
+ function validateLagrangeReference(obj, reference, knownIds) {
858
+ if (!knownIds.has(reference.primary)) {
859
+ throw new WorldOrbitError(`Unknown Lagrange reference "${reference.primary}" on "${obj.id}"`);
860
+ }
861
+ if (reference.secondary && !knownIds.has(reference.secondary)) {
862
+ throw new WorldOrbitError(`Unknown Lagrange reference "${reference.secondary}" on "${obj.id}"`);
863
+ }
864
+ }
865
+ function validateAnchorReference(obj, reference, knownIds) {
866
+ if (!knownIds.has(reference.objectId)) {
867
+ throw new WorldOrbitError(`Unknown anchor target "${reference.objectId}" on "${obj.id}"`);
868
+ }
869
+ }
870
+
871
+ // packages/core/dist/diagnostics.js
872
+ function diagnosticFromError(error, source, code = `${source}.failed`) {
873
+ if (error instanceof WorldOrbitError) {
874
+ return {
875
+ code,
876
+ severity: "error",
877
+ source,
878
+ message: error.message,
879
+ line: error.line,
880
+ column: error.column
881
+ };
882
+ }
883
+ if (error instanceof Error) {
884
+ return {
885
+ code,
886
+ severity: "error",
887
+ source,
888
+ message: error.message
889
+ };
890
+ }
891
+ return {
892
+ code,
893
+ severity: "error",
894
+ source,
895
+ message: String(error)
896
+ };
897
+ }
898
+
899
+ // packages/core/dist/scene.js
900
+ var AU_IN_KM = 1495978707e-1;
901
+ var EARTH_RADIUS_IN_KM = 6371;
902
+ var SOLAR_RADIUS_IN_KM = 695700;
903
+ var ISO_FLATTENING = 0.68;
904
+ var MIN_ISO_MINOR_SCALE = 0.2;
905
+ var ARC_SAMPLE_COUNT = 28;
906
+ function renderDocumentToScene(document2, options = {}) {
907
+ const frame = resolveSceneFrame(options);
908
+ const width = frame.width;
909
+ const height = frame.height;
910
+ const padding = frame.padding;
911
+ const layoutPreset = resolveLayoutPreset(document2);
912
+ const projection = resolveProjection(document2, options.projection);
913
+ const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
914
+ const spacingFactor = layoutPresetSpacing(layoutPreset);
915
+ const systemId = document2.system?.id ?? null;
916
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
917
+ const relationships = buildSceneRelationships(document2.objects, objectMap);
918
+ const positions = /* @__PURE__ */ new Map();
919
+ const orbitDrafts = [];
920
+ const leaderDrafts = [];
921
+ const rootObjects = [];
922
+ const freeObjects = [];
923
+ const atObjects = [];
924
+ const surfaceChildren = /* @__PURE__ */ new Map();
925
+ const orbitChildren = /* @__PURE__ */ new Map();
926
+ for (const object of document2.objects) {
927
+ const placement = object.placement;
928
+ if (!placement) {
929
+ rootObjects.push(object);
930
+ continue;
931
+ }
932
+ if (placement.mode === "orbit") {
933
+ pushGrouped(orbitChildren, placement.target, object);
934
+ continue;
935
+ }
936
+ if (placement.mode === "surface") {
937
+ pushGrouped(surfaceChildren, placement.target, object);
938
+ continue;
939
+ }
940
+ if (placement.mode === "at") {
941
+ atObjects.push(object);
942
+ continue;
943
+ }
944
+ freeObjects.push(object);
945
+ }
946
+ const centerX = freeObjects.length > 0 ? width * 0.42 : width / 2;
947
+ const centerY = height / 2;
948
+ const context = {
949
+ orbitChildren,
950
+ surfaceChildren,
951
+ objectMap,
952
+ spacingFactor,
953
+ projection,
954
+ scaleModel
955
+ };
956
+ const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
957
+ if (primaryRoot) {
958
+ placeObject(primaryRoot, centerX, centerY, 0, positions, orbitDrafts, leaderDrafts, context);
959
+ }
960
+ const secondaryRoots = rootObjects.filter((object) => object.id !== primaryRoot?.id);
961
+ if (secondaryRoots.length > 0) {
962
+ const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
963
+ secondaryRoots.forEach((object, index) => {
964
+ const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
965
+ const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
966
+ placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
967
+ });
968
+ }
969
+ freeObjects.forEach((object, index) => {
970
+ const x = width - padding - 140 - freePlacementOffsetPx(object.placement?.mode === "free" ? object.placement.distance : void 0, scaleModel);
971
+ const rowStep = Math.max(76, (height - padding * 2 - 180) / Math.max(1, freeObjects.length) * spacingFactor) * scaleModel.freePlacementMultiplier;
972
+ const y = padding + 92 + index * rowStep;
973
+ positions.set(object.id, {
974
+ object,
975
+ x,
976
+ y,
977
+ radius: visualRadiusFor(object, 0, scaleModel),
978
+ sortKey: computeSortKey(x, y, 0)
979
+ });
980
+ leaderDrafts.push({
981
+ object,
982
+ groupId: relationships.groupIds.get(object.id) ?? null,
983
+ x1: x - 60,
984
+ y1: y,
985
+ x2: x - 18,
986
+ y2: y,
987
+ mode: "free"
988
+ });
989
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, 1);
990
+ });
991
+ atObjects.forEach((object, index) => {
992
+ if (positions.has(object.id) || !object.placement || object.placement.mode !== "at") {
993
+ return;
994
+ }
995
+ const resolved = resolveAtPosition(object.placement.reference, positions, objectMap, index, atObjects.length, width, height, padding, context);
996
+ positions.set(object.id, {
997
+ object,
998
+ x: resolved.x,
999
+ y: resolved.y,
1000
+ radius: visualRadiusFor(object, 2, scaleModel),
1001
+ sortKey: computeSortKey(resolved.x, resolved.y, 2),
1002
+ anchorX: resolved.anchorX,
1003
+ anchorY: resolved.anchorY
1004
+ });
1005
+ if (resolved.anchorX !== void 0 && resolved.anchorY !== void 0) {
1006
+ leaderDrafts.push({
1007
+ object,
1008
+ groupId: relationships.groupIds.get(object.id) ?? null,
1009
+ x1: resolved.anchorX,
1010
+ y1: resolved.anchorY,
1011
+ x2: resolved.x,
1012
+ y2: resolved.y,
1013
+ mode: "at"
1014
+ });
1015
+ }
1016
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, 2);
1017
+ });
1018
+ const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1019
+ const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1020
+ const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1021
+ const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1022
+ const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1023
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1024
+ const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1025
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1026
+ return {
1027
+ width,
1028
+ height,
1029
+ padding,
1030
+ renderPreset: frame.preset,
1031
+ projection,
1032
+ scaleModel,
1033
+ title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1034
+ subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1035
+ systemId,
1036
+ viewMode: projection,
1037
+ layoutPreset,
1038
+ metadata: {
1039
+ format: document2.format,
1040
+ version: document2.version,
1041
+ view: projection,
1042
+ scale: String(document2.system?.properties.scale ?? layoutPreset),
1043
+ units: String(document2.system?.properties.units ?? "mixed"),
1044
+ preset: frame.preset ?? "custom"
1045
+ },
1046
+ contentBounds,
1047
+ layers,
1048
+ groups,
1049
+ viewpoints,
1050
+ objects,
1051
+ orbitVisuals,
1052
+ leaders,
1053
+ labels
1054
+ };
1055
+ }
1056
+ function resolveLayoutPreset(document2) {
1057
+ const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1058
+ switch (rawScale) {
1059
+ case "compressed":
1060
+ case "compact":
1061
+ return "compact";
1062
+ case "expanded":
1063
+ case "presentation":
1064
+ return "presentation";
1065
+ default:
1066
+ return "balanced";
1067
+ }
1068
+ }
1069
+ function resolveSceneFrame(options) {
1070
+ const defaults = scenePresetDefaults(options.preset);
1071
+ return {
1072
+ width: options.width ?? defaults.width,
1073
+ height: options.height ?? defaults.height,
1074
+ padding: options.padding ?? defaults.padding,
1075
+ preset: options.preset ?? null
1076
+ };
1077
+ }
1078
+ function scenePresetDefaults(preset) {
1079
+ switch (preset) {
1080
+ case "presentation":
1081
+ return { width: 1440, height: 900, padding: 88 };
1082
+ case "atlas-card":
1083
+ return { width: 960, height: 560, padding: 56 };
1084
+ case "markdown":
1085
+ return { width: 920, height: 540, padding: 48 };
1086
+ case "diagram":
1087
+ default:
1088
+ return { width: 1200, height: 780, padding: 72 };
1089
+ }
1090
+ }
1091
+ function resolveProjection(document2, projection) {
1092
+ if (projection === "topdown" || projection === "isometric") {
1093
+ return projection;
1094
+ }
1095
+ return String(document2.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1096
+ }
1097
+ function resolveScaleModel(layoutPreset, overrides) {
1098
+ const defaults = defaultScaleModel(layoutPreset);
1099
+ return {
1100
+ ...defaults,
1101
+ ...overrides
1102
+ };
1103
+ }
1104
+ function defaultScaleModel(layoutPreset) {
1105
+ switch (layoutPreset) {
1106
+ case "compact":
1107
+ return {
1108
+ orbitDistanceMultiplier: 0.84,
1109
+ bodyRadiusMultiplier: 0.92,
1110
+ labelMultiplier: 0.9,
1111
+ freePlacementMultiplier: 0.9,
1112
+ ringThicknessMultiplier: 0.92,
1113
+ minBodyRadius: 4,
1114
+ maxBodyRadius: 36
1115
+ };
1116
+ case "presentation":
1117
+ return {
1118
+ orbitDistanceMultiplier: 1.2,
1119
+ bodyRadiusMultiplier: 1.18,
1120
+ labelMultiplier: 1.08,
1121
+ freePlacementMultiplier: 1.05,
1122
+ ringThicknessMultiplier: 1.16,
1123
+ minBodyRadius: 5,
1124
+ maxBodyRadius: 48
1125
+ };
1126
+ default:
1127
+ return {
1128
+ orbitDistanceMultiplier: 1,
1129
+ bodyRadiusMultiplier: 1,
1130
+ labelMultiplier: 1,
1131
+ freePlacementMultiplier: 1,
1132
+ ringThicknessMultiplier: 1,
1133
+ minBodyRadius: 4,
1134
+ maxBodyRadius: 40
1135
+ };
1136
+ }
1137
+ }
1138
+ function layoutPresetSpacing(layoutPreset) {
1139
+ switch (layoutPreset) {
1140
+ case "compact":
1141
+ return 0.84;
1142
+ case "presentation":
1143
+ return 1.2;
1144
+ default:
1145
+ return 1;
1146
+ }
1147
+ }
1148
+ function createSceneObject(position, scaleModel, relationships) {
1149
+ const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1150
+ return {
1151
+ renderId: createRenderId(object.id),
1152
+ objectId: object.id,
1153
+ object,
1154
+ parentId: relationships.parentIds.get(object.id) ?? null,
1155
+ ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1156
+ childIds: relationships.childIds.get(object.id) ?? [],
1157
+ groupId: relationships.groupIds.get(object.id) ?? null,
1158
+ x,
1159
+ y,
1160
+ radius,
1161
+ visualRadius: visualExtentForObject(object, radius, scaleModel),
1162
+ sortKey,
1163
+ anchorX,
1164
+ anchorY,
1165
+ label: object.id,
1166
+ secondaryLabel: object.type === "structure" ? String(object.properties.kind ?? object.type) : object.type,
1167
+ fillColor: customColorFor(object.properties.color),
1168
+ imageHref: typeof object.properties.image === "string" && object.properties.image.trim() ? object.properties.image : void 0,
1169
+ hidden: object.properties.hidden === true
1170
+ };
1171
+ }
1172
+ function createOrbitVisual(draft, groupId) {
1173
+ return {
1174
+ renderId: `${createRenderId(draft.object.id)}-orbit`,
1175
+ objectId: draft.object.id,
1176
+ object: draft.object,
1177
+ parentId: draft.parentId,
1178
+ groupId,
1179
+ kind: draft.kind,
1180
+ cx: draft.cx,
1181
+ cy: draft.cy,
1182
+ radius: draft.radius,
1183
+ rx: draft.rx,
1184
+ ry: draft.ry,
1185
+ rotationDeg: draft.rotationDeg,
1186
+ band: draft.band,
1187
+ bandThickness: draft.bandThickness,
1188
+ frontArcPath: draft.frontArcPath,
1189
+ backArcPath: draft.backArcPath,
1190
+ hidden: draft.object.properties.hidden === true
1191
+ };
1192
+ }
1193
+ function createLeaderLine(draft) {
1194
+ return {
1195
+ renderId: `${createRenderId(draft.object.id)}-leader-${draft.mode}`,
1196
+ objectId: draft.object.id,
1197
+ object: draft.object,
1198
+ groupId: draft.groupId,
1199
+ x1: draft.x1,
1200
+ y1: draft.y1,
1201
+ x2: draft.x2,
1202
+ y2: draft.y2,
1203
+ mode: draft.mode,
1204
+ hidden: draft.object.properties.hidden === true
1205
+ };
1206
+ }
1207
+ function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1208
+ const labels = [];
1209
+ const occupied = [];
1210
+ const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1211
+ for (const object of visibleObjects) {
1212
+ const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1213
+ const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1214
+ let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1215
+ let secondaryY = labelY + direction * (16 * labelMultiplier);
1216
+ let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1217
+ let attempts = 0;
1218
+ while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1219
+ labelY += direction * 14 * labelMultiplier;
1220
+ secondaryY += direction * 14 * labelMultiplier;
1221
+ bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1222
+ attempts += 1;
1223
+ }
1224
+ occupied.push(bounds);
1225
+ labels.push({
1226
+ renderId: `${object.renderId}-label`,
1227
+ objectId: object.objectId,
1228
+ object: object.object,
1229
+ groupId: object.groupId,
1230
+ label: object.label,
1231
+ secondaryLabel: object.secondaryLabel,
1232
+ x: object.x,
1233
+ y: labelY,
1234
+ secondaryY,
1235
+ textAnchor: "middle",
1236
+ direction: direction < 0 ? "above" : "below",
1237
+ hidden: object.hidden
1238
+ });
1239
+ }
1240
+ return labels;
1241
+ }
1242
+ function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1243
+ const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1244
+ const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1245
+ return [
1246
+ { id: "background", renderIds: ["wo-bg", "wo-bg-glow", "wo-grid"] },
1247
+ {
1248
+ id: "guides",
1249
+ renderIds: leaders.filter((leader) => !leader.hidden).map((leader) => leader.renderId)
1250
+ },
1251
+ { id: "orbits-back", renderIds: backOrbitIds },
1252
+ { id: "orbits-front", renderIds: frontOrbitIds },
1253
+ {
1254
+ id: "objects",
1255
+ renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
1256
+ },
1257
+ {
1258
+ id: "labels",
1259
+ renderIds: labels.filter((label) => !label.hidden).map((label) => label.renderId)
1260
+ },
1261
+ { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1262
+ ];
1263
+ }
1264
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1265
+ const groups = /* @__PURE__ */ new Map();
1266
+ const ensureGroup = (groupId) => {
1267
+ if (!groupId) {
1268
+ return null;
1269
+ }
1270
+ const existing = groups.get(groupId);
1271
+ if (existing) {
1272
+ return existing;
1273
+ }
1274
+ const rootObjectId = relationships.groupRoots.get(groupId) ?? null;
1275
+ const created = {
1276
+ renderId: groupId,
1277
+ rootObjectId,
1278
+ label: rootObjectId ?? groupId,
1279
+ objectIds: [],
1280
+ orbitIds: [],
1281
+ labelIds: [],
1282
+ leaderIds: [],
1283
+ contentBounds: createBounds(0, 0, 0, 0)
1284
+ };
1285
+ groups.set(groupId, created);
1286
+ return created;
1287
+ };
1288
+ for (const object of objects) {
1289
+ const group = ensureGroup(object.groupId);
1290
+ if (group && !object.hidden) {
1291
+ group.objectIds.push(object.objectId);
1292
+ }
1293
+ }
1294
+ for (const orbit of orbitVisuals) {
1295
+ const group = ensureGroup(orbit.groupId);
1296
+ if (group && !orbit.hidden) {
1297
+ group.orbitIds.push(orbit.objectId);
1298
+ }
1299
+ }
1300
+ for (const leader of leaders) {
1301
+ const group = ensureGroup(leader.groupId);
1302
+ if (group && !leader.hidden) {
1303
+ group.leaderIds.push(leader.objectId);
1304
+ }
1305
+ }
1306
+ for (const label of labels) {
1307
+ const group = ensureGroup(label.groupId);
1308
+ if (group && !label.hidden) {
1309
+ group.labelIds.push(label.objectId);
1310
+ }
1311
+ }
1312
+ for (const group of groups.values()) {
1313
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1314
+ }
1315
+ return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1316
+ }
1317
+ function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1318
+ const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1319
+ const drafts = /* @__PURE__ */ new Map();
1320
+ for (const [key, value] of Object.entries(document2.system?.info ?? {})) {
1321
+ if (!key.startsWith("viewpoint.")) {
1322
+ continue;
1323
+ }
1324
+ const [prefix, rawId, ...fieldParts] = key.split(".");
1325
+ if (prefix !== "viewpoint" || !rawId || fieldParts.length === 0) {
1326
+ continue;
1327
+ }
1328
+ const id = normalizeViewpointId(rawId);
1329
+ if (!id) {
1330
+ continue;
1331
+ }
1332
+ const field = fieldParts.join(".").toLowerCase();
1333
+ const draft = drafts.get(id) ?? { id };
1334
+ applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1335
+ drafts.set(id, draft);
1336
+ }
1337
+ const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
1338
+ const overviewIndex = viewpoints.findIndex((viewpoint) => viewpoint.id === generatedOverview.id);
1339
+ if (overviewIndex >= 0) {
1340
+ viewpoints.splice(overviewIndex, 1, {
1341
+ ...generatedOverview,
1342
+ ...viewpoints[overviewIndex],
1343
+ layers: {
1344
+ ...generatedOverview.layers,
1345
+ ...viewpoints[overviewIndex].layers
1346
+ },
1347
+ filter: viewpoints[overviewIndex].filter ?? generatedOverview.filter,
1348
+ generated: false
1349
+ });
1350
+ } else {
1351
+ viewpoints.unshift(generatedOverview);
1352
+ }
1353
+ return viewpoints.sort((left, right) => {
1354
+ if (left.id === "overview")
1355
+ return -1;
1356
+ if (right.id === "overview")
1357
+ return 1;
1358
+ return left.label.localeCompare(right.label);
1359
+ });
1360
+ }
1361
+ function createGeneratedOverviewViewpoint(document2, projection, preset) {
1362
+ const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1363
+ return {
1364
+ id: "overview",
1365
+ label,
1366
+ summary: "Fit the whole system with the current atlas defaults.",
1367
+ objectId: null,
1368
+ selectedObjectId: null,
1369
+ projection,
1370
+ preset,
1371
+ rotationDeg: 0,
1372
+ scale: null,
1373
+ layers: {},
1374
+ filter: null,
1375
+ generated: true
1376
+ };
1377
+ }
1378
+ function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1379
+ const normalizedValue = value.trim();
1380
+ switch (field) {
1381
+ case "label":
1382
+ case "title":
1383
+ if (normalizedValue) {
1384
+ draft.label = normalizedValue;
1385
+ }
1386
+ return;
1387
+ case "summary":
1388
+ case "description":
1389
+ if (normalizedValue) {
1390
+ draft.summary = normalizedValue;
1391
+ }
1392
+ return;
1393
+ case "focus":
1394
+ case "object":
1395
+ if (normalizedValue) {
1396
+ draft.focus = normalizedValue;
1397
+ }
1398
+ return;
1399
+ case "select":
1400
+ case "selection":
1401
+ if (normalizedValue) {
1402
+ draft.select = normalizedValue;
1403
+ }
1404
+ return;
1405
+ case "projection":
1406
+ case "view":
1407
+ draft.projection = parseViewProjection(normalizedValue) ?? projection;
1408
+ return;
1409
+ case "preset":
1410
+ draft.preset = parseRenderPreset(normalizedValue) ?? preset;
1411
+ return;
1412
+ case "rotation":
1413
+ case "angle":
1414
+ draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1415
+ return;
1416
+ case "zoom":
1417
+ case "scale":
1418
+ draft.scale = parsePositiveNumber(normalizedValue);
1419
+ return;
1420
+ case "layers":
1421
+ draft.layers = parseViewpointLayers(normalizedValue);
1422
+ return;
1423
+ case "query":
1424
+ draft.filter = {
1425
+ ...draft.filter ?? createEmptyViewpointFilter(),
1426
+ query: normalizedValue || null
1427
+ };
1428
+ return;
1429
+ case "types":
1430
+ case "objecttypes":
1431
+ draft.filter = {
1432
+ ...draft.filter ?? createEmptyViewpointFilter(),
1433
+ objectTypes: parseViewpointObjectTypes(normalizedValue)
1434
+ };
1435
+ return;
1436
+ case "tags":
1437
+ draft.filter = {
1438
+ ...draft.filter ?? createEmptyViewpointFilter(),
1439
+ tags: splitListValue(normalizedValue)
1440
+ };
1441
+ return;
1442
+ case "groups":
1443
+ draft.filter = {
1444
+ ...draft.filter ?? createEmptyViewpointFilter(),
1445
+ groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1446
+ };
1447
+ return;
1448
+ }
1449
+ }
1450
+ function finalizeViewpointDraft(draft, projection, preset, objectMap) {
1451
+ const objectId = draft.focus && objectMap.has(draft.focus) ? draft.focus : null;
1452
+ const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1453
+ const filter = normalizeViewpointFilter(draft.filter);
1454
+ const label = draft.label?.trim() || humanizeIdentifier(draft.id);
1455
+ return {
1456
+ id: draft.id,
1457
+ label,
1458
+ summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1459
+ objectId,
1460
+ selectedObjectId,
1461
+ projection: draft.projection ?? projection,
1462
+ preset: draft.preset ?? preset,
1463
+ rotationDeg: draft.rotationDeg ?? 0,
1464
+ scale: draft.scale ?? null,
1465
+ layers: draft.layers ?? {},
1466
+ filter,
1467
+ generated: false
1468
+ };
1469
+ }
1470
+ function createEmptyViewpointFilter() {
1471
+ return {
1472
+ query: null,
1473
+ objectTypes: [],
1474
+ tags: [],
1475
+ groupIds: []
1476
+ };
1477
+ }
1478
+ function normalizeViewpointFilter(filter) {
1479
+ if (!filter) {
1480
+ return null;
1481
+ }
1482
+ const normalized = {
1483
+ query: filter.query?.trim() || null,
1484
+ objectTypes: [...new Set(filter.objectTypes)],
1485
+ tags: [...new Set(filter.tags)],
1486
+ groupIds: [...new Set(filter.groupIds)]
1487
+ };
1488
+ return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1489
+ }
1490
+ function parseViewProjection(value) {
1491
+ return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
1492
+ }
1493
+ function parseRenderPreset(value) {
1494
+ const normalized = value.toLowerCase();
1495
+ if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
1496
+ return normalized;
1497
+ }
1498
+ return null;
1499
+ }
1500
+ function parseFiniteNumber(value) {
1501
+ const parsed = Number(value);
1502
+ return Number.isFinite(parsed) ? parsed : null;
1503
+ }
1504
+ function parsePositiveNumber(value) {
1505
+ const parsed = parseFiniteNumber(value);
1506
+ return parsed !== null && parsed > 0 ? parsed : null;
1507
+ }
1508
+ function parseViewpointLayers(value) {
1509
+ const next = {};
1510
+ for (const token of splitListValue(value)) {
1511
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
1512
+ const rawLayer = token.replace(/^[-!]+/, "").toLowerCase();
1513
+ if (rawLayer === "orbits") {
1514
+ next["orbits-back"] = enabled;
1515
+ next["orbits-front"] = enabled;
1516
+ continue;
1517
+ }
1518
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1519
+ next[rawLayer] = enabled;
1520
+ }
1521
+ }
1522
+ return next;
1523
+ }
1524
+ function parseViewpointObjectTypes(value) {
1525
+ return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1526
+ }
1527
+ function parseViewpointGroups(value, relationships, objectMap) {
1528
+ return splitListValue(value).map((entry) => {
1529
+ if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1530
+ return entry;
1531
+ }
1532
+ if (relationships.groupIds.has(entry)) {
1533
+ return relationships.groupIds.get(entry) ?? createGroupId(entry);
1534
+ }
1535
+ return objectMap.has(entry) ? createGroupId(entry) : createGroupId(entry);
1536
+ });
1537
+ }
1538
+ function splitListValue(value) {
1539
+ return value.split(/[\s,]+/).map((entry) => entry.trim()).filter(Boolean);
1540
+ }
1541
+ function normalizeViewpointId(value) {
1542
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
1543
+ }
1544
+ function humanizeIdentifier(value) {
1545
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
1546
+ }
1547
+ function createViewpointSummary(label, objectId, filter) {
1548
+ const parts = [label];
1549
+ if (objectId) {
1550
+ parts.push(`focus ${objectId}`);
1551
+ }
1552
+ if (filter?.objectTypes.length) {
1553
+ parts.push(filter.objectTypes.join("/"));
1554
+ }
1555
+ if (filter?.tags.length) {
1556
+ parts.push(`tags ${filter.tags.join(", ")}`);
1557
+ }
1558
+ if (filter?.query) {
1559
+ parts.push(`query "${filter.query}"`);
1560
+ }
1561
+ return parts.join(" - ");
1562
+ }
1563
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1564
+ let minX = Number.POSITIVE_INFINITY;
1565
+ let minY = Number.POSITIVE_INFINITY;
1566
+ let maxX = Number.NEGATIVE_INFINITY;
1567
+ let maxY = Number.NEGATIVE_INFINITY;
1568
+ const include = (x, y) => {
1569
+ minX = Math.min(minX, x);
1570
+ minY = Math.min(minY, y);
1571
+ maxX = Math.max(maxX, x);
1572
+ maxY = Math.max(maxY, y);
1573
+ };
1574
+ for (const orbit of orbitVisuals) {
1575
+ if (orbit.hidden)
1576
+ continue;
1577
+ includeOrbitBounds(orbit, include);
1578
+ }
1579
+ for (const leader of leaders) {
1580
+ if (leader.hidden)
1581
+ continue;
1582
+ include(leader.x1, leader.y1);
1583
+ include(leader.x2, leader.y2);
1584
+ }
1585
+ for (const object of objects) {
1586
+ if (object.hidden)
1587
+ continue;
1588
+ includeObjectBounds(object, include);
1589
+ }
1590
+ for (const label of labels) {
1591
+ if (label.hidden)
1592
+ continue;
1593
+ includeLabelBounds(label, include);
1594
+ }
1595
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1596
+ return createBounds(0, 0, width, height);
1597
+ }
1598
+ return createBounds(minX, minY, maxX, maxY);
1599
+ }
1600
+ function includeOrbitBounds(orbit, include) {
1601
+ const strokePadding = orbit.bandThickness !== void 0 ? orbit.bandThickness / 2 + 4 : orbit.band ? 10 : 3;
1602
+ if (orbit.kind === "circle" && orbit.radius !== void 0) {
1603
+ include(orbit.cx - orbit.radius - strokePadding, orbit.cy - orbit.radius - strokePadding);
1604
+ include(orbit.cx + orbit.radius + strokePadding, orbit.cy + orbit.radius + strokePadding);
1605
+ return;
1606
+ }
1607
+ const rx = orbit.rx ?? orbit.radius ?? 0;
1608
+ const ry = orbit.ry ?? orbit.radius ?? 0;
1609
+ const points = sampleEllipseArcPoints(orbit.cx, orbit.cy, rx, ry, orbit.rotationDeg, 0, Math.PI * 2, ARC_SAMPLE_COUNT * 2);
1610
+ for (const point of points) {
1611
+ include(point.x - strokePadding, point.y - strokePadding);
1612
+ include(point.x + strokePadding, point.y + strokePadding);
1613
+ }
1614
+ }
1615
+ function createBounds(minX, minY, maxX, maxY) {
1616
+ return {
1617
+ minX,
1618
+ minY,
1619
+ maxX,
1620
+ maxY,
1621
+ width: maxX - minX,
1622
+ height: maxY - minY,
1623
+ centerX: minX + (maxX - minX) / 2,
1624
+ centerY: minY + (maxY - minY) / 2
1625
+ };
1626
+ }
1627
+ function includeObjectBounds(object, include) {
1628
+ include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1629
+ include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1630
+ }
1631
+ function includeLabelBounds(label, include) {
1632
+ const labelScale = 1;
1633
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1634
+ include(label.x - labelHalfWidth, label.y - 18);
1635
+ include(label.x + labelHalfWidth, label.y + 8);
1636
+ include(label.x - labelHalfWidth, label.secondaryY - 14);
1637
+ include(label.x + labelHalfWidth, label.secondaryY + 8);
1638
+ }
1639
+ function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1640
+ if (positions.has(object.id)) {
1641
+ return;
1642
+ }
1643
+ positions.set(object.id, {
1644
+ object,
1645
+ x,
1646
+ y,
1647
+ radius: visualRadiusFor(object, depth, context.scaleModel),
1648
+ sortKey: computeSortKey(x, y, depth)
1649
+ });
1650
+ placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, depth + 1);
1651
+ }
1652
+ function placeOrbitingChildren(object, positions, orbitDrafts, leaderDrafts, context, depth) {
1653
+ const parent = positions.get(object.id);
1654
+ if (!parent) {
1655
+ return;
1656
+ }
1657
+ const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1658
+ const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1659
+ orbiting.forEach((child, index) => {
1660
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
1661
+ orbitDrafts.push({
1662
+ object: child,
1663
+ parentId: object.id,
1664
+ kind: orbitGeometry.kind,
1665
+ cx: orbitGeometry.cx,
1666
+ cy: orbitGeometry.cy,
1667
+ radius: orbitGeometry.radius,
1668
+ rx: orbitGeometry.rx,
1669
+ ry: orbitGeometry.ry,
1670
+ rotationDeg: orbitGeometry.rotationDeg,
1671
+ band: orbitGeometry.band,
1672
+ bandThickness: orbitGeometry.bandThickness,
1673
+ frontArcPath: orbitGeometry.frontArcPath,
1674
+ backArcPath: orbitGeometry.backArcPath
1675
+ });
1676
+ placeObject(child, orbitGeometry.objectX, orbitGeometry.objectY, depth, positions, orbitDrafts, leaderDrafts, context);
1677
+ });
1678
+ const surfaceObjects = [...context.surfaceChildren.get(object.id) ?? []];
1679
+ surfaceObjects.forEach((child, index) => {
1680
+ const angle = angleForIndex(index, surfaceObjects.length, -Math.PI / 3);
1681
+ const leaderDistance = 28 * context.spacingFactor;
1682
+ const anchorOffset = projectPolarOffset(angle, parent.radius, context.projection, context.projection === "isometric" ? 0.9 : 1);
1683
+ const bodyOffset = projectPolarOffset(angle, parent.radius + leaderDistance, context.projection, context.projection === "isometric" ? 0.9 : 1);
1684
+ const anchorX = parent.x + anchorOffset.x;
1685
+ const anchorY = parent.y + anchorOffset.y;
1686
+ const x = parent.x + bodyOffset.x;
1687
+ const y = parent.y + bodyOffset.y;
1688
+ positions.set(child.id, {
1689
+ object: child,
1690
+ x,
1691
+ y,
1692
+ radius: visualRadiusFor(child, depth + 1, context.scaleModel),
1693
+ sortKey: computeSortKey(x, y, depth + 1),
1694
+ anchorX,
1695
+ anchorY
1696
+ });
1697
+ leaderDrafts.push({
1698
+ object: child,
1699
+ groupId: context.objectMap.has(child.id) ? createGroupId(resolveGroupRootObjectId(child, context.objectMap)) : null,
1700
+ x1: anchorX,
1701
+ y1: anchorY,
1702
+ x2: x,
1703
+ y2: y,
1704
+ mode: "surface"
1705
+ });
1706
+ placeOrbitingChildren(child, positions, orbitDrafts, leaderDrafts, context, depth + 1);
1707
+ });
1708
+ }
1709
+ function compareOrbiting(left, right) {
1710
+ const leftMetric = orbitMetric(left);
1711
+ const rightMetric = orbitMetric(right);
1712
+ if (leftMetric !== null && rightMetric !== null && leftMetric !== rightMetric) {
1713
+ return leftMetric - rightMetric;
1714
+ }
1715
+ if (leftMetric !== null && rightMetric === null)
1716
+ return -1;
1717
+ if (leftMetric === null && rightMetric !== null)
1718
+ return 1;
1719
+ return left.id.localeCompare(right.id);
1720
+ }
1721
+ function computeOrbitMetricContext(objects, parentRadius, spacingFactor, scaleModel) {
1722
+ const metrics = objects.map((object) => orbitMetric(object));
1723
+ const presentMetrics = metrics.filter((value) => value !== null);
1724
+ const innerPx = parentRadius + 56 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1725
+ const stepPx = (objects.length > 2 ? 54 : 64) * spacingFactor * scaleModel.orbitDistanceMultiplier;
1726
+ if (presentMetrics.length === 0) {
1727
+ return {
1728
+ metrics,
1729
+ minMetric: 0,
1730
+ maxMetric: 0,
1731
+ metricSpread: 0,
1732
+ innerPx,
1733
+ stepPx,
1734
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1735
+ };
1736
+ }
1737
+ const minMetric = Math.min(...presentMetrics);
1738
+ const maxMetric = Math.max(...presentMetrics);
1739
+ const metricSpread = maxMetric - minMetric;
1740
+ return {
1741
+ metrics,
1742
+ minMetric,
1743
+ maxMetric,
1744
+ metricSpread,
1745
+ innerPx,
1746
+ stepPx,
1747
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1748
+ };
1749
+ }
1750
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
1751
+ const placement = object.placement;
1752
+ const band = object.type === "belt" || object.type === "ring";
1753
+ if (!placement || placement.mode !== "orbit") {
1754
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
1755
+ return {
1756
+ kind: "circle",
1757
+ cx: parent.x,
1758
+ cy: parent.y,
1759
+ radius: fallbackRadius,
1760
+ rotationDeg: 0,
1761
+ band,
1762
+ bandThickness: band ? 12 * context.scaleModel.ringThicknessMultiplier : void 0,
1763
+ objectX: parent.x,
1764
+ objectY: parent.y - fallbackRadius
1765
+ };
1766
+ }
1767
+ const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1768
+ const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
1769
+ const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1770
+ const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1771
+ const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
1772
+ const semiMinor = Math.max(baseMinor * inclinationScale, semiMajor * 0.14);
1773
+ const rotationDeg = unitValueToDegrees(placement.angle) ?? 0;
1774
+ const focusOffset = semiMajor * eccentricity;
1775
+ const centerOffset = rotateOffset(-focusOffset, 0, rotationDeg);
1776
+ const cx = parent.x + centerOffset.x;
1777
+ const cy = parent.y + centerOffset.y;
1778
+ const phase = resolveOrbitPhase(placement.phase, index, count);
1779
+ const objectPoint = ellipsePoint(cx, cy, semiMajor, semiMinor, rotationDeg, phase);
1780
+ const useCircle = context.projection === "topdown" && eccentricity <= 1e-4 && Math.abs(rotationDeg) <= 1e-4;
1781
+ const bandThickness = band ? resolveBandThickness(object, semiMajor, metricContext, context.scaleModel) : void 0;
1782
+ return {
1783
+ kind: useCircle ? "circle" : "ellipse",
1784
+ cx: useCircle ? parent.x : cx,
1785
+ cy: useCircle ? parent.y : cy,
1786
+ radius: useCircle ? semiMajor : void 0,
1787
+ rx: useCircle ? void 0 : semiMajor,
1788
+ ry: useCircle ? void 0 : semiMinor,
1789
+ rotationDeg,
1790
+ band,
1791
+ bandThickness,
1792
+ frontArcPath: context.projection === "isometric" || band ? buildEllipseArcPath(cx, cy, semiMajor, semiMinor, rotationDeg, 0, Math.PI) : void 0,
1793
+ backArcPath: context.projection === "isometric" || band ? buildEllipseArcPath(cx, cy, semiMajor, semiMinor, rotationDeg, Math.PI, Math.PI * 2) : void 0,
1794
+ objectX: objectPoint.x,
1795
+ objectY: objectPoint.y
1796
+ };
1797
+ }
1798
+ function resolveOrbitRadiusPx(object, index, metricContext) {
1799
+ const metric = orbitMetric(object);
1800
+ if (metric === null) {
1801
+ return metricContext.innerPx + index * metricContext.stepPx;
1802
+ }
1803
+ if (metricContext.metricSpread > 0) {
1804
+ return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
1805
+ }
1806
+ return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
1807
+ }
1808
+ function orbitMetric(object) {
1809
+ if (!object.placement || object.placement.mode !== "orbit") {
1810
+ return null;
1811
+ }
1812
+ return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1813
+ }
1814
+ function resolveOrbitPhase(phase, index, count) {
1815
+ const degreeValue = phase ? unitValueToDegrees(phase) : null;
1816
+ if (degreeValue !== null) {
1817
+ return degreesToRadians(degreeValue - 90);
1818
+ }
1819
+ return angleForIndex(index, count, -Math.PI / 2);
1820
+ }
1821
+ function resolveBandThickness(object, orbitRadius, metricContext, scaleModel) {
1822
+ const innerMetric = toDistanceMetric(toUnitValue(object.properties.inner));
1823
+ const outerMetric = toDistanceMetric(toUnitValue(object.properties.outer));
1824
+ if (innerMetric !== null && outerMetric !== null) {
1825
+ const thicknessMetric = Math.abs(outerMetric - innerMetric);
1826
+ if (metricContext.metricSpread > 0) {
1827
+ return clampNumber(thicknessMetric / metricContext.metricSpread * metricContext.pixelSpread * scaleModel.ringThicknessMultiplier, 8, 54);
1828
+ }
1829
+ const referenceMetric = Math.max(Math.max(innerMetric, outerMetric), 1e-4);
1830
+ return clampNumber(thicknessMetric / referenceMetric * orbitRadius * 0.75 * scaleModel.ringThicknessMultiplier, 8, 48);
1831
+ }
1832
+ const fallbackBase = object.type === "belt" ? 18 : 12;
1833
+ return fallbackBase * scaleModel.ringThicknessMultiplier;
1834
+ }
1835
+ function resolveAtPosition(reference, positions, objectMap, index, count, width, height, padding, context) {
1836
+ if (reference.kind === "lagrange") {
1837
+ return resolveLagrangePosition(reference, positions, objectMap, width, height);
1838
+ }
1839
+ if (reference.kind === "anchor") {
1840
+ const anchor = positions.get(reference.objectId);
1841
+ if (anchor) {
1842
+ const angle = angleForIndex(index, count, Math.PI / 5);
1843
+ const distance = (anchor.radius + 36) * context.scaleModel.labelMultiplier;
1844
+ const offset = projectPolarOffset(angle, distance, context.projection, context.projection === "isometric" ? 0.92 : 1);
1845
+ return {
1846
+ x: anchor.x + offset.x,
1847
+ y: anchor.y + offset.y,
1848
+ anchorX: anchor.x,
1849
+ anchorY: anchor.y
1850
+ };
1851
+ }
1852
+ }
1853
+ if (reference.kind === "named") {
1854
+ const anchor = positions.get(reference.name);
1855
+ if (anchor) {
1856
+ const angle = angleForIndex(index, count, Math.PI / 6);
1857
+ const distance = (anchor.radius + 36) * context.scaleModel.labelMultiplier;
1858
+ const offset = projectPolarOffset(angle, distance, context.projection, context.projection === "isometric" ? 0.92 : 1);
1859
+ return {
1860
+ x: anchor.x + offset.x,
1861
+ y: anchor.y + offset.y,
1862
+ anchorX: anchor.x,
1863
+ anchorY: anchor.y
1864
+ };
1865
+ }
1866
+ }
1867
+ return {
1868
+ x: width - padding - 170,
1869
+ y: height - padding - 86 - index * 58 * context.scaleModel.freePlacementMultiplier
1870
+ };
1871
+ }
1872
+ function resolveLagrangePosition(reference, positions, objectMap, width, height) {
1873
+ const primary = reference.secondary ? positions.get(reference.primary) : deriveParentAnchor(reference.primary, positions, objectMap);
1874
+ const secondary = positions.get(reference.secondary ?? reference.primary);
1875
+ if (!primary || !secondary) {
1876
+ return {
1877
+ x: width * 0.7,
1878
+ y: height * 0.25
1879
+ };
1880
+ }
1881
+ const dx = secondary.x - primary.x;
1882
+ const dy = secondary.y - primary.y;
1883
+ const distance = Math.hypot(dx, dy) || 1;
1884
+ const ux = dx / distance;
1885
+ const uy = dy / distance;
1886
+ const nx = -uy;
1887
+ const ny = ux;
1888
+ const offset = clampNumber(distance * 0.25, 24, 68);
1889
+ switch (reference.point) {
1890
+ case "L1":
1891
+ return {
1892
+ x: secondary.x - ux * offset,
1893
+ y: secondary.y - uy * offset,
1894
+ anchorX: secondary.x,
1895
+ anchorY: secondary.y
1896
+ };
1897
+ case "L2":
1898
+ return {
1899
+ x: secondary.x + ux * offset,
1900
+ y: secondary.y + uy * offset,
1901
+ anchorX: secondary.x,
1902
+ anchorY: secondary.y
1903
+ };
1904
+ case "L3":
1905
+ return {
1906
+ x: primary.x - ux * offset,
1907
+ y: primary.y - uy * offset,
1908
+ anchorX: primary.x,
1909
+ anchorY: primary.y
1910
+ };
1911
+ case "L4":
1912
+ return {
1913
+ x: secondary.x + (ux * 0.5 - nx * 0.8660254) * offset,
1914
+ y: secondary.y + (uy * 0.5 - ny * 0.8660254) * offset,
1915
+ anchorX: secondary.x,
1916
+ anchorY: secondary.y
1917
+ };
1918
+ case "L5":
1919
+ return {
1920
+ x: secondary.x + (ux * 0.5 + nx * 0.8660254) * offset,
1921
+ y: secondary.y + (uy * 0.5 + ny * 0.8660254) * offset,
1922
+ anchorX: secondary.x,
1923
+ anchorY: secondary.y
1924
+ };
1925
+ }
1926
+ }
1927
+ function buildSceneRelationships(objects, objectMap) {
1928
+ const parentIds = /* @__PURE__ */ new Map();
1929
+ const childIds = /* @__PURE__ */ new Map();
1930
+ for (const object of objects) {
1931
+ const parentId = resolveParentId(object, objectMap);
1932
+ parentIds.set(object.id, parentId);
1933
+ if (parentId) {
1934
+ const existing = childIds.get(parentId);
1935
+ if (existing) {
1936
+ existing.push(object.id);
1937
+ } else {
1938
+ childIds.set(parentId, [object.id]);
1939
+ }
1940
+ }
1941
+ if (!childIds.has(object.id)) {
1942
+ childIds.set(object.id, []);
1943
+ }
1944
+ }
1945
+ const ancestorIds = /* @__PURE__ */ new Map();
1946
+ const groupIds = /* @__PURE__ */ new Map();
1947
+ const groupRoots = /* @__PURE__ */ new Map();
1948
+ const buildAncestors = (objectId) => {
1949
+ const cached = ancestorIds.get(objectId);
1950
+ if (cached) {
1951
+ return cached;
1952
+ }
1953
+ const seen = /* @__PURE__ */ new Set();
1954
+ const results = [];
1955
+ let cursor = parentIds.get(objectId) ?? null;
1956
+ while (cursor && !seen.has(cursor)) {
1957
+ results.push(cursor);
1958
+ seen.add(cursor);
1959
+ cursor = parentIds.get(cursor) ?? null;
1960
+ }
1961
+ ancestorIds.set(objectId, results);
1962
+ return results;
1963
+ };
1964
+ const resolveGroupRootObjectId2 = (objectId) => {
1965
+ const cached = groupRoots.get(groupIds.get(objectId) ?? "");
1966
+ if (cached) {
1967
+ return cached;
1968
+ }
1969
+ const parentId = parentIds.get(objectId) ?? null;
1970
+ const object = objectMap.get(objectId);
1971
+ let rootObjectId = objectId;
1972
+ if (object?.placement && object.placement.mode !== "free" && parentId) {
1973
+ rootObjectId = resolveGroupRootObjectId2(parentId);
1974
+ }
1975
+ return rootObjectId;
1976
+ };
1977
+ for (const object of objects) {
1978
+ buildAncestors(object.id);
1979
+ const rootObjectId = resolveGroupRootObjectId2(object.id);
1980
+ const groupId = createGroupId(rootObjectId);
1981
+ groupIds.set(object.id, groupId);
1982
+ groupRoots.set(groupId, rootObjectId);
1983
+ }
1984
+ return {
1985
+ parentIds,
1986
+ childIds,
1987
+ ancestorIds,
1988
+ groupIds,
1989
+ groupRoots
1990
+ };
1991
+ }
1992
+ function resolveParentId(object, objectMap) {
1993
+ const placement = object.placement;
1994
+ if (!placement) {
1995
+ return null;
1996
+ }
1997
+ switch (placement.mode) {
1998
+ case "orbit":
1999
+ case "surface":
2000
+ return objectMap.has(placement.target) ? placement.target : null;
2001
+ case "at":
2002
+ switch (placement.reference.kind) {
2003
+ case "anchor":
2004
+ return objectMap.has(placement.reference.objectId) ? placement.reference.objectId : null;
2005
+ case "named":
2006
+ return objectMap.has(placement.reference.name) ? placement.reference.name : null;
2007
+ case "lagrange":
2008
+ if (placement.reference.secondary && objectMap.has(placement.reference.secondary)) {
2009
+ return placement.reference.secondary;
2010
+ }
2011
+ return objectMap.has(placement.reference.primary) ? placement.reference.primary : null;
2012
+ }
2013
+ case "free":
2014
+ return null;
2015
+ }
2016
+ }
2017
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2018
+ let minX = Number.POSITIVE_INFINITY;
2019
+ let minY = Number.POSITIVE_INFINITY;
2020
+ let maxX = Number.NEGATIVE_INFINITY;
2021
+ let maxY = Number.NEGATIVE_INFINITY;
2022
+ const include = (x, y) => {
2023
+ minX = Math.min(minX, x);
2024
+ minY = Math.min(minY, y);
2025
+ maxX = Math.max(maxX, x);
2026
+ maxY = Math.max(maxY, y);
2027
+ };
2028
+ for (const object of objects) {
2029
+ if (!object.hidden && group.objectIds.includes(object.objectId)) {
2030
+ includeObjectBounds(object, include);
2031
+ }
2032
+ }
2033
+ for (const orbit of orbitVisuals) {
2034
+ if (!orbit.hidden && group.orbitIds.includes(orbit.objectId)) {
2035
+ includeOrbitBounds(orbit, include);
2036
+ }
2037
+ }
2038
+ for (const leader of leaders) {
2039
+ if (!leader.hidden && group.leaderIds.includes(leader.objectId)) {
2040
+ include(leader.x1, leader.y1);
2041
+ include(leader.x2, leader.y2);
2042
+ }
2043
+ }
2044
+ for (const label of labels) {
2045
+ if (!label.hidden && group.labelIds.includes(label.objectId)) {
2046
+ includeLabelBounds(label, include);
2047
+ }
2048
+ }
2049
+ if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
2050
+ return createBounds(0, 0, 0, 0);
2051
+ }
2052
+ return createBounds(minX, minY, maxX, maxY);
2053
+ }
2054
+ function resolveGroupRootObjectId(object, objectMap) {
2055
+ let current = object;
2056
+ const seen = /* @__PURE__ */ new Set();
2057
+ while (current.placement && current.placement.mode !== "free" && !seen.has(current.id)) {
2058
+ seen.add(current.id);
2059
+ const parentId = resolveParentId(current, objectMap);
2060
+ if (!parentId) {
2061
+ break;
2062
+ }
2063
+ const parent = objectMap.get(parentId);
2064
+ if (!parent) {
2065
+ break;
2066
+ }
2067
+ current = parent;
2068
+ }
2069
+ return current.id;
2070
+ }
2071
+ function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2072
+ return {
2073
+ left: x - labelHalfWidth,
2074
+ right: x + labelHalfWidth,
2075
+ top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2076
+ bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2077
+ };
2078
+ }
2079
+ function rectsOverlap(left, right) {
2080
+ return !(left.right < right.left || right.right < left.left || left.bottom < right.top || right.bottom < left.top);
2081
+ }
2082
+ function deriveParentAnchor(objectId, positions, objectMap) {
2083
+ const object = objectMap.get(objectId);
2084
+ if (!object?.placement || object.placement.mode !== "orbit") {
2085
+ return positions.get(objectId);
2086
+ }
2087
+ return positions.get(object.placement.target);
2088
+ }
2089
+ function visualRadiusFor(object, depth, scaleModel) {
2090
+ const explicitRadius = toVisualSizeMetric(object.properties.radius, scaleModel);
2091
+ if (explicitRadius !== null) {
2092
+ return explicitRadius;
2093
+ }
2094
+ const multiplier = scaleModel.bodyRadiusMultiplier;
2095
+ switch (object.type) {
2096
+ case "star":
2097
+ return clampNumber((depth === 0 ? 28 : 20) * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2098
+ case "planet":
2099
+ return clampNumber(12 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2100
+ case "moon":
2101
+ return clampNumber(7 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2102
+ case "belt":
2103
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2104
+ case "asteroid":
2105
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2106
+ case "comet":
2107
+ return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2108
+ case "ring":
2109
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2110
+ case "structure":
2111
+ return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2112
+ case "phenomenon":
2113
+ return clampNumber(8 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2114
+ }
2115
+ }
2116
+ function visualExtentForObject(object, radius, scaleModel) {
2117
+ const atmosphereBoost = typeof object.properties.atmosphere === "string" ? 4 : 0;
2118
+ switch (object.type) {
2119
+ case "star":
2120
+ return radius * 2.4;
2121
+ case "phenomenon":
2122
+ return radius * 1.25;
2123
+ case "structure":
2124
+ return radius + 2;
2125
+ default:
2126
+ return Math.min(radius + atmosphereBoost, scaleModel.maxBodyRadius + 10);
2127
+ }
2128
+ }
2129
+ function toDistanceMetric(value) {
2130
+ if (!value)
2131
+ return null;
2132
+ switch (value.unit) {
2133
+ case "au":
2134
+ return value.value;
2135
+ case "km":
2136
+ return value.value / AU_IN_KM;
2137
+ case "re":
2138
+ return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2139
+ case "sol":
2140
+ return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2141
+ default:
2142
+ return value.value;
2143
+ }
2144
+ }
2145
+ function freePlacementOffsetPx(distance, scaleModel) {
2146
+ const metric = toDistanceMetric(distance ?? null);
2147
+ if (metric === null || metric <= 0) {
2148
+ return 0;
2149
+ }
2150
+ return clampNumber(metric * 96 * scaleModel.freePlacementMultiplier, 0, 420);
2151
+ }
2152
+ function toVisualSizeMetric(value, scaleModel) {
2153
+ const unitValue = toUnitValue(value);
2154
+ if (!unitValue) {
2155
+ return null;
2156
+ }
2157
+ let size;
2158
+ switch (unitValue.unit) {
2159
+ case "sol":
2160
+ size = clampNumber(unitValue.value * 22, 14, 40);
2161
+ break;
2162
+ case "re":
2163
+ size = clampNumber(unitValue.value * 10, 6, 18);
2164
+ break;
2165
+ case "km":
2166
+ size = clampNumber(Math.log10(Math.max(unitValue.value, 1)) * 2.6, 4, 16);
2167
+ break;
2168
+ default:
2169
+ size = clampNumber(unitValue.value * 4, 4, 20);
2170
+ break;
2171
+ }
2172
+ return clampNumber(size * scaleModel.bodyRadiusMultiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2173
+ }
2174
+ function toUnitValue(value) {
2175
+ if (!value || typeof value !== "object" || !("value" in value)) {
2176
+ return null;
2177
+ }
2178
+ return value;
2179
+ }
2180
+ function unitValueToDegrees(value) {
2181
+ if (!value) {
2182
+ return null;
2183
+ }
2184
+ return value.unit === "deg" || value.unit === null ? value.value : null;
2185
+ }
2186
+ function angleForIndex(index, count, startAngle) {
2187
+ if (count <= 1)
2188
+ return startAngle;
2189
+ return startAngle + index * Math.PI * 2 / count;
2190
+ }
2191
+ function buildEllipseArcPath(cx, cy, rx, ry, rotationDeg, start, end) {
2192
+ const points = sampleEllipseArcPoints(cx, cy, rx, ry, rotationDeg, start, end, ARC_SAMPLE_COUNT);
2193
+ if (points.length === 0) {
2194
+ return "";
2195
+ }
2196
+ return points.map((point, index) => `${index === 0 ? "M" : "L"} ${formatNumber(point.x)} ${formatNumber(point.y)}`).join(" ");
2197
+ }
2198
+ function sampleEllipseArcPoints(cx, cy, rx, ry, rotationDeg, start, end, segments) {
2199
+ const points = [];
2200
+ for (let index = 0; index <= segments; index += 1) {
2201
+ const t = start + (end - start) * index / segments;
2202
+ points.push(ellipsePoint(cx, cy, rx, ry, rotationDeg, t));
2203
+ }
2204
+ return points;
2205
+ }
2206
+ function ellipsePoint(cx, cy, rx, ry, rotationDeg, angle) {
2207
+ const localX = rx * Math.cos(angle);
2208
+ const localY = ry * Math.sin(angle);
2209
+ const rotated = rotateOffset(localX, localY, rotationDeg);
2210
+ return {
2211
+ x: cx + rotated.x,
2212
+ y: cy + rotated.y
2213
+ };
2214
+ }
2215
+ function rotateOffset(x, y, rotationDeg) {
2216
+ const radians = degreesToRadians(rotationDeg);
2217
+ return {
2218
+ x: x * Math.cos(radians) - y * Math.sin(radians),
2219
+ y: x * Math.sin(radians) + y * Math.cos(radians)
2220
+ };
2221
+ }
2222
+ function projectPolarOffset(angle, distance, projection, verticalFactor) {
2223
+ const yScale = projection === "isometric" ? ISO_FLATTENING * verticalFactor : verticalFactor;
2224
+ return {
2225
+ x: Math.cos(angle) * distance,
2226
+ y: Math.sin(angle) * distance * yScale
2227
+ };
2228
+ }
2229
+ function computeSortKey(x, y, depth) {
2230
+ return y * 1e3 + x + depth * 0.01;
2231
+ }
2232
+ function clampNumber(value, min, max) {
2233
+ return Math.min(Math.max(value, min), max);
2234
+ }
2235
+ function pushGrouped(map, key, value) {
2236
+ const existing = map.get(key);
2237
+ if (existing) {
2238
+ existing.push(value);
2239
+ } else {
2240
+ map.set(key, [value]);
2241
+ }
2242
+ }
2243
+ function createRenderId(objectId) {
2244
+ const normalized = objectId.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "object";
2245
+ return `wo-${normalized}`;
2246
+ }
2247
+ function createGroupId(objectId) {
2248
+ return `${createRenderId(objectId)}-group`;
2249
+ }
2250
+ function customColorFor(value) {
2251
+ return typeof value === "string" && value.trim() ? value : void 0;
2252
+ }
2253
+ function estimateLabelHalfWidth(object, labelMultiplier) {
2254
+ const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2255
+ const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2256
+ return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2257
+ }
2258
+ function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2259
+ const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2260
+ const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
2261
+ return Math.max(primaryWidth, secondaryWidth, 24);
2262
+ }
2263
+ function capitalizeLabel(value) {
2264
+ return value.length > 0 ? value[0].toUpperCase() + value.slice(1) : value;
2265
+ }
2266
+ function degreesToRadians(value) {
2267
+ return value * Math.PI / 180;
2268
+ }
2269
+ function formatNumber(value) {
2270
+ return Number.isInteger(value) ? String(value) : value.toFixed(2);
2271
+ }
2272
+
2273
+ // packages/core/dist/draft.js
2274
+ function materializeAtlasDocument(document2) {
2275
+ const system = document2.system ? {
2276
+ type: "system",
2277
+ id: document2.system.id,
2278
+ properties: materializeDraftSystemProperties(document2.system),
2279
+ info: materializeDraftSystemInfo(document2.system)
2280
+ } : null;
2281
+ return {
2282
+ format: "worldorbit",
2283
+ version: "1.0",
2284
+ system,
2285
+ objects: document2.objects.map(cloneWorldOrbitObject)
2286
+ };
2287
+ }
2288
+ function cloneWorldOrbitObject(object) {
2289
+ return {
2290
+ ...object,
2291
+ properties: cloneProperties(object.properties),
2292
+ placement: object.placement ? structuredClone(object.placement) : null,
2293
+ info: { ...object.info }
2294
+ };
2295
+ }
2296
+ function cloneProperties(properties) {
2297
+ const next = {};
2298
+ for (const [key, value] of Object.entries(properties)) {
2299
+ if (Array.isArray(value)) {
2300
+ next[key] = [...value];
2301
+ continue;
2302
+ }
2303
+ if (value && typeof value === "object" && "value" in value) {
2304
+ next[key] = {
2305
+ value: value.value,
2306
+ unit: value.unit
2307
+ };
2308
+ continue;
2309
+ }
2310
+ next[key] = value;
2311
+ }
2312
+ return next;
2313
+ }
2314
+ function materializeDraftSystemProperties(system) {
2315
+ const properties = {};
2316
+ if (system.title) {
2317
+ properties.title = system.title;
2318
+ }
2319
+ properties.view = system.defaults.view;
2320
+ if (system.defaults.scale) {
2321
+ properties.scale = system.defaults.scale;
2322
+ }
2323
+ if (system.defaults.units) {
2324
+ properties.units = system.defaults.units;
2325
+ }
2326
+ return properties;
2327
+ }
2328
+ function materializeDraftSystemInfo(system) {
2329
+ const info = {
2330
+ ...system.atlasMetadata
2331
+ };
2332
+ if (system.defaults.theme) {
2333
+ info["atlas.theme"] = system.defaults.theme;
2334
+ }
2335
+ for (const viewpoint of system.viewpoints) {
2336
+ const prefix = `viewpoint.${viewpoint.id}`;
2337
+ info[`${prefix}.label`] = viewpoint.label;
2338
+ if (viewpoint.summary) {
2339
+ info[`${prefix}.summary`] = viewpoint.summary;
2340
+ }
2341
+ if (viewpoint.focusObjectId) {
2342
+ info[`${prefix}.focus`] = viewpoint.focusObjectId;
2343
+ }
2344
+ if (viewpoint.selectedObjectId) {
2345
+ info[`${prefix}.select`] = viewpoint.selectedObjectId;
2346
+ }
2347
+ if (viewpoint.projection) {
2348
+ info[`${prefix}.projection`] = viewpoint.projection;
2349
+ }
2350
+ if (viewpoint.preset) {
2351
+ info[`${prefix}.preset`] = viewpoint.preset;
2352
+ }
2353
+ if (viewpoint.zoom !== null) {
2354
+ info[`${prefix}.zoom`] = String(viewpoint.zoom);
2355
+ }
2356
+ if (viewpoint.rotationDeg !== 0) {
2357
+ info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2358
+ }
2359
+ const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2360
+ if (serializedLayers) {
2361
+ info[`${prefix}.layers`] = serializedLayers;
2362
+ }
2363
+ if (viewpoint.filter?.query) {
2364
+ info[`${prefix}.query`] = viewpoint.filter.query;
2365
+ }
2366
+ if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2367
+ info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2368
+ }
2369
+ if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2370
+ info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2371
+ }
2372
+ if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2373
+ info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2374
+ }
2375
+ }
2376
+ for (const annotation of system.annotations) {
2377
+ const prefix = `annotation.${annotation.id}`;
2378
+ info[`${prefix}.label`] = annotation.label;
2379
+ if (annotation.targetObjectId) {
2380
+ info[`${prefix}.target`] = annotation.targetObjectId;
2381
+ }
2382
+ info[`${prefix}.body`] = annotation.body;
2383
+ if (annotation.tags.length > 0) {
2384
+ info[`${prefix}.tags`] = annotation.tags.join(" ");
2385
+ }
2386
+ if (annotation.sourceObjectId) {
2387
+ info[`${prefix}.source`] = annotation.sourceObjectId;
2388
+ }
2389
+ }
2390
+ return info;
2391
+ }
2392
+ function serializeViewpointLayers(layers) {
2393
+ const tokens = [];
2394
+ const orbitFront = layers["orbits-front"];
2395
+ const orbitBack = layers["orbits-back"];
2396
+ if (orbitFront !== void 0 || orbitBack !== void 0) {
2397
+ tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2398
+ }
2399
+ for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2400
+ if (layers[key] !== void 0) {
2401
+ tokens.push(layers[key] ? key : `-${key}`);
2402
+ }
2403
+ }
2404
+ return tokens.join(" ");
2405
+ }
2406
+
2407
+ // packages/core/dist/draft-parse.js
2408
+ function parseWorldOrbitAtlas(source) {
2409
+ return parseAtlasSource(source, "2.0");
2410
+ }
2411
+ function parseAtlasSource(source, outputVersion) {
2412
+ const lines = source.split(/\r?\n/);
2413
+ let sawSchemaHeader = false;
2414
+ let schemaVersion = "2.0";
2415
+ let system = null;
2416
+ let section = null;
2417
+ const objectNodes = [];
2418
+ let sawDefaults = false;
2419
+ let sawAtlas = false;
2420
+ const viewpointIds = /* @__PURE__ */ new Set();
2421
+ const annotationIds = /* @__PURE__ */ new Set();
2422
+ for (let index = 0; index < lines.length; index++) {
2423
+ const rawLine = lines[index];
2424
+ const lineNumber = index + 1;
2425
+ if (!rawLine.trim()) {
2426
+ continue;
2427
+ }
2428
+ const indent = getIndent(rawLine);
2429
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
2430
+ line: lineNumber,
2431
+ columnOffset: indent
2432
+ });
2433
+ if (tokens.length === 0) {
2434
+ continue;
2435
+ }
2436
+ if (!sawSchemaHeader) {
2437
+ schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
2438
+ sawSchemaHeader = true;
2439
+ continue;
2440
+ }
2441
+ if (indent === 0) {
2442
+ section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
2443
+ sawDefaults,
2444
+ sawAtlas
2445
+ });
2446
+ if (section.kind === "system") {
2447
+ system = section.system;
2448
+ } else if (section.kind === "defaults") {
2449
+ sawDefaults = true;
2450
+ } else if (section.kind === "atlas") {
2451
+ sawAtlas = true;
2452
+ }
2453
+ continue;
2454
+ }
2455
+ if (!section) {
2456
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
2457
+ }
2458
+ handleSectionLine(section, indent, tokens, lineNumber);
2459
+ }
2460
+ if (!sawSchemaHeader) {
2461
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
2462
+ }
2463
+ const ast = {
2464
+ type: "document",
2465
+ objects: objectNodes
2466
+ };
2467
+ const normalizedObjects = normalizeDocument(ast).objects;
2468
+ validateDocument({
2469
+ format: "worldorbit",
2470
+ version: "1.0",
2471
+ system: null,
2472
+ objects: normalizedObjects
2473
+ });
2474
+ const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
2475
+ {
2476
+ code: "load.schema.deprecatedDraft",
2477
+ severity: "warning",
2478
+ source: "upgrade",
2479
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
2480
+ }
2481
+ ] : [];
2482
+ return {
2483
+ format: "worldorbit",
2484
+ version: outputVersion,
2485
+ sourceVersion: "1.0",
2486
+ system,
2487
+ objects: normalizedObjects,
2488
+ diagnostics
2489
+ };
2490
+ }
2491
+ function assertDraftSchemaHeader(tokens, line) {
2492
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
2493
+ throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
2494
+ }
2495
+ return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
2496
+ }
2497
+ function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
2498
+ const keyword = tokens[0]?.value.toLowerCase();
2499
+ switch (keyword) {
2500
+ case "system":
2501
+ if (system) {
2502
+ throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
2503
+ }
2504
+ return startSystemSection(tokens, line);
2505
+ case "defaults":
2506
+ if (!system) {
2507
+ throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
2508
+ }
2509
+ if (flags.sawDefaults) {
2510
+ throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
2511
+ }
2512
+ return {
2513
+ kind: "defaults",
2514
+ system,
2515
+ seenFields: /* @__PURE__ */ new Set()
2516
+ };
2517
+ case "atlas":
2518
+ if (!system) {
2519
+ throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
2520
+ }
2521
+ if (flags.sawAtlas) {
2522
+ throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
2523
+ }
2524
+ return {
2525
+ kind: "atlas",
2526
+ system,
2527
+ inMetadata: false,
2528
+ metadataIndent: null
2529
+ };
2530
+ case "viewpoint":
2531
+ if (!system) {
2532
+ throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
2533
+ }
2534
+ return startViewpointSection(tokens, line, system, viewpointIds);
2535
+ case "annotation":
2536
+ if (!system) {
2537
+ throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
2538
+ }
2539
+ return startAnnotationSection(tokens, line, system, annotationIds);
2540
+ case "object":
2541
+ return startObjectSection(tokens, line, objectNodes);
2542
+ default:
2543
+ throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
2544
+ }
2545
+ }
2546
+ function startSystemSection(tokens, line) {
2547
+ if (tokens.length !== 2) {
2548
+ throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
2549
+ }
2550
+ const system = {
2551
+ type: "system",
2552
+ id: tokens[1].value,
2553
+ title: null,
2554
+ defaults: {
2555
+ view: "topdown",
2556
+ scale: null,
2557
+ units: null,
2558
+ preset: null,
2559
+ theme: null
2560
+ },
2561
+ atlasMetadata: {},
2562
+ viewpoints: [],
2563
+ annotations: []
2564
+ };
2565
+ return {
2566
+ kind: "system",
2567
+ system,
2568
+ seenFields: /* @__PURE__ */ new Set()
2569
+ };
2570
+ }
2571
+ function startViewpointSection(tokens, line, system, viewpointIds) {
2572
+ if (tokens.length !== 2) {
2573
+ throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
2574
+ }
2575
+ const id = normalizeIdentifier(tokens[1].value);
2576
+ if (!id) {
2577
+ throw new WorldOrbitError("Viewpoint id must not be empty", line, tokens[1].column);
2578
+ }
2579
+ if (viewpointIds.has(id)) {
2580
+ throw new WorldOrbitError(`Duplicate viewpoint id "${id}"`, line, tokens[1].column);
2581
+ }
2582
+ const viewpoint = {
2583
+ id,
2584
+ label: humanizeIdentifier2(id),
2585
+ summary: "",
2586
+ focusObjectId: null,
2587
+ selectedObjectId: null,
2588
+ projection: system.defaults.view,
2589
+ preset: system.defaults.preset,
2590
+ zoom: null,
2591
+ rotationDeg: 0,
2592
+ layers: {},
2593
+ filter: null
2594
+ };
2595
+ system.viewpoints.push(viewpoint);
2596
+ viewpointIds.add(id);
2597
+ return {
2598
+ kind: "viewpoint",
2599
+ viewpoint,
2600
+ seenFields: /* @__PURE__ */ new Set(),
2601
+ inFilter: false,
2602
+ filterIndent: null,
2603
+ seenFilterFields: /* @__PURE__ */ new Set()
2604
+ };
2605
+ }
2606
+ function startAnnotationSection(tokens, line, system, annotationIds) {
2607
+ if (tokens.length !== 2) {
2608
+ throw new WorldOrbitError("Invalid annotation declaration", line, tokens[0]?.column ?? 1);
2609
+ }
2610
+ const id = normalizeIdentifier(tokens[1].value);
2611
+ if (!id) {
2612
+ throw new WorldOrbitError("Annotation id must not be empty", line, tokens[1].column);
2613
+ }
2614
+ if (annotationIds.has(id)) {
2615
+ throw new WorldOrbitError(`Duplicate annotation id "${id}"`, line, tokens[1].column);
2616
+ }
2617
+ const annotation = {
2618
+ id,
2619
+ label: humanizeIdentifier2(id),
2620
+ targetObjectId: null,
2621
+ body: "",
2622
+ tags: [],
2623
+ sourceObjectId: null
2624
+ };
2625
+ system.annotations.push(annotation);
2626
+ annotationIds.add(id);
2627
+ return {
2628
+ kind: "annotation",
2629
+ annotation,
2630
+ seenFields: /* @__PURE__ */ new Set()
2631
+ };
2632
+ }
2633
+ function startObjectSection(tokens, line, objectNodes) {
2634
+ if (tokens.length < 3) {
2635
+ throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
2636
+ }
2637
+ const objectTypeToken = tokens[1];
2638
+ const idToken = tokens[2];
2639
+ const objectType = objectTypeToken.value;
2640
+ if (!WORLDORBIT_OBJECT_TYPES.has(objectType) || objectType === "system") {
2641
+ throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
2642
+ }
2643
+ const objectNode = {
2644
+ type: "object",
2645
+ objectType,
2646
+ name: idToken.value,
2647
+ inlineFields: parseInlineFields2(tokens.slice(3), line),
2648
+ blockFields: [],
2649
+ infoEntries: [],
2650
+ location: {
2651
+ line,
2652
+ column: objectTypeToken.column
2653
+ }
2654
+ };
2655
+ objectNodes.push(objectNode);
2656
+ return {
2657
+ kind: "object",
2658
+ objectNode,
2659
+ inInfoBlock: false,
2660
+ infoIndent: null
2661
+ };
2662
+ }
2663
+ function handleSectionLine(section, indent, tokens, line) {
2664
+ switch (section.kind) {
2665
+ case "system":
2666
+ applySystemField(section, tokens, line);
2667
+ return;
2668
+ case "defaults":
2669
+ applyDefaultsField(section, tokens, line);
2670
+ return;
2671
+ case "atlas":
2672
+ applyAtlasField(section, indent, tokens, line);
2673
+ return;
2674
+ case "viewpoint":
2675
+ applyViewpointField2(section, indent, tokens, line);
2676
+ return;
2677
+ case "annotation":
2678
+ applyAnnotationField(section, tokens, line);
2679
+ return;
2680
+ case "object":
2681
+ applyObjectField(section, indent, tokens, line);
2682
+ return;
2683
+ }
2684
+ }
2685
+ function applySystemField(section, tokens, line) {
2686
+ const key = requireUniqueField(tokens, section.seenFields, line);
2687
+ if (key !== "title") {
2688
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
2689
+ }
2690
+ section.system.title = joinFieldValue(tokens, line);
2691
+ }
2692
+ function applyDefaultsField(section, tokens, line) {
2693
+ const key = requireUniqueField(tokens, section.seenFields, line);
2694
+ const value = joinFieldValue(tokens, line);
2695
+ switch (key) {
2696
+ case "view":
2697
+ section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
2698
+ return;
2699
+ case "scale":
2700
+ section.system.defaults.scale = value;
2701
+ return;
2702
+ case "units":
2703
+ section.system.defaults.units = value;
2704
+ return;
2705
+ case "preset":
2706
+ section.system.defaults.preset = parsePresetValue(value, line, tokens[0].column);
2707
+ return;
2708
+ case "theme":
2709
+ section.system.defaults.theme = value;
2710
+ return;
2711
+ default:
2712
+ throw new WorldOrbitError(`Unknown defaults field "${tokens[0].value}"`, line, tokens[0].column);
2713
+ }
2714
+ }
2715
+ function applyAtlasField(section, indent, tokens, line) {
2716
+ if (section.inMetadata && indent <= (section.metadataIndent ?? 0)) {
2717
+ section.inMetadata = false;
2718
+ section.metadataIndent = null;
2719
+ }
2720
+ if (section.inMetadata) {
2721
+ if (tokens.length < 2) {
2722
+ throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
2723
+ }
2724
+ const key = tokens[0].value;
2725
+ if (key in section.system.atlasMetadata) {
2726
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
2727
+ }
2728
+ section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
2729
+ return;
2730
+ }
2731
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
2732
+ section.inMetadata = true;
2733
+ section.metadataIndent = indent;
2734
+ return;
2735
+ }
2736
+ throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
2737
+ }
2738
+ function applyViewpointField2(section, indent, tokens, line) {
2739
+ if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
2740
+ section.inFilter = false;
2741
+ section.filterIndent = null;
2742
+ }
2743
+ if (section.inFilter) {
2744
+ applyViewpointFilterField(section, tokens, line);
2745
+ return;
2746
+ }
2747
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
2748
+ if (section.seenFields.has("filter")) {
2749
+ throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
2750
+ }
2751
+ section.seenFields.add("filter");
2752
+ section.inFilter = true;
2753
+ section.filterIndent = indent;
2754
+ return;
2755
+ }
2756
+ const key = requireUniqueField(tokens, section.seenFields, line);
2757
+ const value = joinFieldValue(tokens, line);
2758
+ switch (key) {
2759
+ case "label":
2760
+ section.viewpoint.label = value;
2761
+ return;
2762
+ case "summary":
2763
+ section.viewpoint.summary = value;
2764
+ return;
2765
+ case "focus":
2766
+ section.viewpoint.focusObjectId = value;
2767
+ return;
2768
+ case "select":
2769
+ section.viewpoint.selectedObjectId = value;
2770
+ return;
2771
+ case "projection":
2772
+ section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
2773
+ return;
2774
+ case "preset":
2775
+ section.viewpoint.preset = parsePresetValue(value, line, tokens[0].column);
2776
+ return;
2777
+ case "zoom":
2778
+ section.viewpoint.zoom = parsePositiveNumber2(value, line, tokens[0].column, "zoom");
2779
+ return;
2780
+ case "rotation":
2781
+ section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
2782
+ return;
2783
+ case "layers":
2784
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
2785
+ return;
2786
+ default:
2787
+ throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
2788
+ }
2789
+ }
2790
+ function applyViewpointFilterField(section, tokens, line) {
2791
+ const key = requireUniqueField(tokens, section.seenFilterFields, line);
2792
+ const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
2793
+ switch (key) {
2794
+ case "query":
2795
+ filter.query = joinFieldValue(tokens, line);
2796
+ break;
2797
+ case "objecttypes":
2798
+ filter.objectTypes = parseObjectTypeTokens(tokens.slice(1), line);
2799
+ break;
2800
+ case "tags":
2801
+ filter.tags = parseTokenList(tokens.slice(1), line, "tags");
2802
+ break;
2803
+ case "groups":
2804
+ filter.groupIds = parseTokenList(tokens.slice(1), line, "groups");
2805
+ break;
2806
+ default:
2807
+ throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
2808
+ }
2809
+ section.viewpoint.filter = filter;
2810
+ }
2811
+ function applyAnnotationField(section, tokens, line) {
2812
+ const key = requireUniqueField(tokens, section.seenFields, line);
2813
+ switch (key) {
2814
+ case "label":
2815
+ section.annotation.label = joinFieldValue(tokens, line);
2816
+ return;
2817
+ case "target":
2818
+ section.annotation.targetObjectId = joinFieldValue(tokens, line);
2819
+ return;
2820
+ case "body":
2821
+ section.annotation.body = joinFieldValue(tokens, line);
2822
+ return;
2823
+ case "tags":
2824
+ section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
2825
+ return;
2826
+ default:
2827
+ throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
2828
+ }
2829
+ }
2830
+ function applyObjectField(section, indent, tokens, line) {
2831
+ if (tokens.length === 1 && tokens[0].value === "info") {
2832
+ section.inInfoBlock = true;
2833
+ section.infoIndent = indent;
2834
+ return;
2835
+ }
2836
+ if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
2837
+ section.inInfoBlock = false;
2838
+ section.infoIndent = null;
2839
+ }
2840
+ if (section.inInfoBlock) {
2841
+ section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
2842
+ return;
2843
+ }
2844
+ section.objectNode.blockFields.push(parseField2(tokens, line));
2845
+ }
2846
+ function requireUniqueField(tokens, seenFields, line) {
2847
+ if (tokens.length < 2) {
2848
+ throw new WorldOrbitError("Invalid atlas field line", line, tokens[0]?.column ?? 1);
2849
+ }
2850
+ const key = tokens[0].value.toLowerCase();
2851
+ if (seenFields.has(key)) {
2852
+ throw new WorldOrbitError(`Duplicate atlas field "${tokens[0].value}"`, line, tokens[0].column);
2853
+ }
2854
+ seenFields.add(key);
2855
+ return key;
2856
+ }
2857
+ function joinFieldValue(tokens, line) {
2858
+ if (tokens.length < 2) {
2859
+ throw new WorldOrbitError("Missing value for atlas field", line, tokens[0]?.column ?? 1);
2860
+ }
2861
+ return tokens.slice(1).map((token) => token.value).join(" ").trim();
2862
+ }
2863
+ function parseObjectTypeTokens(tokens, line) {
2864
+ if (tokens.length === 0) {
2865
+ throw new WorldOrbitError("Missing value for atlas field", line);
2866
+ }
2867
+ return tokens.map((token) => {
2868
+ const value = token.value;
2869
+ if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
2870
+ throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
2871
+ }
2872
+ return value;
2873
+ });
2874
+ }
2875
+ function parseTokenList(tokens, line, field) {
2876
+ if (tokens.length === 0) {
2877
+ throw new WorldOrbitError(`Missing value for field "${field}"`, line);
2878
+ }
2879
+ return tokens.map((token) => token.value);
2880
+ }
2881
+ function parseLayerTokens(tokens, line) {
2882
+ if (tokens.length === 0) {
2883
+ throw new WorldOrbitError('Missing value for field "layers"', line);
2884
+ }
2885
+ const next = {};
2886
+ for (const token of tokens) {
2887
+ const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
2888
+ const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
2889
+ if (rawLayer === "orbits") {
2890
+ next["orbits-back"] = enabled;
2891
+ next["orbits-front"] = enabled;
2892
+ continue;
2893
+ }
2894
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2895
+ next[rawLayer] = enabled;
2896
+ continue;
2897
+ }
2898
+ throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
2899
+ }
2900
+ return next;
2901
+ }
2902
+ function parseProjectionValue(value, line, column) {
2903
+ const normalized = value.toLowerCase();
2904
+ if (normalized === "topdown" || normalized === "isometric") {
2905
+ return normalized;
2906
+ }
2907
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
2908
+ }
2909
+ function parsePresetValue(value, line, column) {
2910
+ const normalized = value.toLowerCase();
2911
+ if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
2912
+ return normalized;
2913
+ }
2914
+ throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
2915
+ }
2916
+ function parsePositiveNumber2(value, line, column, field) {
2917
+ const parsed = Number(value);
2918
+ if (!Number.isFinite(parsed) || parsed <= 0) {
2919
+ throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
2920
+ }
2921
+ return parsed;
2922
+ }
2923
+ function parseFiniteNumber2(value, line, column, field) {
2924
+ const parsed = Number(value);
2925
+ if (!Number.isFinite(parsed)) {
2926
+ throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
2927
+ }
2928
+ return parsed;
2929
+ }
2930
+ function createEmptyViewpointFilter2() {
2931
+ return {
2932
+ query: null,
2933
+ objectTypes: [],
2934
+ tags: [],
2935
+ groupIds: []
2936
+ };
2937
+ }
2938
+ function parseInlineFields2(tokens, line) {
2939
+ const fields = [];
2940
+ let index = 0;
2941
+ while (index < tokens.length) {
2942
+ const keyToken = tokens[index];
2943
+ const schema = getFieldSchema(keyToken.value);
2944
+ if (!schema) {
2945
+ throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
2946
+ }
2947
+ index++;
2948
+ const valueTokens = [];
2949
+ if (schema.arity === "multiple") {
2950
+ while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
2951
+ valueTokens.push(tokens[index]);
2952
+ index++;
2953
+ }
2954
+ } else {
2955
+ const nextToken = tokens[index];
2956
+ if (nextToken) {
2957
+ valueTokens.push(nextToken);
2958
+ index++;
2959
+ }
2960
+ }
2961
+ if (valueTokens.length === 0) {
2962
+ throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
2963
+ }
2964
+ fields.push({
2965
+ type: "field",
2966
+ key: keyToken.value,
2967
+ values: valueTokens.map((token) => token.value),
2968
+ location: { line, column: keyToken.column }
2969
+ });
2970
+ }
2971
+ return fields;
2972
+ }
2973
+ function parseField2(tokens, line) {
2974
+ if (tokens.length < 2) {
2975
+ throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
2976
+ }
2977
+ if (!getFieldSchema(tokens[0].value)) {
2978
+ throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
2979
+ }
2980
+ return {
2981
+ type: "field",
2982
+ key: tokens[0].value,
2983
+ values: tokens.slice(1).map((token) => token.value),
2984
+ location: { line, column: tokens[0].column }
2985
+ };
2986
+ }
2987
+ function parseInfoEntry2(tokens, line) {
2988
+ if (tokens.length < 2) {
2989
+ throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
2990
+ }
2991
+ return {
2992
+ type: "info-entry",
2993
+ key: tokens[0].value,
2994
+ value: tokens.slice(1).map((token) => token.value).join(" "),
2995
+ location: { line, column: tokens[0].column }
2996
+ };
2997
+ }
2998
+ function normalizeIdentifier(value) {
2999
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3000
+ }
3001
+ function humanizeIdentifier2(value) {
3002
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3003
+ }
3004
+
3005
+ // packages/core/dist/load.js
3006
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
3007
+ var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3008
+ function detectWorldOrbitSchemaVersion(source) {
3009
+ for (const line of source.split(/\r?\n/)) {
3010
+ const trimmed = line.trim();
3011
+ if (!trimmed) {
3012
+ continue;
3013
+ }
3014
+ if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3015
+ return "2.0-draft";
3016
+ }
3017
+ if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3018
+ return "2.0";
3019
+ }
3020
+ return "1.0";
3021
+ }
3022
+ return "1.0";
3023
+ }
3024
+ function loadWorldOrbitSource(source) {
3025
+ const result = loadWorldOrbitSourceWithDiagnostics(source);
3026
+ if (!result.ok || !result.value) {
3027
+ const diagnostic = result.diagnostics[0];
3028
+ throw new WorldOrbitError(diagnostic?.message ?? "Failed to load WorldOrbit source", diagnostic?.line, diagnostic?.column);
3029
+ }
3030
+ return result.value;
3031
+ }
3032
+ function loadWorldOrbitSourceWithDiagnostics(source) {
3033
+ const schemaVersion = detectWorldOrbitSchemaVersion(source);
3034
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
3035
+ return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3036
+ }
3037
+ let ast;
3038
+ try {
3039
+ ast = parseWorldOrbit(source);
3040
+ } catch (error) {
3041
+ return {
3042
+ ok: false,
3043
+ value: null,
3044
+ diagnostics: [diagnosticFromError(error, "parse")]
3045
+ };
3046
+ }
3047
+ let document2;
3048
+ try {
3049
+ document2 = normalizeDocument(ast);
3050
+ } catch (error) {
3051
+ return {
3052
+ ok: false,
3053
+ value: null,
3054
+ diagnostics: [diagnosticFromError(error, "normalize")]
3055
+ };
3056
+ }
3057
+ try {
3058
+ validateDocument(document2);
3059
+ } catch (error) {
3060
+ return {
3061
+ ok: false,
3062
+ value: null,
3063
+ diagnostics: [diagnosticFromError(error, "validate")]
3064
+ };
3065
+ }
3066
+ return {
3067
+ ok: true,
3068
+ value: {
3069
+ schemaVersion,
3070
+ ast,
3071
+ document: document2,
3072
+ atlasDocument: null,
3073
+ draftDocument: null,
3074
+ diagnostics: []
3075
+ },
3076
+ diagnostics: []
3077
+ };
3078
+ }
3079
+ function loadAtlasSourceWithDiagnostics(source, schemaVersion) {
3080
+ let atlasDocument;
3081
+ try {
3082
+ atlasDocument = parseWorldOrbitAtlas(source);
3083
+ } catch (error) {
3084
+ return {
3085
+ ok: false,
3086
+ value: null,
3087
+ diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
3088
+ };
3089
+ }
3090
+ let document2;
3091
+ try {
3092
+ document2 = materializeAtlasDocument(atlasDocument);
3093
+ } catch (error) {
3094
+ return {
3095
+ ok: false,
3096
+ value: null,
3097
+ diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
3098
+ };
3099
+ }
3100
+ try {
3101
+ validateDocument(document2);
3102
+ } catch (error) {
3103
+ return {
3104
+ ok: false,
3105
+ value: null,
3106
+ diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
3107
+ };
3108
+ }
3109
+ const loaded = {
3110
+ schemaVersion,
3111
+ ast: null,
3112
+ document: document2,
3113
+ atlasDocument,
3114
+ draftDocument: atlasDocument,
3115
+ diagnostics: [...atlasDocument.diagnostics]
3116
+ };
3117
+ return {
3118
+ ok: true,
3119
+ value: loaded,
3120
+ diagnostics: [...atlasDocument.diagnostics]
3121
+ };
3122
+ }
3123
+
3124
+ // packages/viewer/dist/theme.js
3125
+ var DEFAULT_LAYERS = {
3126
+ background: true,
3127
+ guides: true,
3128
+ orbits: true,
3129
+ objects: true,
3130
+ labels: true,
3131
+ structures: true,
3132
+ metadata: true
3133
+ };
3134
+ var THEME_PRESETS = {
3135
+ atlas: {
3136
+ name: "atlas",
3137
+ backgroundStart: "#041018",
3138
+ backgroundEnd: "#0a2331",
3139
+ backgroundGlow: "rgba(240, 180, 100, 0.18)",
3140
+ panel: "rgba(7, 17, 27, 0.9)",
3141
+ panelLine: "rgba(168, 207, 242, 0.18)",
3142
+ orbit: "rgba(163, 209, 255, 0.24)",
3143
+ orbitBand: "rgba(255, 190, 120, 0.28)",
3144
+ guide: "rgba(255, 255, 255, 0.04)",
3145
+ leader: "rgba(225, 238, 255, 0.4)",
3146
+ ink: "#e8f0ff",
3147
+ muted: "rgba(232, 240, 255, 0.7)",
3148
+ accent: "#f0b464",
3149
+ accentStrong: "#ff7f5f",
3150
+ selected: "rgba(255, 214, 139, 0.92)",
3151
+ starCore: "#ffcc67",
3152
+ starStroke: "rgba(255, 245, 203, 0.85)",
3153
+ starGlow: "#ffe8a3",
3154
+ fontFamily: '"Segoe UI Variable", "Bahnschrift", sans-serif',
3155
+ displayFont: '"Bahnschrift", "Segoe UI Variable", sans-serif'
3156
+ },
3157
+ nightglass: {
3158
+ name: "nightglass",
3159
+ backgroundStart: "#07131f",
3160
+ backgroundEnd: "#13283a",
3161
+ backgroundGlow: "rgba(120, 255, 215, 0.16)",
3162
+ panel: "rgba(7, 20, 30, 0.9)",
3163
+ panelLine: "rgba(120, 255, 215, 0.16)",
3164
+ orbit: "rgba(120, 255, 215, 0.2)",
3165
+ orbitBand: "rgba(137, 185, 255, 0.24)",
3166
+ guide: "rgba(255, 255, 255, 0.035)",
3167
+ leader: "rgba(192, 255, 233, 0.42)",
3168
+ ink: "#edfff8",
3169
+ muted: "rgba(237, 255, 248, 0.68)",
3170
+ accent: "#78ffd7",
3171
+ accentStrong: "#9ce7ff",
3172
+ selected: "rgba(120, 255, 215, 0.9)",
3173
+ starCore: "#e5f98c",
3174
+ starStroke: "rgba(246, 255, 217, 0.9)",
3175
+ starGlow: "#fffab4",
3176
+ fontFamily: '"Segoe UI Variable", "Bahnschrift", sans-serif',
3177
+ displayFont: '"Bahnschrift", "Segoe UI Variable", sans-serif'
3178
+ },
3179
+ ember: {
3180
+ name: "ember",
3181
+ backgroundStart: "#17090b",
3182
+ backgroundEnd: "#31111a",
3183
+ backgroundGlow: "rgba(255, 127, 95, 0.18)",
3184
+ panel: "rgba(24, 9, 13, 0.9)",
3185
+ panelLine: "rgba(255, 166, 149, 0.16)",
3186
+ orbit: "rgba(255, 188, 164, 0.22)",
3187
+ orbitBand: "rgba(255, 214, 139, 0.24)",
3188
+ guide: "rgba(255, 255, 255, 0.03)",
3189
+ leader: "rgba(255, 223, 209, 0.42)",
3190
+ ink: "#fff3ee",
3191
+ muted: "rgba(255, 243, 238, 0.68)",
3192
+ accent: "#ffb27d",
3193
+ accentStrong: "#ff7f5f",
3194
+ selected: "rgba(255, 178, 125, 0.9)",
3195
+ starCore: "#ffb766",
3196
+ starStroke: "rgba(255, 236, 205, 0.88)",
3197
+ starGlow: "#ffe2ad",
3198
+ fontFamily: '"Segoe UI Variable", "Bahnschrift", sans-serif',
3199
+ displayFont: '"Bahnschrift", "Segoe UI Variable", sans-serif'
3200
+ }
3201
+ };
3202
+ function resolveTheme(theme) {
3203
+ if (!theme) {
3204
+ return THEME_PRESETS.atlas;
3205
+ }
3206
+ if (typeof theme === "string") {
3207
+ return THEME_PRESETS[theme] ?? THEME_PRESETS.atlas;
3208
+ }
3209
+ return {
3210
+ ...THEME_PRESETS.atlas,
3211
+ ...theme
3212
+ };
3213
+ }
3214
+ function resolveLayers(layers) {
3215
+ return {
3216
+ ...DEFAULT_LAYERS,
3217
+ ...layers
3218
+ };
3219
+ }
3220
+
3221
+ // packages/viewer/dist/atlas-state.js
3222
+ function normalizeViewerFilter(filter) {
3223
+ if (!filter) {
3224
+ return null;
3225
+ }
3226
+ const normalized = {
3227
+ query: filter.query?.trim() || void 0,
3228
+ objectTypes: dedupeList(filter.objectTypes ?? []),
3229
+ tags: dedupeList((filter.tags ?? []).map((tag) => tag.trim()).filter(Boolean)),
3230
+ groupIds: dedupeList((filter.groupIds ?? []).map((groupId) => groupId.trim()).filter(Boolean)),
3231
+ includeAncestors: filter.includeAncestors ?? true
3232
+ };
3233
+ return isViewerFilterActive(normalized) ? normalized : null;
3234
+ }
3235
+ function isViewerFilterActive(filter) {
3236
+ return Boolean(filter && (filter.query?.trim() || filter.objectTypes?.length || filter.tags?.length || filter.groupIds?.length));
3237
+ }
3238
+ function computeVisibleObjectIds(scene, filter) {
3239
+ const normalized = normalizeViewerFilter(filter);
3240
+ const visible = /* @__PURE__ */ new Set();
3241
+ for (const object of scene.objects) {
3242
+ if (object.hidden) {
3243
+ continue;
3244
+ }
3245
+ if (matchesObjectFilter(object, normalized)) {
3246
+ visible.add(object.objectId);
3247
+ if (normalized?.includeAncestors !== false) {
3248
+ for (const ancestorId of object.ancestorIds) {
3249
+ visible.add(ancestorId);
3250
+ }
3251
+ }
3252
+ }
3253
+ }
3254
+ if (!normalized) {
3255
+ return new Set(scene.objects.filter((object) => !object.hidden).map((object) => object.objectId));
3256
+ }
3257
+ return visible;
3258
+ }
3259
+ function matchesObjectFilter(object, filter) {
3260
+ if (!filter) {
3261
+ return true;
3262
+ }
3263
+ if (filter.objectTypes?.length && !filter.objectTypes.includes(object.object.type)) {
3264
+ return false;
3265
+ }
3266
+ if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
3267
+ return false;
3268
+ }
3269
+ if (filter.tags?.length) {
3270
+ const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
3271
+ if (!filter.tags.every((tag) => objectTags.includes(tag))) {
3272
+ return false;
3273
+ }
3274
+ }
3275
+ if (filter.query?.trim()) {
3276
+ const haystack = buildSearchText(object.object, object.label).toLowerCase();
3277
+ const tokens = filter.query.toLowerCase().split(/\s+/).filter(Boolean);
3278
+ if (!tokens.every((token) => haystack.includes(token))) {
3279
+ return false;
3280
+ }
3281
+ }
3282
+ return true;
3283
+ }
3284
+ function buildSearchText(object, label) {
3285
+ const infoValues = Object.values(object.info);
3286
+ const propertyValues = Object.values(object.properties).flatMap((value) => {
3287
+ if (Array.isArray(value)) {
3288
+ return value;
3289
+ }
3290
+ if (typeof value === "object" && value && "value" in value) {
3291
+ return [String(value.value), String(value.unit ?? "")];
3292
+ }
3293
+ return [String(value)];
3294
+ }).filter(Boolean);
3295
+ return [
3296
+ object.id,
3297
+ label,
3298
+ object.type,
3299
+ ...propertyValues,
3300
+ ...infoValues
3301
+ ].join(" ");
3302
+ }
3303
+ function dedupeList(values) {
3304
+ return [...new Set(values)];
3305
+ }
3306
+
3307
+ // packages/viewer/dist/render.js
3308
+ var WORLD_LAYER_ID = "worldorbit-camera-root";
3309
+ function renderSceneToSvg(scene, options = {}) {
3310
+ const theme = resolveTheme(options.theme);
3311
+ const presetDefaults = resolveRenderPreset(options.preset ?? scene.renderPreset ?? void 0);
3312
+ const layers = resolveLayers({
3313
+ ...presetDefaults.layers,
3314
+ ...options.layers
3315
+ });
3316
+ const subtitle = options.subtitle ?? scene.subtitle;
3317
+ const visibleObjectIds = computeVisibleObjectIds(scene, options.filter ?? null);
3318
+ const visibleObjects = scene.objects.filter((object) => !object.hidden).filter((object) => visibleObjectIds.has(object.objectId)).filter((object) => layers.structures || !isStructureLike(object.object)).sort((left, right) => left.sortKey - right.sortKey);
3319
+ const visibleLabels = scene.labels.filter((label) => !label.hidden).filter((label) => visibleObjectIds.has(label.objectId)).filter((label) => layers.structures || !isStructureLike(label.object));
3320
+ const imageDefinitions = buildImageDefinitions(visibleObjects);
3321
+ const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
3322
+ const leaderMarkup = layers.guides ? scene.leaders.filter((leader) => !leader.hidden).filter((leader) => visibleObjectIds.has(leader.objectId)).filter((leader) => layers.structures || !isStructureLike(leader.object)).map((leader) => `<line class="wo-leader wo-leader-${leader.mode}" x1="${leader.x1}" y1="${leader.y1}" x2="${leader.x2}" y2="${leader.y2}" data-render-id="${escapeXml(leader.renderId)}" data-group-id="${escapeAttribute(leader.groupId ?? "")}" />`).join("") : "";
3323
+ const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
3324
+ const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
3325
+ const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
3326
+ <text class="wo-subtitle" x="56" y="88">${escapeXml(subtitle)}</text>
3327
+ <text class="wo-meta" x="56" y="${scene.height - 42}">${escapeXml(renderMetadata(scene))}</text>` : "";
3328
+ const backgroundMarkup = layers.background ? `<rect class="wo-bg" x="0" y="0" width="${scene.width}" height="${scene.height}" rx="28" ry="28" />
3329
+ <rect class="wo-bg-glow" x="0" y="0" width="${scene.width}" height="${scene.height}" rx="28" ry="28" />
3330
+ ${layers.guides ? renderBackdrop(scene.width, scene.height) : ""}` : "";
3331
+ return `<svg xmlns="http://www.w3.org/2000/svg" data-worldorbit-svg="true" width="${scene.width}" height="${scene.height}" viewBox="0 0 ${scene.width} ${scene.height}" preserveAspectRatio="xMidYMid meet" role="img" aria-labelledby="worldorbit-title worldorbit-desc">
3332
+ <title id="worldorbit-title">${escapeXml(scene.title)}</title>
3333
+ <desc id="worldorbit-desc">A ${escapeXml(scene.viewMode)} WorldOrbit render with ${visibleObjects.length} visible objects.</desc>
3334
+ <defs>
3335
+ <linearGradient id="wo-bg" x1="0%" y1="0%" x2="100%" y2="100%">
3336
+ <stop offset="0%" stop-color="${theme.backgroundStart}" />
3337
+ <stop offset="100%" stop-color="${theme.backgroundEnd}" />
3338
+ </linearGradient>
3339
+ <radialGradient id="wo-star-glow" cx="50%" cy="50%" r="50%">
3340
+ <stop offset="0%" stop-color="${theme.starGlow}" stop-opacity="0.95" />
3341
+ <stop offset="100%" stop-color="${theme.starCore}" stop-opacity="0" />
3342
+ </radialGradient>
3343
+ <radialGradient id="wo-bg-glow" cx="20%" cy="10%" r="90%">
3344
+ <stop offset="0%" stop-color="${theme.backgroundGlow}" />
3345
+ <stop offset="100%" stop-color="transparent" />
3346
+ </radialGradient>
3347
+ ${imageDefinitions}
3348
+ </defs>
3349
+ <style>
3350
+ .wo-bg { fill: url(#wo-bg); }
3351
+ .wo-bg-glow { fill: url(#wo-bg-glow); }
3352
+ .wo-grid { fill: none; stroke: ${theme.guide}; stroke-width: 1; }
3353
+ .wo-orbit { fill: none; stroke: ${theme.orbit}; stroke-width: 1.5; }
3354
+ .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
3355
+ .wo-orbit-front { opacity: 0.9; }
3356
+ .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
3357
+ .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
3358
+ .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
3359
+ .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
3360
+ .wo-title { fill: ${theme.ink}; font: 700 24px ${theme.displayFont}; letter-spacing: 0.06em; text-transform: uppercase; }
3361
+ .wo-subtitle { fill: ${theme.muted}; font: 500 12px ${theme.fontFamily}; letter-spacing: 0.14em; text-transform: uppercase; }
3362
+ .wo-meta { fill: ${theme.muted}; font: 500 11px ${theme.fontFamily}; letter-spacing: 0.06em; }
3363
+ .wo-object { cursor: pointer; outline: none; }
3364
+ .wo-object:focus-visible .wo-selection-ring,
3365
+ .wo-object:hover .wo-selection-ring,
3366
+ .wo-object-selected .wo-selection-ring { display: block; }
3367
+ .wo-object-selected .wo-selection-ring { stroke: ${theme.selected}; }
3368
+ .wo-object-label-selected .wo-label { fill: ${theme.accent}; }
3369
+ .wo-object-label-selected .wo-label-secondary { fill: ${theme.selected}; }
3370
+ .wo-chain-selected .wo-selection-ring,
3371
+ .wo-chain-hover .wo-selection-ring { display: block; }
3372
+ .wo-ancestor-selected .wo-selection-ring,
3373
+ .wo-ancestor-hover .wo-selection-ring { display: block; opacity: 0.66; }
3374
+ .wo-chain-selected .wo-label,
3375
+ .wo-chain-hover .wo-label { fill: ${theme.accent}; }
3376
+ .wo-ancestor-selected .wo-label,
3377
+ .wo-ancestor-hover .wo-label { fill: ${theme.ink}; opacity: 0.82; }
3378
+ .wo-orbit-related-selected,
3379
+ .wo-orbit-related-hover { stroke: ${theme.accentStrong}; opacity: 1; }
3380
+ .wo-selection-ring { display: none; fill: none; stroke-width: 2; stroke-dasharray: 6 5; }
3381
+ .wo-object-image { pointer-events: none; }
3382
+ </style>
3383
+ ${backgroundMarkup}
3384
+ ${metadataMarkup}
3385
+ <g data-worldorbit-world="true">
3386
+ <g data-worldorbit-camera-root="${WORLD_LAYER_ID}" id="${WORLD_LAYER_ID}">
3387
+ <g data-worldorbit-world-content="true">
3388
+ ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
3389
+ ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
3390
+ ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
3391
+ ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
3392
+ ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
3393
+ </g>
3394
+ </g>
3395
+ </g>
3396
+ </svg>`;
3397
+ }
3398
+ function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
3399
+ const backParts = [];
3400
+ const frontParts = [];
3401
+ for (const visual of scene.orbitVisuals.filter((entry) => !entry.hidden && visibleObjectIds.has(entry.objectId) && (includeStructures || !isStructureLike(entry.object)))) {
3402
+ const strokeWidth = visual.bandThickness ?? (visual.band ? 10 : 1.5);
3403
+ const orbitClass = visual.band ? "wo-orbit wo-orbit-band wo-orbit-node" : "wo-orbit wo-orbit-node";
3404
+ const dataAttributes = `data-render-id="${escapeXml(visual.renderId)}" data-orbit-object-id="${escapeAttribute(visual.objectId)}" data-parent-id="${escapeAttribute(visual.parentId)}" data-group-id="${escapeAttribute(visual.groupId ?? "")}"`;
3405
+ if (visual.backArcPath || visual.frontArcPath) {
3406
+ if (visual.backArcPath) {
3407
+ backParts.push(`<path class="${orbitClass} wo-orbit-back" d="${visual.backArcPath}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
3408
+ }
3409
+ if (visual.frontArcPath) {
3410
+ frontParts.push(`<path class="${orbitClass} wo-orbit-front" d="${visual.frontArcPath}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
3411
+ }
3412
+ continue;
3413
+ }
3414
+ if (visual.kind === "ellipse") {
3415
+ const rx = visual.rx ?? visual.radius ?? 0;
3416
+ const ry = visual.ry ?? visual.radius ?? 0;
3417
+ frontParts.push(`<ellipse class="${orbitClass} wo-orbit-front" cx="${visual.cx}" cy="${visual.cy}" rx="${rx}" ry="${ry}" transform="rotate(${visual.rotationDeg} ${visual.cx} ${visual.cy})" stroke-width="${strokeWidth}" ${dataAttributes} />`);
3418
+ continue;
3419
+ }
3420
+ frontParts.push(`<circle class="${orbitClass} wo-orbit-front" cx="${visual.cx}" cy="${visual.cy}" r="${visual.radius ?? 0}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
3421
+ }
3422
+ return {
3423
+ back: backParts.join(""),
3424
+ front: frontParts.join("")
3425
+ };
3426
+ }
3427
+ function buildImageDefinitions(objects) {
3428
+ return objects.filter((object) => Boolean(object.imageHref)).map((object) => renderImageClipPath(object)).join("");
3429
+ }
3430
+ function renderSceneObject(sceneObject, selectedObjectId, theme) {
3431
+ const { object, x, y, radius, visualRadius } = sceneObject;
3432
+ const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
3433
+ const palette = resolveObjectPalette(sceneObject, theme);
3434
+ const imageMarkup = renderObjectImage(sceneObject);
3435
+ const outlineMarkup = imageMarkup ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true }) : "";
3436
+ return `<g class="wo-object wo-object-${object.type}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
3437
+ <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
3438
+ ${renderAtmosphere(sceneObject, palette)}
3439
+ ${renderObjectBody(object, x, y, radius, palette)}
3440
+ ${imageMarkup}
3441
+ ${outlineMarkup}
3442
+ </g>`;
3443
+ }
3444
+ function renderSceneLabel(scene, label, selectedObjectId) {
3445
+ const selectionClass = selectedObjectId === label.objectId ? " wo-object-label-selected" : "";
3446
+ const labelScale = scene.scaleModel.labelMultiplier;
3447
+ return `<g class="wo-object-label${selectionClass}" data-object-id="${escapeXml(label.objectId)}" data-group-id="${escapeAttribute(label.groupId ?? "")}" data-render-id="${escapeXml(label.renderId)}">
3448
+ <text class="wo-label" x="${label.x}" y="${label.y}" text-anchor="${label.textAnchor}" font-size="${14 * labelScale}">${escapeXml(label.label)}</text>
3449
+ <text class="wo-label-secondary" x="${label.x}" y="${label.secondaryY}" text-anchor="${label.textAnchor}" font-size="${11 * labelScale}">${escapeXml(label.secondaryLabel)}</text>
3450
+ </g>`;
3451
+ }
3452
+ function renderObjectBody(object, x, y, radius, palette, options = {}) {
3453
+ const fill = options.outlineOnly ? "transparent" : palette.fill;
3454
+ const tail = palette.tail ?? palette.fill;
3455
+ switch (object.type) {
3456
+ case "star":
3457
+ return options.outlineOnly ? `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="2" />` : `<circle cx="${x}" cy="${y}" r="${radius * 2.4}" fill="${palette.glow ?? "url(#wo-star-glow)"}" opacity="0.84" />
3458
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
3459
+ case "planet":
3460
+ case "moon":
3461
+ case "belt":
3462
+ case "asteroid":
3463
+ case "ring":
3464
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="${options.outlineOnly ? 1.5 : 1.4}" />`;
3465
+ case "comet":
3466
+ return options.outlineOnly ? `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />` : `<path d="M ${x - radius * 2} ${y + radius * 1.3} Q ${x - radius * 0.7} ${y + radius * 0.3} ${x - radius * 0.45} ${y}" fill="none" stroke="${tail}" stroke-width="${Math.max(2, radius * 0.8)}" stroke-linecap="round" opacity="0.85" />
3467
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3468
+ case "structure":
3469
+ return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3470
+ case "phenomenon":
3471
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3472
+ }
3473
+ }
3474
+ function renderAtmosphere(sceneObject, palette) {
3475
+ if (!palette.atmosphere) {
3476
+ return "";
3477
+ }
3478
+ const { object, x, y, radius } = sceneObject;
3479
+ if (object.type !== "planet" && object.type !== "moon" && object.type !== "asteroid") {
3480
+ return "";
3481
+ }
3482
+ return `<circle cx="${x}" cy="${y}" r="${radius + 4}" fill="none" stroke="${palette.atmosphere}" stroke-width="4" opacity="0.45" />`;
3483
+ }
3484
+ function renderObjectImage(sceneObject) {
3485
+ if (!sceneObject.imageHref) {
3486
+ return "";
3487
+ }
3488
+ const bounds = imageBoundsForObject(sceneObject);
3489
+ return `<image class="wo-object-image" x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" href="${escapeAttribute(sceneObject.imageHref)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${escapeAttribute(getObjectClipPathId(sceneObject))})" />`;
3490
+ }
3491
+ function renderImageClipPath(sceneObject) {
3492
+ const { x, y, radius } = sceneObject;
3493
+ let content = "";
3494
+ switch (sceneObject.object.type) {
3495
+ case "star":
3496
+ case "planet":
3497
+ case "moon":
3498
+ case "asteroid":
3499
+ case "comet":
3500
+ content = `<circle cx="${x}" cy="${y}" r="${radius}" />`;
3501
+ break;
3502
+ case "structure":
3503
+ content = `<polygon points="${diamondPoints(x, y, radius)}" />`;
3504
+ break;
3505
+ case "phenomenon":
3506
+ content = `<polygon points="${phenomenonPoints(x, y, radius)}" />`;
3507
+ break;
3508
+ default:
3509
+ return "";
3510
+ }
3511
+ return `<clipPath id="${escapeAttribute(getObjectClipPathId(sceneObject))}" clipPathUnits="userSpaceOnUse">${content}</clipPath>`;
3512
+ }
3513
+ function imageBoundsForObject(sceneObject) {
3514
+ const { object, x, y, radius } = sceneObject;
3515
+ switch (object.type) {
3516
+ case "structure":
3517
+ return {
3518
+ x: x - radius,
3519
+ y: y - radius,
3520
+ width: radius * 2,
3521
+ height: radius * 2
3522
+ };
3523
+ case "phenomenon":
3524
+ return {
3525
+ x: x - radius * 1.2,
3526
+ y: y - radius * 1.2,
3527
+ width: radius * 2.4,
3528
+ height: radius * 2.4
3529
+ };
3530
+ default:
3531
+ return {
3532
+ x: x - radius,
3533
+ y: y - radius,
3534
+ width: radius * 2,
3535
+ height: radius * 2
3536
+ };
3537
+ }
3538
+ }
3539
+ function resolveObjectPalette(sceneObject, theme) {
3540
+ const base = basePaletteForType(sceneObject.object.type, theme);
3541
+ const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor) ? sceneObject.fillColor : base.fill;
3542
+ const albedo = numericValue(sceneObject.object.properties.albedo);
3543
+ const temperature = numericValue(sceneObject.object.properties.temperature);
3544
+ const fill = applyTemperatureAndAlbedo(customFill, temperature, albedo, sceneObject.object.type);
3545
+ const stroke = mixColors(fill, "#ffffff", 0.4) ?? base.stroke;
3546
+ const atmosphere = atmosphereColor(sceneObject.object.properties.atmosphere);
3547
+ const glow = sceneObject.object.type === "star" ? rgbaString(mixColors(fill, "#fff2cb", 0.4) ?? fill, 0.82) : void 0;
3548
+ return {
3549
+ fill,
3550
+ stroke,
3551
+ glow,
3552
+ atmosphere,
3553
+ tail: sceneObject.object.type === "comet" ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72) : void 0
3554
+ };
3555
+ }
3556
+ function basePaletteForType(type, theme) {
3557
+ switch (type) {
3558
+ case "star":
3559
+ return {
3560
+ fill: theme.starCore,
3561
+ stroke: theme.starStroke
3562
+ };
3563
+ case "planet":
3564
+ return { fill: "#72b7ff", stroke: "#d8efff" };
3565
+ case "moon":
3566
+ return { fill: "#c7d7ea", stroke: "#f2f8ff" };
3567
+ case "belt":
3568
+ return { fill: "#d9aa74", stroke: "#f7d5aa" };
3569
+ case "asteroid":
3570
+ return { fill: "#a7a5b8", stroke: "#ebe5ff" };
3571
+ case "comet":
3572
+ return { fill: "#9ce7ff", stroke: "#e7fbff" };
3573
+ case "ring":
3574
+ return { fill: "#e59f7d", stroke: "#fff0d3" };
3575
+ case "structure":
3576
+ return { fill: theme.accentStrong, stroke: "#fff2ea" };
3577
+ case "phenomenon":
3578
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
3579
+ }
3580
+ }
3581
+ function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
3582
+ let nextColor = baseColor;
3583
+ if (temperature !== null) {
3584
+ const tint = temperatureTint(temperature, type);
3585
+ nextColor = mixColors(nextColor, tint, type === "star" ? 0.42 : 0.2) ?? nextColor;
3586
+ }
3587
+ if (albedo !== null) {
3588
+ const clamped = Math.min(Math.max(albedo, 0), 1);
3589
+ nextColor = mixColors(nextColor, clamped >= 0.5 ? "#ffffff" : "#0f1520", Math.abs(clamped - 0.5) * 0.7) ?? nextColor;
3590
+ }
3591
+ return nextColor;
3592
+ }
3593
+ function temperatureTint(value, type) {
3594
+ if (type === "star") {
3595
+ if (value >= 8e3)
3596
+ return "#d7ebff";
3597
+ if (value >= 6e3)
3598
+ return "#fff3d8";
3599
+ if (value >= 4e3)
3600
+ return "#ffd39a";
3601
+ return "#ff9d76";
3602
+ }
3603
+ if (value <= 180)
3604
+ return "#8fd8ff";
3605
+ if (value >= 600)
3606
+ return "#ffb56e";
3607
+ return "#fff1c4";
3608
+ }
3609
+ function atmosphereColor(value) {
3610
+ if (typeof value !== "string" || !value.trim()) {
3611
+ return void 0;
3612
+ }
3613
+ const normalized = value.trim().toLowerCase();
3614
+ if (normalized.includes("methane"))
3615
+ return "rgba(134, 236, 255, 0.75)";
3616
+ if (normalized.includes("nitrogen") || normalized.includes("oxygen") || normalized.includes("air")) {
3617
+ return "rgba(122, 194, 255, 0.75)";
3618
+ }
3619
+ if (normalized.includes("carbon") || normalized.includes("co2")) {
3620
+ return "rgba(255, 198, 138, 0.75)";
3621
+ }
3622
+ if (normalized.includes("sulfur")) {
3623
+ return "rgba(255, 233, 138, 0.75)";
3624
+ }
3625
+ return "rgba(180, 222, 255, 0.68)";
3626
+ }
3627
+ function resolveRenderPreset(preset) {
3628
+ switch (preset) {
3629
+ case "presentation":
3630
+ return {
3631
+ layers: {
3632
+ background: true,
3633
+ guides: true,
3634
+ orbits: true,
3635
+ objects: true,
3636
+ labels: true,
3637
+ structures: true,
3638
+ metadata: true
3639
+ }
3640
+ };
3641
+ case "atlas-card":
3642
+ return {
3643
+ layers: {
3644
+ background: true,
3645
+ guides: false,
3646
+ orbits: true,
3647
+ objects: true,
3648
+ labels: true,
3649
+ structures: true,
3650
+ metadata: true
3651
+ }
3652
+ };
3653
+ case "markdown":
3654
+ return {
3655
+ layers: {
3656
+ background: true,
3657
+ guides: false,
3658
+ orbits: true,
3659
+ objects: true,
3660
+ labels: true,
3661
+ structures: true,
3662
+ metadata: false
3663
+ }
3664
+ };
3665
+ case "diagram":
3666
+ default:
3667
+ return {
3668
+ layers: {
3669
+ background: true,
3670
+ guides: true,
3671
+ orbits: true,
3672
+ objects: true,
3673
+ labels: true,
3674
+ structures: true,
3675
+ metadata: true
3676
+ }
3677
+ };
3678
+ }
3679
+ }
3680
+ function numericValue(value) {
3681
+ if (typeof value === "number") {
3682
+ return value;
3683
+ }
3684
+ if (value && typeof value === "object" && "value" in value) {
3685
+ return value.value;
3686
+ }
3687
+ return null;
3688
+ }
3689
+ function getObjectClipPathId(sceneObject) {
3690
+ return `${sceneObject.renderId}-clip`;
3691
+ }
3692
+ function diamondPoints(x, y, radius) {
3693
+ return `${x},${y - radius} ${x + radius},${y} ${x},${y + radius} ${x - radius},${y}`;
3694
+ }
3695
+ function phenomenonPoints(x, y, radius) {
3696
+ return `${x},${y - radius * 1.2} ${x + radius * 0.9},${y - radius * 0.3} ${x + radius * 1.2},${y + radius * 0.8} ${x},${y + radius * 1.2} ${x - radius * 1.1},${y + radius * 0.3} ${x - radius * 0.8},${y - radius * 0.8}`;
3697
+ }
3698
+ function renderBackdrop(width, height) {
3699
+ const verticalLines = Array.from({ length: 10 }, (_, index) => {
3700
+ const x = 90 + index * ((width - 180) / 9);
3701
+ return `<line class="wo-grid" x1="${x}" y1="120" x2="${x}" y2="${height - 70}" />`;
3702
+ }).join("");
3703
+ const horizontalLines = Array.from({ length: 6 }, (_, index) => {
3704
+ const y = 150 + index * ((height - 240) / 5);
3705
+ return `<line class="wo-grid" x1="56" y1="${y}" x2="${width - 56}" y2="${y}" />`;
3706
+ }).join("");
3707
+ return `${verticalLines}${horizontalLines}`;
3708
+ }
3709
+ function renderMetadata(scene) {
3710
+ return [
3711
+ `${scene.layoutPreset} layout`,
3712
+ `${scene.viewMode} view`,
3713
+ `${scene.renderPreset ?? "custom"} preset`,
3714
+ `schema ${scene.metadata.version ?? "1.0"}`
3715
+ ].join(" - ");
3716
+ }
3717
+ function isStructureLike(object) {
3718
+ return object.type === "structure" || object.type === "phenomenon";
3719
+ }
3720
+ function mixColors(left, right, amount) {
3721
+ const leftRgb = parseColor(left);
3722
+ const rightRgb = parseColor(right);
3723
+ if (!leftRgb || !rightRgb) {
3724
+ return void 0;
3725
+ }
3726
+ const clamped = Math.min(Math.max(amount, 0), 1);
3727
+ return rgbToHex({
3728
+ r: Math.round(leftRgb.r + (rightRgb.r - leftRgb.r) * clamped),
3729
+ g: Math.round(leftRgb.g + (rightRgb.g - leftRgb.g) * clamped),
3730
+ b: Math.round(leftRgb.b + (rightRgb.b - leftRgb.b) * clamped)
3731
+ });
3732
+ }
3733
+ function rgbaString(color, alpha) {
3734
+ const rgb = parseColor(color);
3735
+ if (!rgb) {
3736
+ return color;
3737
+ }
3738
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
3739
+ }
3740
+ function parseColor(value) {
3741
+ const hex = value.trim();
3742
+ if (/^#[0-9a-f]{6}$/i.test(hex)) {
3743
+ return {
3744
+ r: Number.parseInt(hex.slice(1, 3), 16),
3745
+ g: Number.parseInt(hex.slice(3, 5), 16),
3746
+ b: Number.parseInt(hex.slice(5, 7), 16)
3747
+ };
3748
+ }
3749
+ if (/^#[0-9a-f]{3}$/i.test(hex)) {
3750
+ return {
3751
+ r: Number.parseInt(hex[1] + hex[1], 16),
3752
+ g: Number.parseInt(hex[2] + hex[2], 16),
3753
+ b: Number.parseInt(hex[3] + hex[3], 16)
3754
+ };
3755
+ }
3756
+ return null;
3757
+ }
3758
+ function rgbToHex(color) {
3759
+ const part = (value) => value.toString(16).padStart(2, "0");
3760
+ return `#${part(color.r)}${part(color.g)}${part(color.b)}`;
3761
+ }
3762
+ function isColorLike(value) {
3763
+ return Boolean(value.trim());
3764
+ }
3765
+ function escapeXml(value) {
3766
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
3767
+ }
3768
+ function escapeAttribute(value) {
3769
+ return escapeXml(value);
3770
+ }
3771
+
3772
+ // packages/viewer/dist/embed.js
3773
+ function serializeWorldOrbitEmbedPayload(payload) {
3774
+ return encodeURIComponent(JSON.stringify(payload));
3775
+ }
3776
+ function createEmbedPayload(scene, mode, options = {}) {
3777
+ return {
3778
+ version: "2.0",
3779
+ mode,
3780
+ scene,
3781
+ options: {
3782
+ initialViewpointId: options.initialViewpointId,
3783
+ initialSelectionObjectId: options.initialSelectionObjectId,
3784
+ initialFilter: options.initialFilter ?? null,
3785
+ atlasState: options.atlasState ?? null,
3786
+ minimap: options.minimap
3787
+ }
3788
+ };
3789
+ }
3790
+ function createWorldOrbitEmbedMarkup(payload, options = {}) {
3791
+ const mergedPayload = {
3792
+ ...payload,
3793
+ options: {
3794
+ ...payload.options,
3795
+ theme: options.theme ?? payload.options?.theme,
3796
+ layers: options.layers ?? payload.options?.layers,
3797
+ subtitle: options.subtitle ?? payload.options?.subtitle,
3798
+ preset: options.preset ?? payload.options?.preset,
3799
+ initialViewpointId: options.initialViewpointId ?? payload.options?.initialViewpointId,
3800
+ initialSelectionObjectId: options.initialSelectionObjectId ?? payload.options?.initialSelectionObjectId,
3801
+ initialFilter: options.initialFilter ?? payload.options?.initialFilter ?? null,
3802
+ atlasState: options.atlasState ?? payload.options?.atlasState ?? null,
3803
+ minimap: options.minimap ?? payload.options?.minimap
3804
+ }
3805
+ };
3806
+ const html = renderSceneToSvg(payload.scene, {
3807
+ theme: mergedPayload.options?.theme,
3808
+ layers: mergedPayload.options?.layers,
3809
+ filter: mergedPayload.options?.initialFilter ?? null,
3810
+ selectedObjectId: mergedPayload.options?.initialSelectionObjectId ?? null,
3811
+ subtitle: mergedPayload.options?.subtitle,
3812
+ preset: mergedPayload.options?.preset
3813
+ });
3814
+ return `<div class="${escapeAttribute2(options.className ?? "worldorbit-embed")}" data-worldorbit-embed="true" data-worldorbit-mode="${payload.mode}" data-worldorbit-preset="${escapeAttribute2(mergedPayload.options?.preset ?? payload.scene.renderPreset ?? "custom")}" data-worldorbit-viewpoint="${escapeAttribute2(mergedPayload.options?.initialViewpointId ?? "")}" data-worldorbit-payload="${escapeAttribute2(serializeWorldOrbitEmbedPayload(mergedPayload))}">${html}</div>`;
3815
+ }
3816
+ function escapeAttribute2(value) {
3817
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
3818
+ }
3819
+
3820
+ // packages/markdown/dist/html.js
3821
+ function renderWorldOrbitBlock(source, options = {}) {
3822
+ try {
3823
+ const loaded = loadWorldOrbitSource(source);
3824
+ const scene = renderDocumentToScene(loaded.document, resolveSourceRenderOptions(loaded, options));
3825
+ if ((options.mode ?? "static") === "interactive") {
3826
+ return createWorldOrbitEmbedMarkup(createEmbedPayload(scene, "interactive", {
3827
+ initialViewpointId: options.initialViewpointId,
3828
+ initialSelectionObjectId: options.initialSelectionObjectId,
3829
+ initialFilter: options.initialFilter ?? null,
3830
+ atlasState: options.atlasState ?? null,
3831
+ minimap: options.minimap
3832
+ }), {
3833
+ className: options.className ?? "worldorbit-block worldorbit-interactive",
3834
+ theme: options.theme,
3835
+ layers: options.layers,
3836
+ subtitle: options.subtitle,
3837
+ preset: options.preset,
3838
+ initialViewpointId: options.initialViewpointId,
3839
+ initialSelectionObjectId: options.initialSelectionObjectId,
3840
+ initialFilter: options.initialFilter ?? null,
3841
+ atlasState: options.atlasState ?? null,
3842
+ minimap: options.minimap
3843
+ });
3844
+ }
3845
+ return `<figure class="${escapeAttribute3(options.className ?? "worldorbit-block worldorbit-static")}">${renderSceneToSvg(scene, options)}</figure>`;
3846
+ } catch (error) {
3847
+ if (options.strict) {
3848
+ throw error;
3849
+ }
3850
+ return renderWorldOrbitError(error instanceof Error ? error.message : String(error));
3851
+ }
3852
+ }
3853
+ function renderWorldOrbitError(message) {
3854
+ return `<pre class="worldorbit-error">WorldOrbit error: ${escapeHtml(message)}</pre>`;
3855
+ }
3856
+ function escapeHtml(value) {
3857
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3858
+ }
3859
+ function escapeAttribute3(value) {
3860
+ return escapeHtml(value).replaceAll('"', "&quot;");
3861
+ }
3862
+ function resolveSourceRenderOptions(loaded, options) {
3863
+ const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
3864
+ if (options.preset || !atlasDocument?.system?.defaults.preset) {
3865
+ return options;
3866
+ }
3867
+ return {
3868
+ ...options,
3869
+ preset: atlasDocument.system.defaults.preset
3870
+ };
3871
+ }
3872
+
3873
+ // packages/markdown/dist/remark.js
3874
+ function remarkWorldOrbit(options = {}) {
3875
+ return function transform(tree) {
3876
+ visitMdNodes(tree, (node, index, parent) => {
3877
+ if (!parent || index === -1 || node.type !== "code" || !isWorldOrbitLanguage(node.lang)) {
3878
+ return;
3879
+ }
3880
+ parent.children[index] = {
3881
+ type: "html",
3882
+ value: renderWorldOrbitBlock(node.value ?? "", options)
3883
+ };
3884
+ });
3885
+ };
3886
+ }
3887
+ function visitMdNodes(node, visitor, parent = null) {
3888
+ const children = node.children ?? [];
3889
+ for (let index = 0; index < children.length; index += 1) {
3890
+ const child = children[index];
3891
+ visitor(child, index, node);
3892
+ visitMdNodes(child, visitor, node);
3893
+ }
3894
+ if (!parent) {
3895
+ visitor(node, -1, null);
3896
+ }
3897
+ }
3898
+ function isWorldOrbitLanguage(language) {
3899
+ return (language ?? "").trim().toLowerCase() === "worldorbit";
3900
+ }
3901
+
3902
+ // packages/markdown/dist/rehype.js
3903
+ function rehypeWorldOrbit(options = {}) {
3904
+ return function transform(tree) {
3905
+ visitHastNodes(tree, (node, index, parent) => {
3906
+ if (!parent || index === -1 || node.type !== "element" || node.tagName !== "pre") {
3907
+ return;
3908
+ }
3909
+ const codeElement = node.children?.[0];
3910
+ if (!codeElement || codeElement.type !== "element" || codeElement.tagName !== "code") {
3911
+ return;
3912
+ }
3913
+ const classNames = normalizeClassNames(codeElement.properties?.className);
3914
+ if (!classNames.includes("language-worldorbit")) {
3915
+ return;
3916
+ }
3917
+ const source = collectText(codeElement);
3918
+ parent.children[index] = {
3919
+ type: "raw",
3920
+ value: renderWorldOrbitBlock(source, options)
3921
+ };
3922
+ });
3923
+ };
3924
+ }
3925
+ function visitHastNodes(node, visitor, parent = null) {
3926
+ const children = node.children ?? [];
3927
+ for (let index = 0; index < children.length; index += 1) {
3928
+ const child = children[index];
3929
+ visitor(child, index, node);
3930
+ visitHastNodes(child, visitor, node);
3931
+ }
3932
+ if (!parent) {
3933
+ visitor(node, -1, null);
3934
+ }
3935
+ }
3936
+ function normalizeClassNames(value) {
3937
+ if (Array.isArray(value)) {
3938
+ return value.map((entry) => String(entry));
3939
+ }
3940
+ if (typeof value === "string") {
3941
+ return value.split(/\s+/).filter(Boolean);
3942
+ }
3943
+ return [];
3944
+ }
3945
+ function collectText(node) {
3946
+ if (node.type === "text") {
3947
+ return node.value ?? "";
3948
+ }
3949
+ return (node.children ?? []).map((child) => collectText(child)).join("");
3950
+ }
3951
+ })();