worldorbit 2.6.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +20 -9
  2. package/dist/browser/core/dist/atlas-edit.d.ts +11 -0
  3. package/dist/browser/core/dist/atlas-edit.js +347 -0
  4. package/dist/browser/core/dist/atlas-utils.d.ts +22 -0
  5. package/dist/browser/core/dist/atlas-utils.js +189 -0
  6. package/dist/browser/core/dist/atlas-validate.d.ts +2 -0
  7. package/dist/browser/core/dist/atlas-validate.js +488 -0
  8. package/dist/browser/core/dist/diagnostics.d.ts +10 -0
  9. package/dist/browser/core/dist/diagnostics.js +109 -0
  10. package/dist/browser/core/dist/draft-parse.d.ts +3 -0
  11. package/dist/browser/core/dist/draft-parse.js +1654 -0
  12. package/dist/browser/core/dist/draft.d.ts +21 -0
  13. package/dist/browser/core/dist/draft.js +482 -0
  14. package/dist/browser/core/dist/errors.d.ts +7 -0
  15. package/dist/browser/core/dist/errors.js +16 -0
  16. package/dist/browser/core/dist/format.d.ts +4 -0
  17. package/dist/browser/core/dist/format.js +613 -0
  18. package/dist/browser/core/dist/index.d.ts +29 -0
  19. package/dist/browser/core/dist/index.js +35 -6542
  20. package/dist/browser/core/dist/load.d.ts +4 -0
  21. package/dist/browser/core/dist/load.js +182 -0
  22. package/dist/browser/core/dist/markdown.d.ts +2 -0
  23. package/dist/browser/core/dist/markdown.js +37 -0
  24. package/dist/browser/core/dist/normalize.d.ts +2 -0
  25. package/dist/browser/core/dist/normalize.js +312 -0
  26. package/dist/browser/core/dist/parse.d.ts +2 -0
  27. package/dist/browser/core/dist/parse.js +133 -0
  28. package/dist/browser/core/dist/scene.d.ts +3 -0
  29. package/dist/browser/core/dist/scene.js +1901 -0
  30. package/dist/browser/core/dist/schema.d.ts +8 -0
  31. package/dist/browser/core/dist/schema.js +298 -0
  32. package/dist/browser/core/dist/spatial-scene.d.ts +3 -0
  33. package/dist/browser/core/dist/spatial-scene.js +420 -0
  34. package/dist/browser/core/dist/tokenize.d.ts +4 -0
  35. package/dist/browser/core/dist/tokenize.js +68 -0
  36. package/dist/browser/core/dist/types.d.ts +637 -0
  37. package/dist/browser/core/dist/types.js +1 -0
  38. package/dist/browser/core/dist/validate.d.ts +2 -0
  39. package/dist/browser/core/dist/validate.js +56 -0
  40. package/dist/browser/editor/dist/editor.d.ts +2 -0
  41. package/dist/browser/editor/dist/editor.js +3700 -0
  42. package/dist/browser/editor/dist/index.d.ts +2 -0
  43. package/dist/browser/editor/dist/index.js +1 -12250
  44. package/dist/browser/editor/dist/types.d.ts +59 -0
  45. package/dist/browser/editor/dist/types.js +1 -0
  46. package/dist/browser/markdown/dist/html.d.ts +3 -0
  47. package/dist/browser/markdown/dist/html.js +64 -0
  48. package/dist/browser/markdown/dist/index.d.ts +4 -0
  49. package/dist/browser/markdown/dist/index.js +3 -6179
  50. package/dist/browser/markdown/dist/rehype.d.ts +10 -0
  51. package/dist/browser/markdown/dist/rehype.js +49 -0
  52. package/dist/browser/markdown/dist/remark.d.ts +9 -0
  53. package/dist/browser/markdown/dist/remark.js +28 -0
  54. package/dist/browser/markdown/dist/types.d.ts +11 -0
  55. package/dist/browser/markdown/dist/types.js +1 -0
  56. package/dist/browser/viewer/dist/atlas-state.d.ts +12 -0
  57. package/dist/browser/viewer/dist/atlas-state.js +269 -0
  58. package/dist/browser/viewer/dist/atlas-viewer.d.ts +2 -0
  59. package/dist/browser/viewer/dist/atlas-viewer.js +495 -0
  60. package/dist/browser/viewer/dist/custom-element.d.ts +1 -0
  61. package/dist/browser/viewer/dist/custom-element.js +78 -0
  62. package/dist/browser/viewer/dist/embed.d.ts +24 -0
  63. package/dist/browser/viewer/dist/embed.js +172 -0
  64. package/dist/browser/viewer/dist/errors.d.ts +6 -0
  65. package/dist/browser/viewer/dist/errors.js +12 -0
  66. package/dist/browser/viewer/dist/index.d.ts +10 -0
  67. package/dist/browser/viewer/dist/index.js +9 -8334
  68. package/dist/browser/viewer/dist/minimap.d.ts +3 -0
  69. package/dist/browser/viewer/dist/minimap.js +63 -0
  70. package/dist/browser/viewer/dist/render.d.ts +6 -0
  71. package/dist/browser/viewer/dist/render.js +670 -0
  72. package/dist/browser/viewer/dist/runtime-3d.d.ts +19 -0
  73. package/dist/browser/viewer/dist/runtime-3d.js +494 -0
  74. package/dist/browser/viewer/dist/theme.d.ts +4 -0
  75. package/dist/browser/viewer/dist/theme.js +103 -0
  76. package/dist/browser/viewer/dist/tooltip.d.ts +3 -0
  77. package/dist/browser/viewer/dist/tooltip.js +198 -0
  78. package/dist/browser/viewer/dist/types.d.ts +292 -0
  79. package/dist/browser/viewer/dist/types.js +1 -0
  80. package/dist/browser/viewer/dist/vendor/three.module.js +53032 -0
  81. package/dist/browser/viewer/dist/viewer-state.d.ts +19 -0
  82. package/dist/browser/viewer/dist/viewer-state.js +162 -0
  83. package/dist/browser/viewer/dist/viewer.d.ts +2 -0
  84. package/dist/browser/viewer/dist/viewer.js +1662 -0
  85. package/dist/unpkg/core/dist/atlas-edit.d.ts +11 -0
  86. package/dist/unpkg/core/dist/atlas-edit.js +347 -0
  87. package/dist/unpkg/core/dist/atlas-utils.d.ts +22 -0
  88. package/dist/unpkg/core/dist/atlas-utils.js +189 -0
  89. package/dist/unpkg/core/dist/atlas-validate.d.ts +2 -0
  90. package/dist/unpkg/core/dist/atlas-validate.js +488 -0
  91. package/dist/unpkg/core/dist/diagnostics.d.ts +10 -0
  92. package/dist/unpkg/core/dist/diagnostics.js +109 -0
  93. package/dist/unpkg/core/dist/draft-parse.d.ts +3 -0
  94. package/dist/unpkg/core/dist/draft-parse.js +1654 -0
  95. package/dist/unpkg/core/dist/draft.d.ts +21 -0
  96. package/dist/unpkg/core/dist/draft.js +482 -0
  97. package/dist/unpkg/core/dist/errors.d.ts +7 -0
  98. package/dist/unpkg/core/dist/errors.js +16 -0
  99. package/dist/unpkg/core/dist/format.d.ts +4 -0
  100. package/dist/unpkg/core/dist/format.js +613 -0
  101. package/dist/unpkg/core/dist/index.d.ts +29 -0
  102. package/dist/unpkg/core/dist/index.js +35 -6614
  103. package/dist/unpkg/core/dist/load.d.ts +4 -0
  104. package/dist/unpkg/core/dist/load.js +182 -0
  105. package/dist/unpkg/core/dist/markdown.d.ts +2 -0
  106. package/dist/unpkg/core/dist/markdown.js +37 -0
  107. package/dist/unpkg/core/dist/normalize.d.ts +2 -0
  108. package/dist/unpkg/core/dist/normalize.js +312 -0
  109. package/dist/unpkg/core/dist/parse.d.ts +2 -0
  110. package/dist/unpkg/core/dist/parse.js +133 -0
  111. package/dist/unpkg/core/dist/scene.d.ts +3 -0
  112. package/dist/unpkg/core/dist/scene.js +1901 -0
  113. package/dist/unpkg/core/dist/schema.d.ts +8 -0
  114. package/dist/unpkg/core/dist/schema.js +298 -0
  115. package/dist/unpkg/core/dist/spatial-scene.d.ts +3 -0
  116. package/dist/unpkg/core/dist/spatial-scene.js +420 -0
  117. package/dist/unpkg/core/dist/tokenize.d.ts +4 -0
  118. package/dist/unpkg/core/dist/tokenize.js +68 -0
  119. package/dist/unpkg/core/dist/types.d.ts +637 -0
  120. package/dist/unpkg/core/dist/types.js +1 -0
  121. package/dist/unpkg/core/dist/validate.d.ts +2 -0
  122. package/dist/unpkg/core/dist/validate.js +56 -0
  123. package/dist/unpkg/editor/dist/editor.d.ts +2 -0
  124. package/dist/unpkg/editor/dist/editor.js +3700 -0
  125. package/dist/unpkg/editor/dist/index.d.ts +2 -0
  126. package/dist/unpkg/editor/dist/index.js +1 -12275
  127. package/dist/unpkg/editor/dist/types.d.ts +59 -0
  128. package/dist/unpkg/editor/dist/types.js +1 -0
  129. package/dist/unpkg/markdown/dist/html.d.ts +3 -0
  130. package/dist/unpkg/markdown/dist/html.js +64 -0
  131. package/dist/unpkg/markdown/dist/index.d.ts +4 -0
  132. package/dist/unpkg/markdown/dist/index.js +3 -6207
  133. package/dist/unpkg/markdown/dist/rehype.d.ts +10 -0
  134. package/dist/unpkg/markdown/dist/rehype.js +49 -0
  135. package/dist/unpkg/markdown/dist/remark.d.ts +9 -0
  136. package/dist/unpkg/markdown/dist/remark.js +28 -0
  137. package/dist/unpkg/markdown/dist/types.d.ts +11 -0
  138. package/dist/unpkg/markdown/dist/types.js +1 -0
  139. package/dist/unpkg/viewer/dist/atlas-state.d.ts +12 -0
  140. package/dist/unpkg/viewer/dist/atlas-state.js +269 -0
  141. package/dist/unpkg/viewer/dist/atlas-viewer.d.ts +2 -0
  142. package/dist/unpkg/viewer/dist/atlas-viewer.js +495 -0
  143. package/dist/unpkg/viewer/dist/custom-element.d.ts +1 -0
  144. package/dist/unpkg/viewer/dist/custom-element.js +78 -0
  145. package/dist/unpkg/viewer/dist/embed.d.ts +24 -0
  146. package/dist/unpkg/viewer/dist/embed.js +172 -0
  147. package/dist/unpkg/viewer/dist/errors.d.ts +6 -0
  148. package/dist/unpkg/viewer/dist/errors.js +12 -0
  149. package/dist/unpkg/viewer/dist/index.d.ts +10 -0
  150. package/dist/unpkg/viewer/dist/index.js +9 -8391
  151. package/dist/unpkg/viewer/dist/minimap.d.ts +3 -0
  152. package/dist/unpkg/viewer/dist/minimap.js +63 -0
  153. package/dist/unpkg/viewer/dist/render.d.ts +6 -0
  154. package/dist/unpkg/viewer/dist/render.js +670 -0
  155. package/dist/unpkg/viewer/dist/runtime-3d.d.ts +19 -0
  156. package/dist/unpkg/viewer/dist/runtime-3d.js +494 -0
  157. package/dist/unpkg/viewer/dist/theme.d.ts +4 -0
  158. package/dist/unpkg/viewer/dist/theme.js +103 -0
  159. package/dist/unpkg/viewer/dist/tooltip.d.ts +3 -0
  160. package/dist/unpkg/viewer/dist/tooltip.js +198 -0
  161. package/dist/unpkg/viewer/dist/types.d.ts +292 -0
  162. package/dist/unpkg/viewer/dist/types.js +1 -0
  163. package/dist/unpkg/viewer/dist/vendor/three.module.js +53032 -0
  164. package/dist/unpkg/viewer/dist/viewer-state.d.ts +19 -0
  165. package/dist/unpkg/viewer/dist/viewer-state.js +162 -0
  166. package/dist/unpkg/viewer/dist/viewer.d.ts +2 -0
  167. package/dist/unpkg/viewer/dist/viewer.js +1662 -0
  168. package/dist/unpkg/worldorbit-core.min.js +10 -10
  169. package/dist/unpkg/worldorbit-editor.min.js +4109 -256
  170. package/dist/unpkg/worldorbit-markdown.min.js +26 -26
  171. package/dist/unpkg/worldorbit-viewer.min.js +3945 -92
  172. package/dist/unpkg/worldorbit.js +32219 -199
  173. package/dist/unpkg/worldorbit.min.js +3949 -96
  174. package/package.json +1 -1
  175. package/packages/core/dist/index.d.ts +1 -0
  176. package/packages/core/dist/index.js +1 -0
  177. package/packages/core/dist/spatial-scene.d.ts +3 -0
  178. package/packages/core/dist/spatial-scene.js +420 -0
  179. package/packages/core/dist/types.d.ts +105 -0
  180. package/packages/editor/dist/editor.js +25 -4
  181. package/packages/editor/dist/types.d.ts +4 -0
  182. package/packages/markdown/dist/html.js +10 -3
  183. package/packages/viewer/dist/atlas-state.js +3 -0
  184. package/packages/viewer/dist/atlas-viewer.js +1 -0
  185. package/packages/viewer/dist/custom-element.js +18 -4
  186. package/packages/viewer/dist/embed.d.ts +5 -1
  187. package/packages/viewer/dist/embed.js +58 -24
  188. package/packages/viewer/dist/errors.d.ts +6 -0
  189. package/packages/viewer/dist/errors.js +12 -0
  190. package/packages/viewer/dist/index.d.ts +1 -0
  191. package/packages/viewer/dist/index.js +1 -0
  192. package/packages/viewer/dist/runtime-3d.d.ts +19 -0
  193. package/packages/viewer/dist/runtime-3d.js +494 -0
  194. package/packages/viewer/dist/types.d.ts +21 -2
  195. package/packages/viewer/dist/vendor/three.module.js +53032 -0
  196. package/packages/viewer/dist/viewer.js +501 -41
package/README.md CHANGED
@@ -118,7 +118,6 @@ For direct browser usage, use the browser bundle:
118
118
  <html>
119
119
  <head>
120
120
  <meta charset="utf-8" />
121
- <script src="https://unpkg.com/worldorbit@2.6.0/dist/unpkg/worldorbit.min.js"></script>
122
121
  <style>
123
122
  html, body {
124
123
  margin: 0;
@@ -135,7 +134,12 @@ For direct browser usage, use the browser bundle:
135
134
  <body>
136
135
  <div id="view"></div>
137
136
 
138
- <script>
137
+ <script type="module">
138
+ import {
139
+ createInteractiveViewer,
140
+ loadWorldOrbitSource
141
+ } from "https://unpkg.com/worldorbit@3.0.0/dist/unpkg/worldorbit.esm.js";
142
+
139
143
  const source = `
140
144
  schema 2.5
141
145
 
@@ -148,9 +152,9 @@ object planet Naar
148
152
  semiMajor 1.18au
149
153
  `.trim();
150
154
 
151
- const loaded = WorldOrbit.loadWorldOrbitSource(source);
155
+ const loaded = loadWorldOrbitSource(source);
152
156
 
153
- WorldOrbit.createInteractiveViewer(document.getElementById("view"), {
157
+ createInteractiveViewer(document.getElementById("view"), {
154
158
  document: loaded.document
155
159
  });
156
160
  </script>
@@ -158,7 +162,7 @@ object planet Naar
158
162
  </html>
159
163
  ```
160
164
 
161
- For browser usage, `worldorbit.min.js` is the recommended public entry point.
165
+ For browser usage today, prefer the ESM entry point `worldorbit.esm.js`.
162
166
 
163
167
  ## Static and Interactive Rendering
164
168
 
@@ -351,6 +355,7 @@ createInteractiveViewer(document.getElementById("preview"), {
351
355
  document: loaded.document,
352
356
  projection: "isometric",
353
357
  theme: "atlas",
358
+ viewMode: "3d",
354
359
  });
355
360
  ```
356
361
 
@@ -427,9 +432,12 @@ const scene = renderDocumentToScene(loaded.document, {
427
432
 
428
433
  ## Viewer Capabilities
429
434
 
430
- Viewer features in `v2.6` include:
435
+ Viewer features in `v3.0.0` include:
431
436
 
432
437
  * scene-based SVG rendering
438
+ * renderer-neutral spatial scenes through `renderDocumentToSpatialScene(...)`
439
+ * a full 3D viewer mode on the same documents and placements as 2D
440
+ * deterministic orbit animation with play, pause, reset, and speed controls
433
441
  * projections: `topdown`, `isometric`, `orthographic`, and `perspective`
434
442
  * theme presets: `atlas`, `nightglass`, `ember`
435
443
  * layer controls for background, guides, orbits, events, objects, labels, metadata, and relations
@@ -442,7 +450,7 @@ Viewer features in `v2.6` include:
442
450
 
443
451
  ## Markdown Integration
444
452
 
445
- Use `worldorbit/markdown` to transform fenced `worldorbit` blocks into static or interactive output.
453
+ Use `worldorbit/markdown` to transform fenced `worldorbit` blocks into static output, interactive 2D output, or interactive 3D output.
446
454
 
447
455
  ```ts
448
456
  import rehypeStringify from "rehype-stringify";
@@ -455,7 +463,7 @@ import { remarkWorldOrbit } from "worldorbit/markdown";
455
463
  const html = String(
456
464
  await unified()
457
465
  .use(remarkParse)
458
- .use(remarkWorldOrbit, { mode: "interactive" })
466
+ .use(remarkWorldOrbit, { mode: "interactive-3d" })
459
467
  .use(remarkRehype, { allowDangerousHtml: true })
460
468
  .use(rehypeStringify, { allowDangerousHtml: true })
461
469
  .process(markdownSource),
@@ -468,7 +476,7 @@ In the browser:
468
476
  import { mountWorldOrbitEmbeds } from "worldorbit/viewer";
469
477
 
470
478
  mountWorldOrbitEmbeds(document, {
471
- mode: "interactive",
479
+ mode: "interactive-3d",
472
480
  });
473
481
  ```
474
482
 
@@ -477,6 +485,8 @@ mountWorldOrbitEmbeds(document, {
477
485
  Examples live in:
478
486
 
479
487
  * [examples/minimal.worldorbit](./examples/minimal.worldorbit)
488
+ * [examples/minimal-3d.worldorbit](./examples/minimal-3d.worldorbit)
489
+ * [examples/showcase-3d.worldorbit](./examples/showcase-3d.worldorbit)
480
490
  * [examples/schema25-camera.worldorbit](./examples/schema25-camera.worldorbit)
481
491
  * [examples/schema25-event-snapshot.worldorbit](./examples/schema25-event-snapshot.worldorbit)
482
492
  * [examples/studio.schema25.worldorbit](./examples/studio.schema25.worldorbit)
@@ -496,6 +506,7 @@ Browser-facing examples and demos live in the repository under `demo/`, `studio/
496
506
  * [migration guide: v1 to v2](./docs/migration-v1-to-v2.md)
497
507
  * [migration guide: v2.0 to v2.1](./docs/migration-v2-to-v2.1.md)
498
508
  * [migration guide: v2.1 to v2.5](./docs/migration-v2.1-to-v2.5.md)
509
+ * [migration guide: v2.6 to v3.0](./docs/migration-v2.6-to-v3.0.md)
499
510
  * [language reference](./docs/language-reference.md)
500
511
  * [language reference (DE)](./docs/language-reference.de.md)
501
512
  * [API inventory](./docs/api-inventory.md)
@@ -0,0 +1,11 @@
1
+ import type { AtlasDocumentPath, AtlasResolvedDiagnostic, WorldOrbitAtlasDocument, WorldOrbitAtlasDocumentVersion, WorldOrbitDiagnostic } from "./types.js";
2
+ export declare function createEmptyAtlasDocument(systemId?: string, version?: WorldOrbitAtlasDocumentVersion): WorldOrbitAtlasDocument;
3
+ export declare function cloneAtlasDocument(document: WorldOrbitAtlasDocument): WorldOrbitAtlasDocument;
4
+ export declare function listAtlasDocumentPaths(document: WorldOrbitAtlasDocument): AtlasDocumentPath[];
5
+ export declare function getAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath): unknown;
6
+ export declare function upsertAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath, value: unknown): WorldOrbitAtlasDocument;
7
+ export declare function updateAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath, updater: (value: unknown) => unknown): WorldOrbitAtlasDocument;
8
+ export declare function removeAtlasDocumentNode(document: WorldOrbitAtlasDocument, path: AtlasDocumentPath): WorldOrbitAtlasDocument;
9
+ export declare function resolveAtlasDiagnostics(document: WorldOrbitAtlasDocument, diagnostics: WorldOrbitDiagnostic[]): AtlasResolvedDiagnostic[];
10
+ export declare function resolveAtlasDiagnosticPath(document: WorldOrbitAtlasDocument, diagnostic: WorldOrbitDiagnostic): AtlasDocumentPath | null;
11
+ export declare function validateAtlasDocumentWithDiagnostics(document: WorldOrbitAtlasDocument): AtlasResolvedDiagnostic[];
@@ -0,0 +1,347 @@
1
+ import { collectAtlasDiagnostics } from "./atlas-validate.js";
2
+ export function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.5") {
3
+ return {
4
+ format: "worldorbit",
5
+ version,
6
+ schemaVersion: version,
7
+ sourceVersion: "1.0",
8
+ system: {
9
+ type: "system",
10
+ id: systemId,
11
+ title: systemId,
12
+ description: null,
13
+ epoch: null,
14
+ referencePlane: null,
15
+ defaults: {
16
+ view: "topdown",
17
+ scale: null,
18
+ units: null,
19
+ preset: null,
20
+ theme: null,
21
+ },
22
+ atlasMetadata: {},
23
+ viewpoints: [],
24
+ annotations: [],
25
+ },
26
+ groups: [],
27
+ relations: [],
28
+ events: [],
29
+ objects: [],
30
+ diagnostics: [],
31
+ };
32
+ }
33
+ export function cloneAtlasDocument(document) {
34
+ return structuredClone(document);
35
+ }
36
+ export function listAtlasDocumentPaths(document) {
37
+ const paths = [{ kind: "system" }, { kind: "defaults" }];
38
+ if (document.system) {
39
+ for (const key of Object.keys(document.system.atlasMetadata).sort()) {
40
+ paths.push({ kind: "metadata", key });
41
+ }
42
+ for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike)) {
43
+ paths.push({ kind: "viewpoint", id: viewpoint.id });
44
+ }
45
+ for (const annotation of [...document.system.annotations].sort(compareIdLike)) {
46
+ paths.push({ kind: "annotation", id: annotation.id });
47
+ }
48
+ }
49
+ for (const group of [...document.groups].sort(compareIdLike)) {
50
+ paths.push({ kind: "group", id: group.id });
51
+ }
52
+ for (const relation of [...document.relations].sort(compareIdLike)) {
53
+ paths.push({ kind: "relation", id: relation.id });
54
+ }
55
+ for (const event of [...document.events].sort(compareIdLike)) {
56
+ paths.push({ kind: "event", id: event.id });
57
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
58
+ paths.push({ kind: "event-pose", id: event.id, key: pose.objectId });
59
+ }
60
+ }
61
+ for (const object of [...document.objects].sort(compareIdLike)) {
62
+ paths.push({ kind: "object", id: object.id });
63
+ }
64
+ return paths;
65
+ }
66
+ export function getAtlasDocumentNode(document, path) {
67
+ switch (path.kind) {
68
+ case "system":
69
+ return document.system;
70
+ case "defaults":
71
+ return document.system?.defaults ?? null;
72
+ case "metadata":
73
+ return path.key ? (document.system?.atlasMetadata[path.key] ?? null) : null;
74
+ case "group":
75
+ return path.id ? findGroup(document, path.id) : null;
76
+ case "event":
77
+ return path.id ? findEvent(document, path.id) : null;
78
+ case "event-pose":
79
+ return path.id && path.key ? findEventPose(document, path.id, path.key) : null;
80
+ case "object":
81
+ return path.id ? findObject(document, path.id) : null;
82
+ case "viewpoint":
83
+ return path.id ? findViewpoint(document.system, path.id) : null;
84
+ case "annotation":
85
+ return path.id ? findAnnotation(document.system, path.id) : null;
86
+ case "relation":
87
+ return path.id ? findRelation(document, path.id) : null;
88
+ }
89
+ }
90
+ export function upsertAtlasDocumentNode(document, path, value) {
91
+ const next = cloneAtlasDocument(document);
92
+ const system = ensureSystem(next);
93
+ switch (path.kind) {
94
+ case "system":
95
+ next.system = value;
96
+ return next;
97
+ case "defaults":
98
+ system.defaults = {
99
+ ...system.defaults,
100
+ ...value,
101
+ };
102
+ return next;
103
+ case "metadata":
104
+ if (!path.key) {
105
+ throw new Error('Metadata updates require a "key" value.');
106
+ }
107
+ if (value === null || value === undefined || value === "") {
108
+ delete system.atlasMetadata[path.key];
109
+ }
110
+ else {
111
+ system.atlasMetadata[path.key] = String(value);
112
+ }
113
+ return next;
114
+ case "group":
115
+ if (!path.id) {
116
+ throw new Error('Group updates require an "id" value.');
117
+ }
118
+ upsertById(next.groups, value);
119
+ return next;
120
+ case "event":
121
+ if (!path.id) {
122
+ throw new Error('Event updates require an "id" value.');
123
+ }
124
+ upsertById(next.events, value);
125
+ return next;
126
+ case "event-pose":
127
+ if (!path.id || !path.key) {
128
+ throw new Error('Event pose updates require an event "id" and pose "key" value.');
129
+ }
130
+ upsertEventPose(next.events, path.id, value);
131
+ return next;
132
+ case "object":
133
+ if (!path.id) {
134
+ throw new Error('Object updates require an "id" value.');
135
+ }
136
+ upsertById(next.objects, value);
137
+ return next;
138
+ case "viewpoint":
139
+ if (!path.id) {
140
+ throw new Error('Viewpoint updates require an "id" value.');
141
+ }
142
+ upsertById(system.viewpoints, value);
143
+ return next;
144
+ case "annotation":
145
+ if (!path.id) {
146
+ throw new Error('Annotation updates require an "id" value.');
147
+ }
148
+ upsertById(system.annotations, value);
149
+ return next;
150
+ case "relation":
151
+ if (!path.id) {
152
+ throw new Error('Relation updates require an "id" value.');
153
+ }
154
+ upsertById(next.relations, value);
155
+ return next;
156
+ }
157
+ }
158
+ export function updateAtlasDocumentNode(document, path, updater) {
159
+ return upsertAtlasDocumentNode(document, path, updater(getAtlasDocumentNode(document, path)));
160
+ }
161
+ export function removeAtlasDocumentNode(document, path) {
162
+ const next = cloneAtlasDocument(document);
163
+ const system = ensureSystem(next);
164
+ switch (path.kind) {
165
+ case "metadata":
166
+ if (path.key) {
167
+ delete system.atlasMetadata[path.key];
168
+ }
169
+ return next;
170
+ case "object":
171
+ if (path.id) {
172
+ next.objects = next.objects.filter((object) => object.id !== path.id);
173
+ }
174
+ return next;
175
+ case "group":
176
+ if (path.id) {
177
+ next.groups = next.groups.filter((group) => group.id !== path.id);
178
+ }
179
+ return next;
180
+ case "event":
181
+ if (path.id) {
182
+ next.events = next.events.filter((event) => event.id !== path.id);
183
+ }
184
+ return next;
185
+ case "event-pose":
186
+ if (path.id && path.key) {
187
+ const event = findEvent(next, path.id);
188
+ if (event) {
189
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
190
+ }
191
+ }
192
+ return next;
193
+ case "viewpoint":
194
+ if (path.id) {
195
+ system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
196
+ }
197
+ return next;
198
+ case "annotation":
199
+ if (path.id) {
200
+ system.annotations = system.annotations.filter((annotation) => annotation.id !== path.id);
201
+ }
202
+ return next;
203
+ case "relation":
204
+ if (path.id) {
205
+ next.relations = next.relations.filter((relation) => relation.id !== path.id);
206
+ }
207
+ return next;
208
+ default:
209
+ return next;
210
+ }
211
+ }
212
+ export function resolveAtlasDiagnostics(document, diagnostics) {
213
+ return diagnostics.map((diagnostic) => ({
214
+ diagnostic,
215
+ path: resolveAtlasDiagnosticPath(document, diagnostic),
216
+ }));
217
+ }
218
+ export function resolveAtlasDiagnosticPath(document, diagnostic) {
219
+ if (diagnostic.objectId && findObject(document, diagnostic.objectId)) {
220
+ return {
221
+ kind: "object",
222
+ id: diagnostic.objectId,
223
+ };
224
+ }
225
+ if (diagnostic.field?.startsWith("group.")) {
226
+ const parts = diagnostic.field.split(".");
227
+ if (parts[1] && findGroup(document, parts[1])) {
228
+ return {
229
+ kind: "group",
230
+ id: parts[1],
231
+ };
232
+ }
233
+ }
234
+ if (diagnostic.field?.startsWith("viewpoint.")) {
235
+ const parts = diagnostic.field.split(".");
236
+ if (parts[1] && findViewpoint(document.system, parts[1])) {
237
+ return {
238
+ kind: "viewpoint",
239
+ id: parts[1],
240
+ };
241
+ }
242
+ }
243
+ if (diagnostic.field?.startsWith("annotation.")) {
244
+ const parts = diagnostic.field.split(".");
245
+ if (parts[1] && findAnnotation(document.system, parts[1])) {
246
+ return {
247
+ kind: "annotation",
248
+ id: parts[1],
249
+ };
250
+ }
251
+ }
252
+ if (diagnostic.field?.startsWith("relation.")) {
253
+ const parts = diagnostic.field.split(".");
254
+ if (parts[1] && findRelation(document, parts[1])) {
255
+ return {
256
+ kind: "relation",
257
+ id: parts[1],
258
+ };
259
+ }
260
+ }
261
+ if (diagnostic.field?.startsWith("event.")) {
262
+ const parts = diagnostic.field.split(".");
263
+ if (parts[1] && findEvent(document, parts[1])) {
264
+ if (parts[2] === "pose" && parts[3] && findEventPose(document, parts[1], parts[3])) {
265
+ return {
266
+ kind: "event-pose",
267
+ id: parts[1],
268
+ key: parts[3],
269
+ };
270
+ }
271
+ return {
272
+ kind: "event",
273
+ id: parts[1],
274
+ };
275
+ }
276
+ }
277
+ if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
278
+ return {
279
+ kind: "metadata",
280
+ key: diagnostic.field,
281
+ };
282
+ }
283
+ return null;
284
+ }
285
+ export function validateAtlasDocumentWithDiagnostics(document) {
286
+ const diagnostics = [
287
+ ...document.diagnostics,
288
+ ...collectAtlasDiagnostics(document, document.version),
289
+ ];
290
+ return resolveAtlasDiagnostics(document, diagnostics);
291
+ }
292
+ function ensureSystem(document) {
293
+ if (document.system) {
294
+ return document.system;
295
+ }
296
+ document.system = createEmptyAtlasDocument().system;
297
+ return document.system;
298
+ }
299
+ function findObject(document, objectId) {
300
+ return document.objects.find((object) => object.id === objectId) ?? null;
301
+ }
302
+ function findGroup(document, groupId) {
303
+ return document.groups.find((group) => group.id === groupId) ?? null;
304
+ }
305
+ function findRelation(document, relationId) {
306
+ return document.relations.find((relation) => relation.id === relationId) ?? null;
307
+ }
308
+ function findEvent(document, eventId) {
309
+ return document.events.find((event) => event.id === eventId) ?? null;
310
+ }
311
+ function findEventPose(document, eventId, objectId) {
312
+ return findEvent(document, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
313
+ }
314
+ function findViewpoint(system, viewpointId) {
315
+ return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
316
+ }
317
+ function findAnnotation(system, annotationId) {
318
+ return system?.annotations.find((annotation) => annotation.id === annotationId) ?? null;
319
+ }
320
+ function upsertById(items, value) {
321
+ const index = items.findIndex((item) => item.id === value.id);
322
+ if (index === -1) {
323
+ items.push(value);
324
+ items.sort(compareIdLike);
325
+ return;
326
+ }
327
+ items[index] = value;
328
+ }
329
+ function upsertEventPose(events, eventId, value) {
330
+ const event = events.find((entry) => entry.id === eventId);
331
+ if (!event) {
332
+ throw new Error(`Unknown event "${eventId}" for pose update.`);
333
+ }
334
+ const index = event.positions.findIndex((entry) => entry.objectId === value.objectId);
335
+ if (index === -1) {
336
+ event.positions.push(value);
337
+ event.positions.sort(comparePoseObjectId);
338
+ return;
339
+ }
340
+ event.positions[index] = value;
341
+ }
342
+ function compareIdLike(left, right) {
343
+ return left.id.localeCompare(right.id);
344
+ }
345
+ function comparePoseObjectId(left, right) {
346
+ return left.objectId.localeCompare(right.objectId);
347
+ }
@@ -0,0 +1,22 @@
1
+ import type { AstFieldNode, AstSourceLocation, AtReference, NormalizedValue, UnitValue, WorldOrbitObjectType } from "./types.js";
2
+ export interface AtlasFieldLike {
3
+ key: string;
4
+ values: string[];
5
+ location: AstSourceLocation;
6
+ }
7
+ export declare function normalizeIdentifier(value: string): string;
8
+ export declare function humanizeIdentifier(value: string): string;
9
+ export declare function parseAtlasUnitValue(input: string, location?: AstSourceLocation, fieldKey?: string): UnitValue;
10
+ export declare function tryParseAtlasUnitValue(input: string): UnitValue | null;
11
+ export declare function parseAtlasNumber(input: string, key: string, location?: AstSourceLocation): number;
12
+ export declare function parseAtlasBoolean(input: string, key: string, location?: AstSourceLocation): boolean;
13
+ export declare function parseAtlasFieldBoolean(field: AtlasFieldLike): boolean;
14
+ export declare function parseAtlasAtReference(target: string, location?: AstSourceLocation): AtReference;
15
+ export declare function validateAtlasImageSource(value: string, location?: AstSourceLocation): void;
16
+ export declare function normalizeLegacyScalarValue(key: string, values: string[], location: AstSourceLocation): NormalizedValue;
17
+ export declare function ensureAtlasFieldSupported(key: string, objectType: WorldOrbitObjectType, location: AstSourceLocation): void;
18
+ export declare function singleAtlasFieldValue(field: Pick<AtlasFieldLike, "key" | "values" | "location">): string;
19
+ export declare function singleAtlasValue(values: string[], key: string, location?: AstSourceLocation): string;
20
+ export declare function isStructureLikeObjectType(objectType: WorldOrbitObjectType): boolean;
21
+ export declare function cloneNormalizedValue(value: NormalizedValue): NormalizedValue;
22
+ export declare function cloneFieldNode(field: AstFieldNode): AstFieldNode;
@@ -0,0 +1,189 @@
1
+ import { WorldOrbitError } from "./errors.js";
2
+ import { getFieldSchema, unitFamilyAllowsUnit } from "./schema.js";
3
+ const UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
4
+ const BOOLEAN_VALUES = new Map([
5
+ ["true", true],
6
+ ["false", false],
7
+ ["yes", true],
8
+ ["no", false],
9
+ ]);
10
+ const URL_SCHEME_PATTERN = /^[A-Za-z][A-Za-z0-9+.-]*:/;
11
+ export function normalizeIdentifier(value) {
12
+ return value
13
+ .trim()
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9_-]+/g, "-")
16
+ .replace(/^-+|-+$/g, "");
17
+ }
18
+ export function humanizeIdentifier(value) {
19
+ return value
20
+ .split(/[-_]+/)
21
+ .filter(Boolean)
22
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
23
+ .join(" ");
24
+ }
25
+ export function parseAtlasUnitValue(input, location, fieldKey) {
26
+ const match = input.match(UNIT_PATTERN);
27
+ if (!match) {
28
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
29
+ }
30
+ const unitValue = {
31
+ value: Number(match[1]),
32
+ unit: match[2] ?? null,
33
+ };
34
+ if (fieldKey) {
35
+ const schema = getFieldSchema(fieldKey);
36
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
37
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
38
+ }
39
+ }
40
+ return unitValue;
41
+ }
42
+ export function tryParseAtlasUnitValue(input) {
43
+ const match = input.match(UNIT_PATTERN);
44
+ if (!match) {
45
+ return null;
46
+ }
47
+ return {
48
+ value: Number(match[1]),
49
+ unit: match[2] ?? null,
50
+ };
51
+ }
52
+ export function parseAtlasNumber(input, key, location) {
53
+ const value = Number(input);
54
+ if (!Number.isFinite(value)) {
55
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
56
+ }
57
+ return value;
58
+ }
59
+ export function parseAtlasBoolean(input, key, location) {
60
+ const parsed = BOOLEAN_VALUES.get(input.toLowerCase());
61
+ if (parsed === undefined) {
62
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
63
+ }
64
+ return parsed;
65
+ }
66
+ export function parseAtlasFieldBoolean(field) {
67
+ return parseAtlasBoolean(singleAtlasFieldValue(field), field.key, field.location);
68
+ }
69
+ export function parseAtlasAtReference(target, location) {
70
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
71
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
72
+ }
73
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
74
+ if (pairedMatch) {
75
+ return {
76
+ kind: "lagrange",
77
+ primary: pairedMatch[1],
78
+ secondary: pairedMatch[2],
79
+ point: pairedMatch[3],
80
+ };
81
+ }
82
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
83
+ if (simpleMatch) {
84
+ return {
85
+ kind: "lagrange",
86
+ primary: simpleMatch[1],
87
+ secondary: null,
88
+ point: simpleMatch[2],
89
+ };
90
+ }
91
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
92
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
93
+ }
94
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
95
+ if (anchorMatch) {
96
+ return {
97
+ kind: "anchor",
98
+ objectId: anchorMatch[1],
99
+ anchor: anchorMatch[2],
100
+ };
101
+ }
102
+ return {
103
+ kind: "named",
104
+ name: target,
105
+ };
106
+ }
107
+ export function validateAtlasImageSource(value, location) {
108
+ if (!value) {
109
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
110
+ }
111
+ if (value.startsWith("//")) {
112
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
113
+ }
114
+ const schemeMatch = value.match(URL_SCHEME_PATTERN);
115
+ if (!schemeMatch) {
116
+ return;
117
+ }
118
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
119
+ if (scheme !== "http" && scheme !== "https") {
120
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
121
+ }
122
+ }
123
+ export function normalizeLegacyScalarValue(key, values, location) {
124
+ const schema = getFieldSchema(key);
125
+ if (!schema) {
126
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
127
+ }
128
+ if (schema.arity === "single" && values.length !== 1) {
129
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
130
+ }
131
+ switch (schema.kind) {
132
+ case "list":
133
+ return values;
134
+ case "boolean":
135
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
136
+ case "number":
137
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
138
+ case "unit":
139
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
140
+ case "string": {
141
+ const value = values.join(" ").trim();
142
+ if (key === "image") {
143
+ validateAtlasImageSource(value, location);
144
+ }
145
+ return value;
146
+ }
147
+ }
148
+ }
149
+ export function ensureAtlasFieldSupported(key, objectType, location) {
150
+ const schema = getFieldSchema(key);
151
+ if (!schema) {
152
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
153
+ }
154
+ if (!schema.objectTypes.includes(objectType)) {
155
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
156
+ }
157
+ }
158
+ export function singleAtlasFieldValue(field) {
159
+ return singleAtlasValue(field.values, field.key, field.location);
160
+ }
161
+ export function singleAtlasValue(values, key, location) {
162
+ if (values.length !== 1) {
163
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
164
+ }
165
+ return values[0];
166
+ }
167
+ export function isStructureLikeObjectType(objectType) {
168
+ return objectType === "structure" || objectType === "phenomenon";
169
+ }
170
+ export function cloneNormalizedValue(value) {
171
+ if (Array.isArray(value)) {
172
+ return [...value];
173
+ }
174
+ if (value && typeof value === "object" && "value" in value) {
175
+ return {
176
+ value: value.value,
177
+ unit: value.unit,
178
+ };
179
+ }
180
+ return value;
181
+ }
182
+ export function cloneFieldNode(field) {
183
+ return {
184
+ type: "field",
185
+ key: field.key,
186
+ values: [...field.values],
187
+ location: { ...field.location },
188
+ };
189
+ }
@@ -0,0 +1,2 @@
1
+ import type { WorldOrbitAnyDocumentVersion, WorldOrbitAtlasDocument, WorldOrbitDiagnostic, WorldOrbitDraftDocument } from "./types.js";
2
+ export declare function collectAtlasDiagnostics(document: WorldOrbitAtlasDocument | WorldOrbitDraftDocument, sourceSchemaVersion?: WorldOrbitAnyDocumentVersion): WorldOrbitDiagnostic[];