worldorbit 2.6.0 → 3.0.0

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 +12 -5
  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 +1 -12
  169. package/dist/unpkg/worldorbit-editor.min.js +1 -904
  170. package/dist/unpkg/worldorbit-markdown.min.js +1 -103
  171. package/dist/unpkg/worldorbit-viewer.min.js +1 -259
  172. package/dist/unpkg/worldorbit.js +2 -9704
  173. package/dist/unpkg/worldorbit.min.js +2 -263
  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,7 @@ 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>
121
+ <script src="https://unpkg.com/worldorbit@3.0.0/dist/unpkg/worldorbit.min.js"></script>
122
122
  <style>
123
123
  html, body {
124
124
  margin: 0;
@@ -351,6 +351,7 @@ createInteractiveViewer(document.getElementById("preview"), {
351
351
  document: loaded.document,
352
352
  projection: "isometric",
353
353
  theme: "atlas",
354
+ viewMode: "3d",
354
355
  });
355
356
  ```
356
357
 
@@ -427,9 +428,12 @@ const scene = renderDocumentToScene(loaded.document, {
427
428
 
428
429
  ## Viewer Capabilities
429
430
 
430
- Viewer features in `v2.6` include:
431
+ Viewer features in `v3.0.0` include:
431
432
 
432
433
  * scene-based SVG rendering
434
+ * renderer-neutral spatial scenes through `renderDocumentToSpatialScene(...)`
435
+ * a full 3D viewer mode on the same documents and placements as 2D
436
+ * deterministic orbit animation with play, pause, reset, and speed controls
433
437
  * projections: `topdown`, `isometric`, `orthographic`, and `perspective`
434
438
  * theme presets: `atlas`, `nightglass`, `ember`
435
439
  * layer controls for background, guides, orbits, events, objects, labels, metadata, and relations
@@ -442,7 +446,7 @@ Viewer features in `v2.6` include:
442
446
 
443
447
  ## Markdown Integration
444
448
 
445
- Use `worldorbit/markdown` to transform fenced `worldorbit` blocks into static or interactive output.
449
+ Use `worldorbit/markdown` to transform fenced `worldorbit` blocks into static output, interactive 2D output, or interactive 3D output.
446
450
 
447
451
  ```ts
448
452
  import rehypeStringify from "rehype-stringify";
@@ -455,7 +459,7 @@ import { remarkWorldOrbit } from "worldorbit/markdown";
455
459
  const html = String(
456
460
  await unified()
457
461
  .use(remarkParse)
458
- .use(remarkWorldOrbit, { mode: "interactive" })
462
+ .use(remarkWorldOrbit, { mode: "interactive-3d" })
459
463
  .use(remarkRehype, { allowDangerousHtml: true })
460
464
  .use(rehypeStringify, { allowDangerousHtml: true })
461
465
  .process(markdownSource),
@@ -468,7 +472,7 @@ In the browser:
468
472
  import { mountWorldOrbitEmbeds } from "worldorbit/viewer";
469
473
 
470
474
  mountWorldOrbitEmbeds(document, {
471
- mode: "interactive",
475
+ mode: "interactive-3d",
472
476
  });
473
477
  ```
474
478
 
@@ -477,6 +481,8 @@ mountWorldOrbitEmbeds(document, {
477
481
  Examples live in:
478
482
 
479
483
  * [examples/minimal.worldorbit](./examples/minimal.worldorbit)
484
+ * [examples/minimal-3d.worldorbit](./examples/minimal-3d.worldorbit)
485
+ * [examples/showcase-3d.worldorbit](./examples/showcase-3d.worldorbit)
480
486
  * [examples/schema25-camera.worldorbit](./examples/schema25-camera.worldorbit)
481
487
  * [examples/schema25-event-snapshot.worldorbit](./examples/schema25-event-snapshot.worldorbit)
482
488
  * [examples/studio.schema25.worldorbit](./examples/studio.schema25.worldorbit)
@@ -496,6 +502,7 @@ Browser-facing examples and demos live in the repository under `demo/`, `studio/
496
502
  * [migration guide: v1 to v2](./docs/migration-v1-to-v2.md)
497
503
  * [migration guide: v2.0 to v2.1](./docs/migration-v2-to-v2.1.md)
498
504
  * [migration guide: v2.1 to v2.5](./docs/migration-v2.1-to-v2.5.md)
505
+ * [migration guide: v2.6 to v3.0](./docs/migration-v2.6-to-v3.0.md)
499
506
  * [language reference](./docs/language-reference.md)
500
507
  * [language reference (DE)](./docs/language-reference.de.md)
501
508
  * [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[];