worldorbit 2.5.17 → 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 (203) hide show
  1. package/README.md +91 -18
  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 -6101
  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 -11702
  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 -5766
  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 -7901
  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 -6173
  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 -11727
  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 -5794
  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 -7958
  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 -894
  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 -9243
  173. package/dist/unpkg/worldorbit.min.js +2 -263
  174. package/package.json +1 -1
  175. package/packages/core/dist/atlas-edit.js +1 -1
  176. package/packages/core/dist/atlas-validate.js +99 -10
  177. package/packages/core/dist/draft-parse.js +190 -15
  178. package/packages/core/dist/draft.js +50 -11
  179. package/packages/core/dist/format.js +36 -5
  180. package/packages/core/dist/index.d.ts +1 -0
  181. package/packages/core/dist/index.js +1 -0
  182. package/packages/core/dist/load.js +9 -2
  183. package/packages/core/dist/scene.js +158 -24
  184. package/packages/core/dist/spatial-scene.d.ts +3 -0
  185. package/packages/core/dist/spatial-scene.js +420 -0
  186. package/packages/core/dist/types.d.ts +124 -2
  187. package/packages/editor/dist/editor.js +130 -8
  188. package/packages/editor/dist/types.d.ts +4 -0
  189. package/packages/markdown/dist/html.js +10 -3
  190. package/packages/viewer/dist/atlas-state.js +8 -2
  191. package/packages/viewer/dist/atlas-viewer.js +20 -8
  192. package/packages/viewer/dist/custom-element.js +18 -4
  193. package/packages/viewer/dist/embed.d.ts +5 -1
  194. package/packages/viewer/dist/embed.js +58 -24
  195. package/packages/viewer/dist/errors.d.ts +6 -0
  196. package/packages/viewer/dist/errors.js +12 -0
  197. package/packages/viewer/dist/index.d.ts +1 -0
  198. package/packages/viewer/dist/index.js +1 -0
  199. package/packages/viewer/dist/runtime-3d.d.ts +19 -0
  200. package/packages/viewer/dist/runtime-3d.js +494 -0
  201. package/packages/viewer/dist/types.d.ts +25 -3
  202. package/packages/viewer/dist/vendor/three.module.js +53032 -0
  203. package/packages/viewer/dist/viewer.js +517 -41
@@ -0,0 +1,3700 @@
1
+ import { cloneAtlasDocument, createEmptyAtlasDocument, formatDocument, getAtlasDocumentNode, loadWorldOrbitSourceWithDiagnostics, materializeAtlasDocument, removeAtlasDocumentNode, resolveAtlasDiagnostics, rotatePoint, upgradeDocumentToV2, validateAtlasDocumentWithDiagnostics, } from "@worldorbit/core";
2
+ import { renderWorldOrbitBlock } from "@worldorbit/markdown";
3
+ import { createInteractiveViewer, } from "@worldorbit/viewer";
4
+ import { getViewerVisibleBounds, invertViewerPoint } from "@worldorbit/viewer/viewer-state";
5
+ const STYLE_ID = "worldorbit-editor-style";
6
+ const SOURCE_INPUT_DEBOUNCE_MS = 120;
7
+ const PREVIEW_BATCH_DELAY_MS = 16;
8
+ const FREE_DISTANCE_PIXEL_FACTOR = 96;
9
+ const AU_IN_KM = 149_597_870.7;
10
+ const EARTH_RADIUS_IN_KM = 6_371;
11
+ const SOLAR_RADIUS_IN_KM = 695_700;
12
+ const PLACEMENT_DIAGNOSTIC_FIELDS = new Set([
13
+ "placement",
14
+ "target",
15
+ "reference",
16
+ "distance",
17
+ "semiMajor",
18
+ "eccentricity",
19
+ "period",
20
+ "angle",
21
+ "inclination",
22
+ "phase",
23
+ "descriptor",
24
+ ]);
25
+ const SURFACE_TARGET_TYPES = new Set([
26
+ "star",
27
+ "planet",
28
+ "moon",
29
+ "asteroid",
30
+ "comet",
31
+ ]);
32
+ const OBJECT_TYPES = [
33
+ "star",
34
+ "planet",
35
+ "moon",
36
+ "belt",
37
+ "asteroid",
38
+ "comet",
39
+ "ring",
40
+ "structure",
41
+ "phenomenon",
42
+ ];
43
+ const OBJECT_STRING_FIELDS = [
44
+ "kind",
45
+ "class",
46
+ "culture",
47
+ "color",
48
+ "image",
49
+ "atmosphere",
50
+ "on",
51
+ "source",
52
+ ];
53
+ const OBJECT_UNIT_FIELDS = [
54
+ "radius",
55
+ "mass",
56
+ "density",
57
+ "gravity",
58
+ "temperature",
59
+ "inner",
60
+ "outer",
61
+ "cycle",
62
+ ];
63
+ const OBJECT_NUMBER_FIELDS = ["albedo"];
64
+ const FIELD_HELP = {
65
+ "defaults-view": {
66
+ description: "Sets the default camera projection for the atlas.",
67
+ references: [
68
+ "Topdown = map-like",
69
+ "Isometric = angled overview",
70
+ "Orthographic/Perspective = 3D-ready semantic views",
71
+ ],
72
+ },
73
+ "defaults-scale": {
74
+ description: "Chooses the overall spacing/style preset used by the renderer.",
75
+ references: ["diagram = tighter", "presentation = roomier"],
76
+ },
77
+ "defaults-units": {
78
+ description: "Stores a document-wide note about the unit style you want to use.",
79
+ references: ["Example: metric", "Example: lore-standard"],
80
+ },
81
+ "viewpoint-projection": {
82
+ description: "Overrides the projection for this saved viewpoint.",
83
+ references: [
84
+ "Topdown = flat orbital map",
85
+ "Isometric = angled scene",
86
+ "Orthographic/Perspective = stored with current 2D fallback",
87
+ ],
88
+ },
89
+ "viewpoint-zoom": {
90
+ description: "Controls how closely this viewpoint frames the system.",
91
+ references: ["1 = scene fit", "2+ = close-up"],
92
+ },
93
+ "viewpoint-rotation": {
94
+ description: "Legacy 2D screen rotation. This is separate from the Schema 2.5 camera block.",
95
+ references: ["90deg = quarter turn", "Use camera.azimuth for semantic view direction"],
96
+ },
97
+ "viewpoint-camera-azimuth": {
98
+ description: "Horizontal camera direction in degrees for Schema 2.5 viewpoints.",
99
+ references: ["0 = forward/default", "90 = quarter orbit around the scene"],
100
+ },
101
+ "viewpoint-camera-elevation": {
102
+ description: "Vertical camera tilt in degrees for 3D-ready viewpoints.",
103
+ references: ["0 = level", "30 = gentle look down"],
104
+ },
105
+ "viewpoint-camera-roll": {
106
+ description: "Rolls the camera around its forward axis.",
107
+ references: ["0 = upright", "15 = slight bank"],
108
+ },
109
+ "viewpoint-camera-distance": {
110
+ description: "Semantic camera distance for perspective viewpoints.",
111
+ references: ["4 = close", "12 = wide framing"],
112
+ },
113
+ "viewpoint-events": {
114
+ description: "Lists event IDs that this viewpoint should feature in its detail panel.",
115
+ references: ["solar-eclipse-naar", "transit-window conjunction"],
116
+ },
117
+ "event-kind": {
118
+ description: "Short semantic event type for tooling and viewer overlays.",
119
+ references: ["solar-eclipse", "lunar-eclipse", "transit"],
120
+ },
121
+ "event-target": {
122
+ description: "Primary object this event is centered on.",
123
+ references: ["Naar", "Seyra"],
124
+ },
125
+ "event-participants": {
126
+ description: "Objects that participate in the event snapshot or description.",
127
+ references: ["Iyath Naar Seyra", "Naar Seyra Orun"],
128
+ },
129
+ "event-timing": {
130
+ description: "Free-text timing note for the event.",
131
+ references: ['"Every late bloom season"', '"At local midyear"'],
132
+ },
133
+ "event-visibility": {
134
+ description: "Notes where or how the event is visible.",
135
+ references: ['"Visible from Naar"', '"Southern hemisphere only"'],
136
+ },
137
+ "event-epoch": {
138
+ description: "Optional event-wide epoch that event poses inherit unless they override it.",
139
+ references: ['"JY-0001.0"', '"Naar bloom cycle year 18"'],
140
+ },
141
+ "event-referencePlane": {
142
+ description: "Optional event-wide reference plane for all poses in this snapshot.",
143
+ references: ["ecliptic", "naar-equatorial"],
144
+ },
145
+ "event-viewpoints": {
146
+ description: "Viewpoint IDs that should list this event prominently.",
147
+ references: ["naar-system", "overview inner-system"],
148
+ },
149
+ "placement-target": {
150
+ description: "Names the body or reference this object is attached to.",
151
+ references: ["orbit Primary", "surface Homeworld", "at Naar:L4"],
152
+ },
153
+ "placement-free": {
154
+ description: "Stores a free-placement offset or a descriptive label for loose placement.",
155
+ references: ["8au = far from the star", '"outer system" = descriptive note'],
156
+ },
157
+ "placement-distance": {
158
+ description: "Mean orbit distance from the target body.",
159
+ references: ["1 au = Earth-Sun distance", "384400km = Earth-Moon distance"],
160
+ },
161
+ "placement-semiMajor": {
162
+ description: "Semi-major axis of an orbit, used for elliptical orbits.",
163
+ references: ["1 au = Earth-Sun distance", "Use this instead of distance when orbit shape matters"],
164
+ },
165
+ "placement-eccentricity": {
166
+ description: "Controls how stretched the orbit is. Lower is rounder.",
167
+ references: ["0 = circular orbit", "0.1 = mildly elliptical"],
168
+ },
169
+ "placement-period": {
170
+ description: "How long one orbit takes.",
171
+ references: ["1 y = one Earth year", "27.3d = Moon around Earth"],
172
+ },
173
+ "placement-angle": {
174
+ description: "Rotates the orbit ellipse within the scene.",
175
+ references: ["0deg = default orientation", "90deg = quarter turn"],
176
+ },
177
+ "placement-inclination": {
178
+ description: "Tilts the orbit relative to the main orbital plane.",
179
+ references: ["0deg = same plane", "5deg = slight tilt"],
180
+ },
181
+ "placement-phase": {
182
+ description: "Starting position of the object along its orbit.",
183
+ references: ["0deg = start position", "180deg = opposite side"],
184
+ },
185
+ "pose-epoch": {
186
+ description: "Overrides the effective epoch for this pose only.",
187
+ references: ['"JY-0001.0"', "Falls back to event, object, then system"],
188
+ },
189
+ "pose-referencePlane": {
190
+ description: "Overrides the effective reference plane for this pose only.",
191
+ references: ["naar-equatorial", "Falls back to event, object, then system"],
192
+ },
193
+ "prop-radius": {
194
+ description: "Visual body size or real-world-inspired radius value.",
195
+ references: ["1re = Earth radius", "1sol = Sun radius"],
196
+ },
197
+ "prop-mass": {
198
+ description: "Optional mass value for the body.",
199
+ references: ["1me = Earth mass", "1sol = Sun mass"],
200
+ },
201
+ "prop-gravity": {
202
+ description: "Surface gravity or a custom gravity marker.",
203
+ references: ["1g = Earth-like gravity", "0.16g = Moon-like gravity"],
204
+ },
205
+ "prop-temperature": {
206
+ description: "Typical temperature or narrative temperature marker.",
207
+ references: ["288K = Earth-like average", "1200K = very hot world"],
208
+ },
209
+ "prop-atmosphere": {
210
+ description: "Short atmosphere descriptor for the object.",
211
+ references: ['"nitrogen-oxygen"', '"methane haze"'],
212
+ },
213
+ "prop-inner": {
214
+ description: "Inner edge for a belt, ring, or broad phenomenon.",
215
+ references: ["120000km = ring inner edge", "2au = belt starts here"],
216
+ },
217
+ "prop-outer": {
218
+ description: "Outer edge for a belt, ring, or broad phenomenon.",
219
+ references: ["190000km = ring outer edge", "3au = belt ends here"],
220
+ },
221
+ };
222
+ export function createWorldOrbitEditor(container, options = {}) {
223
+ ensureBrowserEnvironment(container);
224
+ installEditorStyles();
225
+ const initial = resolveInitialEditorState(options);
226
+ let atlasDocument = initial.atlasDocument;
227
+ let canonicalSource = initial.source;
228
+ let sourceText = canonicalSource;
229
+ let savedSource = canonicalSource;
230
+ let diagnostics = initial.diagnostics;
231
+ let selection = atlasDocument.objects[0]
232
+ ? { kind: "object", id: atlasDocument.objects[0].id }
233
+ : { kind: "system" };
234
+ const history = [];
235
+ const future = [];
236
+ let dragState = null;
237
+ let ignoreViewerSelection = false;
238
+ let destroyed = false;
239
+ let dirty = false;
240
+ let sourceInputTimer = null;
241
+ let previewTimer = null;
242
+ let lastPreviewSvg = "";
243
+ let lastPreviewMarkup = "";
244
+ const inspectorSectionState = new Map();
245
+ const showTextPane = options.showTextPane ?? true;
246
+ const showInspector = options.showInspector ?? true;
247
+ const showPreview = options.showPreview ?? true;
248
+ const shortcutsEnabled = options.shortcuts ?? true;
249
+ let viewMode = options.viewMode ?? "2d";
250
+ container.classList.add("wo-editor");
251
+ container.dataset.woShowInspector = String(showInspector);
252
+ container.dataset.woShowTextPane = String(showTextPane);
253
+ container.dataset.woShowPreview = String(showPreview);
254
+ container.innerHTML = buildEditorMarkup();
255
+ const toolbar = queryRequired(container, "[data-editor-toolbar]");
256
+ const outline = queryRequired(container, "[data-editor-outline]");
257
+ const stageShell = queryRequired(container, "[data-editor-stage-shell]");
258
+ const stage = queryRequired(container, "[data-editor-stage]");
259
+ const overlay = queryRequired(container, "[data-editor-overlay]");
260
+ const diagnosticsPanel = queryRequired(container, "[data-editor-diagnostics]");
261
+ const sourceDiagnostics = queryRequired(container, "[data-editor-source-diagnostics]");
262
+ const statusBar = queryRequired(container, "[data-editor-status]");
263
+ const liveRegion = queryRequired(container, "[data-editor-live]");
264
+ const inspector = queryRequired(container, "[data-editor-inspector]");
265
+ const sourcePane = queryRequired(container, "[data-editor-source]");
266
+ const previewVisual = queryRequired(container, "[data-editor-preview-visual]");
267
+ const previewMarkup = queryRequired(container, "[data-editor-preview-markup]");
268
+ let viewer = null;
269
+ viewer = createInteractiveViewer(stage, {
270
+ source: canonicalSource,
271
+ width: options.viewerWidth ?? 1120,
272
+ height: options.viewerHeight ?? 680,
273
+ preset: atlasDocument.system?.defaults.preset ?? "atlas-card",
274
+ projection: "document",
275
+ viewMode,
276
+ minimap: true,
277
+ tooltipMode: "hover",
278
+ onSelectionChange(selectedObject) {
279
+ const activeEventId = selection ? selectionEventId(selection) : null;
280
+ if (ignoreViewerSelection || !selectedObject) {
281
+ if (!ignoreViewerSelection) {
282
+ if (selection?.kind === "event-pose" && selection.id) {
283
+ setSelection({ kind: "event", id: selection.id }, false, true);
284
+ }
285
+ else if (selection?.kind === "object") {
286
+ setSelection(activeEventId ? { kind: "event", id: activeEventId } : null, false, true);
287
+ }
288
+ else if (selection?.kind === "event" && selection.id) {
289
+ setSelection({ kind: "event", id: selection.id }, false, true);
290
+ }
291
+ }
292
+ return;
293
+ }
294
+ if (activeEventId && findEventPose(atlasDocument, activeEventId, selectedObject.objectId)) {
295
+ setSelection({ kind: "event-pose", id: activeEventId, key: selectedObject.objectId }, false, true);
296
+ return;
297
+ }
298
+ setSelection({ kind: "object", id: selectedObject.objectId }, false, true);
299
+ },
300
+ onViewChange() {
301
+ renderStageOverlay();
302
+ },
303
+ });
304
+ toolbar.addEventListener("click", handleToolbarClick);
305
+ outline.addEventListener("click", handleOutlineClick);
306
+ overlay.addEventListener("pointerdown", handleOverlayPointerDown);
307
+ inspector?.addEventListener("click", handleInspectorClick);
308
+ inspector?.addEventListener("input", handleInspectorInput);
309
+ inspector?.addEventListener("change", handleInspectorChange);
310
+ sourcePane?.addEventListener("input", handleSourceInput);
311
+ sourcePane?.addEventListener("change", handleSourceCommit);
312
+ sourcePane?.addEventListener("blur", handleSourceCommit);
313
+ container.addEventListener("keydown", handleEditorKeyDown);
314
+ window.addEventListener("pointermove", handleWindowPointerMove);
315
+ window.addEventListener("pointerup", handleWindowPointerUp);
316
+ window.addEventListener("pointercancel", handleWindowPointerUp);
317
+ const api = {
318
+ setSource(nextSource) {
319
+ const applied = applySourceText(nextSource, false);
320
+ if (applied) {
321
+ resetHistory();
322
+ renderToolbar();
323
+ }
324
+ },
325
+ setAtlasDocument(document) {
326
+ replaceAtlasDocument(document, false, selection, false);
327
+ resetHistory();
328
+ renderToolbar();
329
+ },
330
+ getSource() {
331
+ return canonicalSource;
332
+ },
333
+ getAtlasDocument() {
334
+ return cloneAtlasDocument(atlasDocument);
335
+ },
336
+ getDiagnostics() {
337
+ return diagnostics.map(cloneResolvedDiagnostic);
338
+ },
339
+ getSelection() {
340
+ return selection ? { path: { ...selection } } : null;
341
+ },
342
+ getViewMode() {
343
+ return viewMode;
344
+ },
345
+ setViewMode(mode) {
346
+ const previousViewMode = viewMode;
347
+ try {
348
+ viewer.setViewMode(mode);
349
+ viewMode = mode;
350
+ renderStageOverlay();
351
+ renderPreviewNow();
352
+ }
353
+ catch (error) {
354
+ viewMode = previousViewMode;
355
+ throw error;
356
+ }
357
+ },
358
+ isDirty() {
359
+ return dirty;
360
+ },
361
+ markSaved() {
362
+ markSavedInternal(true);
363
+ },
364
+ selectPath(path) {
365
+ setSelection(path, true, true);
366
+ },
367
+ canUndo() {
368
+ return history.length > 0;
369
+ },
370
+ canRedo() {
371
+ return future.length > 0;
372
+ },
373
+ undo() {
374
+ const entry = history.pop();
375
+ if (!entry) {
376
+ return false;
377
+ }
378
+ future.unshift(createHistoryEntry());
379
+ restoreHistoryEntry(entry);
380
+ return true;
381
+ },
382
+ redo() {
383
+ const entry = future.shift();
384
+ if (!entry) {
385
+ return false;
386
+ }
387
+ history.push(createHistoryEntry());
388
+ restoreHistoryEntry(entry);
389
+ return true;
390
+ },
391
+ addObject(type = "planet") {
392
+ const id = createUniqueId(type, atlasDocument.objects.map((object) => object.id));
393
+ const created = createNewObject(type, id, atlasDocument);
394
+ const nextDocument = insertObject(atlasDocument, created);
395
+ replaceAtlasDocument(nextDocument, true, { kind: "object", id });
396
+ return id;
397
+ },
398
+ addEvent() {
399
+ const id = createUniqueId("event", atlasDocument.events.map((event) => event.id));
400
+ const created = {
401
+ id,
402
+ kind: "",
403
+ label: humanizeIdentifier(id),
404
+ summary: null,
405
+ targetObjectId: null,
406
+ participantObjectIds: [],
407
+ timing: null,
408
+ visibility: null,
409
+ epoch: null,
410
+ referencePlane: null,
411
+ tags: [],
412
+ color: null,
413
+ hidden: false,
414
+ positions: [],
415
+ };
416
+ const nextDocument = cloneAtlasDocument(atlasDocument);
417
+ nextDocument.events.push(created);
418
+ nextDocument.events.sort(compareEvents);
419
+ replaceAtlasDocument(nextDocument, true, { kind: "event", id });
420
+ return id;
421
+ },
422
+ addViewpoint() {
423
+ const id = createUniqueId("viewpoint", atlasDocument.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []);
424
+ const created = {
425
+ id,
426
+ label: humanizeIdentifier(id),
427
+ summary: "",
428
+ focusObjectId: null,
429
+ selectedObjectId: null,
430
+ events: [],
431
+ projection: atlasDocument.system?.defaults.view ?? "topdown",
432
+ preset: atlasDocument.system?.defaults.preset ?? null,
433
+ zoom: null,
434
+ rotationDeg: 0,
435
+ camera: null,
436
+ layers: {},
437
+ filter: null,
438
+ };
439
+ const nextDocument = cloneAtlasDocument(atlasDocument);
440
+ nextDocument.system?.viewpoints.push(created);
441
+ nextDocument.system?.viewpoints.sort((left, right) => left.id.localeCompare(right.id));
442
+ replaceAtlasDocument(nextDocument, true, { kind: "viewpoint", id });
443
+ return id;
444
+ },
445
+ addAnnotation() {
446
+ const id = createUniqueId("annotation", atlasDocument.system?.annotations.map((annotation) => annotation.id) ?? []);
447
+ const created = {
448
+ id,
449
+ label: humanizeIdentifier(id),
450
+ targetObjectId: null,
451
+ body: "",
452
+ tags: [],
453
+ sourceObjectId: null,
454
+ };
455
+ const nextDocument = cloneAtlasDocument(atlasDocument);
456
+ nextDocument.system?.annotations.push(created);
457
+ nextDocument.system?.annotations.sort((left, right) => left.id.localeCompare(right.id));
458
+ replaceAtlasDocument(nextDocument, true, { kind: "annotation", id });
459
+ return id;
460
+ },
461
+ addMetadata(key, value) {
462
+ const metadataKey = key?.trim() || createUniqueId("metadata", Object.keys(atlasDocument.system?.atlasMetadata ?? {}));
463
+ const nextDocument = cloneAtlasDocument(atlasDocument);
464
+ if (nextDocument.system) {
465
+ nextDocument.system.atlasMetadata[metadataKey] = value ?? "";
466
+ }
467
+ replaceAtlasDocument(nextDocument, true, { kind: "metadata", key: metadataKey });
468
+ return metadataKey;
469
+ },
470
+ removeSelection() {
471
+ if (!selection || selection.kind === "system" || selection.kind === "defaults") {
472
+ return false;
473
+ }
474
+ const nextDocument = removeSelectedNode(atlasDocument, selection);
475
+ replaceAtlasDocument(nextDocument, true, { kind: "system" });
476
+ return true;
477
+ },
478
+ exportSvg() {
479
+ return viewer.exportSvg();
480
+ },
481
+ exportEmbedMarkup() {
482
+ return buildEmbedMarkup(getCurrentSourceForExport(), atlasDocument, viewMode);
483
+ },
484
+ destroy() {
485
+ if (destroyed) {
486
+ return;
487
+ }
488
+ destroyed = true;
489
+ toolbar.removeEventListener("click", handleToolbarClick);
490
+ outline.removeEventListener("click", handleOutlineClick);
491
+ overlay.removeEventListener("pointerdown", handleOverlayPointerDown);
492
+ inspector?.removeEventListener("click", handleInspectorClick);
493
+ inspector?.removeEventListener("input", handleInspectorInput);
494
+ inspector?.removeEventListener("change", handleInspectorChange);
495
+ sourcePane?.removeEventListener("input", handleSourceInput);
496
+ sourcePane?.removeEventListener("change", handleSourceCommit);
497
+ sourcePane?.removeEventListener("blur", handleSourceCommit);
498
+ container.removeEventListener("keydown", handleEditorKeyDown);
499
+ window.removeEventListener("pointermove", handleWindowPointerMove);
500
+ window.removeEventListener("pointerup", handleWindowPointerUp);
501
+ window.removeEventListener("pointercancel", handleWindowPointerUp);
502
+ clearSourceInputTimer();
503
+ clearPreviewTimer();
504
+ viewer.destroy();
505
+ container.innerHTML = "";
506
+ container.classList.remove("wo-editor");
507
+ },
508
+ };
509
+ renderAll(true);
510
+ updateDirtyState(true);
511
+ return api;
512
+ function createHistoryEntry() {
513
+ return {
514
+ atlasDocument: cloneAtlasDocument(atlasDocument),
515
+ selection: selection ? { ...selection } : null,
516
+ source: canonicalSource,
517
+ };
518
+ }
519
+ function resetHistory() {
520
+ history.length = 0;
521
+ future.length = 0;
522
+ renderToolbar();
523
+ }
524
+ function clearSourceInputTimer() {
525
+ if (sourceInputTimer !== null) {
526
+ window.clearTimeout(sourceInputTimer);
527
+ sourceInputTimer = null;
528
+ }
529
+ }
530
+ function clearPreviewTimer() {
531
+ if (previewTimer !== null) {
532
+ window.clearTimeout(previewTimer);
533
+ previewTimer = null;
534
+ }
535
+ }
536
+ function getCurrentSourceForExport() {
537
+ if (dragState?.changed) {
538
+ return formatAtlasSource(atlasDocument);
539
+ }
540
+ return canonicalSource;
541
+ }
542
+ function updateDirtyState(forceEmit = false) {
543
+ const nextDirty = sourceText !== savedSource || canonicalSource !== savedSource || dragState?.changed === true;
544
+ if (!forceEmit && nextDirty === dirty) {
545
+ return;
546
+ }
547
+ dirty = nextDirty;
548
+ renderStatusBar();
549
+ options.onDirtyChange?.(dirty);
550
+ }
551
+ function markSavedInternal(emit = false) {
552
+ sourceText = canonicalSource;
553
+ renderSourcePane();
554
+ savedSource = canonicalSource;
555
+ updateDirtyState(emit);
556
+ }
557
+ function restoreHistoryEntry(entry) {
558
+ clearSourceInputTimer();
559
+ atlasDocument = cloneAtlasDocument(entry.atlasDocument);
560
+ canonicalSource = entry.source;
561
+ sourceText = canonicalSource;
562
+ diagnostics = collectDocumentDiagnostics(atlasDocument);
563
+ selection = normalizeSelection(entry.selection);
564
+ syncViewer({ preserveCamera: true, applyViewpointSelection: selection?.kind === "viewpoint" });
565
+ setSelection(selection, false, false);
566
+ renderAll();
567
+ updateDirtyState();
568
+ emitSnapshot();
569
+ }
570
+ function replaceAtlasDocument(nextDocument, pushHistory, nextSelection, preserveSourceText = false) {
571
+ if (pushHistory) {
572
+ history.push(createHistoryEntry());
573
+ future.length = 0;
574
+ }
575
+ clearSourceInputTimer();
576
+ atlasDocument = cloneAtlasDocument(nextDocument);
577
+ canonicalSource = formatAtlasSource(atlasDocument);
578
+ if (!preserveSourceText) {
579
+ sourceText = canonicalSource;
580
+ }
581
+ diagnostics = collectDocumentDiagnostics(atlasDocument);
582
+ selection = normalizeSelection(nextSelection);
583
+ syncViewer({
584
+ preserveCamera: selection?.kind !== "viewpoint",
585
+ applyViewpointSelection: selection?.kind === "viewpoint",
586
+ });
587
+ setSelection(selection, false, false);
588
+ renderAll();
589
+ updateDirtyState();
590
+ emitSnapshot();
591
+ }
592
+ function applySourceText(nextSourceText, commitHistory) {
593
+ sourceText = nextSourceText;
594
+ if (sourcePane && sourcePane.value !== nextSourceText) {
595
+ sourcePane.value = nextSourceText;
596
+ }
597
+ const loaded = loadWorldOrbitSourceWithDiagnostics(nextSourceText);
598
+ if (!loaded.ok || !loaded.value) {
599
+ diagnostics = loaded.diagnostics.map((diagnostic) => ({
600
+ diagnostic,
601
+ path: null,
602
+ }));
603
+ renderDiagnostics();
604
+ renderSourceDiagnostics();
605
+ renderOutline();
606
+ renderInspector();
607
+ renderStatusBar();
608
+ updateLiveRegion();
609
+ updateDirtyState();
610
+ options.onDiagnosticsChange?.(diagnostics.map(cloneResolvedDiagnostic));
611
+ return false;
612
+ }
613
+ const nextDocument = loaded.value.atlasDocument ?? upgradeDocumentToV2(loaded.value.document);
614
+ const loadedDiagnostics = resolveAtlasDiagnostics(nextDocument, loaded.diagnostics);
615
+ if (commitHistory) {
616
+ history.push(createHistoryEntry());
617
+ future.length = 0;
618
+ sourceText = formatAtlasSource(nextDocument);
619
+ }
620
+ atlasDocument = cloneAtlasDocument(nextDocument);
621
+ canonicalSource = formatAtlasSource(atlasDocument);
622
+ diagnostics = mergeDiagnostics(loadedDiagnostics, collectDocumentDiagnostics(atlasDocument));
623
+ selection = normalizeSelection(selection);
624
+ syncViewer({
625
+ preserveCamera: selection?.kind !== "viewpoint",
626
+ applyViewpointSelection: selection?.kind === "viewpoint",
627
+ });
628
+ renderAll();
629
+ updateDirtyState();
630
+ emitSnapshot();
631
+ return true;
632
+ }
633
+ function syncViewer(options = {}) {
634
+ if (!viewer) {
635
+ return;
636
+ }
637
+ const previousState = viewer.getState();
638
+ const currentRenderOptions = viewer.getRenderOptions();
639
+ const nextPreset = atlasDocument.system?.defaults.preset ?? "atlas-card";
640
+ const nextActiveEventId = selection ? selectionEventId(selection) : null;
641
+ ignoreViewerSelection = true;
642
+ if (currentRenderOptions.preset !== nextPreset ||
643
+ currentRenderOptions.projection !== "document" ||
644
+ (currentRenderOptions.activeEventId ?? null) !== nextActiveEventId) {
645
+ viewer.setRenderOptions({
646
+ preset: nextPreset,
647
+ projection: "document",
648
+ activeEventId: nextActiveEventId,
649
+ });
650
+ }
651
+ viewer.setDocument(materializeAtlasDocument(atlasDocument));
652
+ if (options.applyViewpointSelection && selection?.kind === "viewpoint" && selection.id) {
653
+ viewer.goToViewpoint(selection.id);
654
+ }
655
+ else if (options.preserveCamera !== false) {
656
+ viewer.setState({
657
+ ...previousState,
658
+ selectedObjectId: selection?.kind === "object"
659
+ ? selection.id ?? null
660
+ : selection?.kind === "event-pose"
661
+ ? selection.key ?? null
662
+ : null,
663
+ });
664
+ }
665
+ else if (selection?.kind === "object" && selection.id) {
666
+ viewer.focusObject(selection.id);
667
+ }
668
+ else if (selection?.kind === "event-pose" && selection.key) {
669
+ viewer.focusObject(selection.key);
670
+ }
671
+ ignoreViewerSelection = false;
672
+ }
673
+ function emitSnapshot() {
674
+ const snapshot = {
675
+ source: canonicalSource,
676
+ atlasDocument: cloneAtlasDocument(atlasDocument),
677
+ diagnostics: diagnostics.map(cloneResolvedDiagnostic),
678
+ selection: selection ? { path: { ...selection } } : null,
679
+ };
680
+ options.onDiagnosticsChange?.(snapshot.diagnostics);
681
+ options.onSelectionChange?.(snapshot.selection);
682
+ options.onChange?.(snapshot);
683
+ }
684
+ function setSelection(nextSelection, syncViewerSelection, emit = true) {
685
+ selection = normalizeSelection(nextSelection);
686
+ if (syncViewerSelection) {
687
+ ignoreViewerSelection = true;
688
+ viewer.setRenderOptions({ activeEventId: selection ? selectionEventId(selection) : null });
689
+ if (selection?.kind === "object" && selection.id) {
690
+ viewer.focusObject(selection.id);
691
+ }
692
+ else if (selection?.kind === "event-pose" && selection.key) {
693
+ viewer.focusObject(selection.key);
694
+ }
695
+ else if (selection?.kind === "viewpoint" && selection.id) {
696
+ viewer.goToViewpoint(selection.id);
697
+ }
698
+ ignoreViewerSelection = false;
699
+ }
700
+ renderToolbar();
701
+ renderOutline();
702
+ if (!inspector?.contains(document.activeElement)) {
703
+ renderInspector();
704
+ }
705
+ renderStageOverlay();
706
+ renderStatusBar();
707
+ updateLiveRegion();
708
+ if (emit) {
709
+ options.onSelectionChange?.(selection ? { path: { ...selection } } : null);
710
+ }
711
+ }
712
+ function normalizeSelection(nextSelection) {
713
+ if (!nextSelection) {
714
+ return null;
715
+ }
716
+ const node = getAtlasDocumentNode(atlasDocument, nextSelection);
717
+ if (node === null || node === undefined) {
718
+ return nextSelection.kind === "system" || nextSelection.kind === "defaults"
719
+ ? nextSelection
720
+ : null;
721
+ }
722
+ return nextSelection;
723
+ }
724
+ function renderAll(immediatePreview = false) {
725
+ renderToolbar();
726
+ renderOutline();
727
+ renderDiagnostics();
728
+ renderSourceDiagnostics();
729
+ // Do not rebuild the inspector DOM if the user is currently typing in it,
730
+ // to prevent losing focus/cursor position and making input glitchy.
731
+ if (!inspector?.contains(document.activeElement)) {
732
+ renderInspector();
733
+ }
734
+ renderSourcePane();
735
+ renderPreview(immediatePreview);
736
+ renderStageOverlay();
737
+ renderStatusBar();
738
+ updateLiveRegion();
739
+ }
740
+ function renderToolbar() {
741
+ const objectType = toolbar.querySelector("[data-editor-add-object-type]")
742
+ ?.value ?? "planet";
743
+ toolbar.innerHTML = `
744
+ <div class="wo-editor-toolbar-group">
745
+ <select data-editor-add-object-type>
746
+ ${OBJECT_TYPES.map((type) => `<option value="${escapeHtml(type)}"${type === objectType ? " selected" : ""}>${escapeHtml(humanizeIdentifier(type))}</option>`).join("")}
747
+ </select>
748
+ <button type="button" data-editor-action="add-object">Add object</button>
749
+ <button type="button" data-editor-action="add-event">Add event</button>
750
+ <button type="button" data-editor-action="add-viewpoint">Add viewpoint</button>
751
+ <button type="button" data-editor-action="add-annotation">Add annotation</button>
752
+ <button type="button" data-editor-action="add-metadata">Add metadata</button>
753
+ </div>
754
+ <div class="wo-editor-toolbar-group">
755
+ <button type="button" data-editor-action="remove"${!selection || selection.kind === "system" || selection.kind === "defaults" ? " disabled" : ""}>Remove</button>
756
+ <button type="button" data-editor-action="undo"${history.length === 0 ? " disabled" : ""}>Undo</button>
757
+ <button type="button" data-editor-action="redo"${future.length === 0 ? " disabled" : ""}>Redo</button>
758
+ <button type="button" data-editor-action="format">Format source</button>
759
+ </div>
760
+ `;
761
+ }
762
+ function renderOutline() {
763
+ const activeKey = selectionKey(selection);
764
+ const diagnosticBuckets = buildDiagnosticBuckets(diagnostics);
765
+ const metadataEntries = Object.entries(atlasDocument.system?.atlasMetadata ?? {}).sort(([left], [right]) => left.localeCompare(right));
766
+ outline.innerHTML = `
767
+ <div class="wo-editor-outline-section">
768
+ <h3>Atlas</h3>
769
+ ${renderOutlineButton({ kind: "system" }, "System", activeKey, diagnosticBuckets)}
770
+ ${renderOutlineButton({ kind: "defaults" }, "Defaults", activeKey, diagnosticBuckets)}
771
+ </div>
772
+ <div class="wo-editor-outline-section">
773
+ <h3>Metadata</h3>
774
+ ${metadataEntries.length > 0
775
+ ? metadataEntries
776
+ .map(([key]) => renderOutlineButton({ kind: "metadata", key }, key, activeKey, diagnosticBuckets))
777
+ .join("")
778
+ : `<p class="wo-editor-empty">No atlas metadata yet.</p>`}
779
+ </div>
780
+ <div class="wo-editor-outline-section">
781
+ <h3>Viewpoints</h3>
782
+ ${(atlasDocument.system?.viewpoints.length ?? 0) > 0
783
+ ? atlasDocument.system?.viewpoints
784
+ .map((viewpoint) => renderOutlineButton({ kind: "viewpoint", id: viewpoint.id }, viewpoint.label, activeKey, diagnosticBuckets))
785
+ .join("")
786
+ : `<p class="wo-editor-empty">No viewpoints yet.</p>`}
787
+ </div>
788
+ <div class="wo-editor-outline-section">
789
+ <h3>Annotations</h3>
790
+ ${(atlasDocument.system?.annotations.length ?? 0) > 0
791
+ ? atlasDocument.system?.annotations
792
+ .map((annotation) => renderOutlineButton({ kind: "annotation", id: annotation.id }, annotation.label, activeKey, diagnosticBuckets))
793
+ .join("")
794
+ : `<p class="wo-editor-empty">No annotations yet.</p>`}
795
+ </div>
796
+ <div class="wo-editor-outline-section">
797
+ <h3>Events</h3>
798
+ ${atlasDocument.events.length > 0
799
+ ? atlasDocument.events
800
+ .map((eventEntry) => renderEventOutlineItems(eventEntry, activeKey, diagnosticBuckets))
801
+ .join("")
802
+ : `<p class="wo-editor-empty">No events yet.</p>`}
803
+ </div>
804
+ <div class="wo-editor-outline-section">
805
+ <h3>Objects</h3>
806
+ ${atlasDocument.objects.length > 0
807
+ ? atlasDocument.objects
808
+ .map((object) => renderOutlineButton({ kind: "object", id: object.id }, `${object.id} - ${object.type}`, activeKey, diagnosticBuckets))
809
+ .join("")
810
+ : `<p class="wo-editor-empty">No objects yet.</p>`}
811
+ </div>
812
+ `;
813
+ }
814
+ function renderDiagnostics() {
815
+ diagnosticsPanel.innerHTML =
816
+ diagnostics.length === 0
817
+ ? `<p class="wo-editor-empty">No diagnostics.</p>`
818
+ : diagnostics
819
+ .map((entry) => {
820
+ const pathLabel = entry.path ? describePath(entry.path) : "Source";
821
+ const location = formatDiagnosticLocation(entry);
822
+ return `<article class="wo-editor-diagnostic wo-editor-diagnostic-${escapeHtml(entry.diagnostic.severity)}">
823
+ <strong>${escapeHtml(entry.diagnostic.severity.toUpperCase())}</strong>
824
+ <span>${escapeHtml(pathLabel)}${location ? ` · ${escapeHtml(location)}` : ""}</span>
825
+ <p>${escapeHtml(entry.diagnostic.message)}</p>
826
+ </article>`;
827
+ })
828
+ .join("");
829
+ }
830
+ function renderSourceDiagnostics() {
831
+ const sourceEntries = diagnostics.filter((entry) => !entry.path || entry.diagnostic.line !== undefined);
832
+ sourceDiagnostics.innerHTML =
833
+ sourceEntries.length === 0
834
+ ? `<p class="wo-editor-empty">No source diagnostics.</p>`
835
+ : sourceEntries
836
+ .map((entry) => {
837
+ const location = formatDiagnosticLocation(entry) ?? "Source";
838
+ return `<article class="wo-editor-diagnostic wo-editor-diagnostic-${escapeHtml(entry.diagnostic.severity)}">
839
+ <strong>${escapeHtml(entry.diagnostic.severity.toUpperCase())}</strong>
840
+ <span>${escapeHtml(location)}</span>
841
+ <p>${escapeHtml(entry.diagnostic.message)}</p>
842
+ </article>`;
843
+ })
844
+ .join("");
845
+ }
846
+ function renderInspector() {
847
+ if (!inspector) {
848
+ return;
849
+ }
850
+ captureInspectorSectionState(inspector, inspectorSectionState);
851
+ const formState = {
852
+ selection: selection ? { path: { ...selection } } : null,
853
+ system: atlasDocument.system,
854
+ viewpoints: atlasDocument.system?.viewpoints ?? [],
855
+ events: atlasDocument.events,
856
+ objects: atlasDocument.objects,
857
+ };
858
+ if (!selection) {
859
+ inspector.innerHTML = `<p class="wo-editor-empty">Select an atlas node or object to edit it.</p>`;
860
+ return;
861
+ }
862
+ const diagnosticSummary = renderInspectorDiagnosticSummary(selection, diagnostics);
863
+ switch (selection.kind) {
864
+ case "system":
865
+ inspector.innerHTML = diagnosticSummary + renderSystemInspector(formState);
866
+ applyInspectorSectionState(inspector, inspectorSectionState);
867
+ decorateInspectorDiagnostics(selection, diagnostics);
868
+ return;
869
+ case "defaults":
870
+ inspector.innerHTML = diagnosticSummary + renderDefaultsInspector(formState);
871
+ applyInspectorSectionState(inspector, inspectorSectionState);
872
+ decorateInspectorDiagnostics(selection, diagnostics);
873
+ return;
874
+ case "metadata":
875
+ inspector.innerHTML = diagnosticSummary + renderMetadataInspector(formState, selection.key ?? "");
876
+ applyInspectorSectionState(inspector, inspectorSectionState);
877
+ decorateInspectorDiagnostics(selection, diagnostics);
878
+ return;
879
+ case "viewpoint":
880
+ inspector.innerHTML = diagnosticSummary + renderViewpointInspector(formState, selection.id ?? "");
881
+ applyInspectorSectionState(inspector, inspectorSectionState);
882
+ decorateInspectorDiagnostics(selection, diagnostics);
883
+ return;
884
+ case "event":
885
+ inspector.innerHTML = diagnosticSummary + renderEventInspector(formState, selection.id ?? "");
886
+ applyInspectorSectionState(inspector, inspectorSectionState);
887
+ decorateInspectorDiagnostics(selection, diagnostics);
888
+ return;
889
+ case "event-pose":
890
+ inspector.innerHTML =
891
+ diagnosticSummary + renderEventPoseInspector(formState, selection.id ?? "", selection.key ?? "");
892
+ applyInspectorSectionState(inspector, inspectorSectionState);
893
+ decorateInspectorDiagnostics(selection, diagnostics);
894
+ return;
895
+ case "annotation":
896
+ inspector.innerHTML = diagnosticSummary + renderAnnotationInspector(formState, selection.id ?? "");
897
+ applyInspectorSectionState(inspector, inspectorSectionState);
898
+ decorateInspectorDiagnostics(selection, diagnostics);
899
+ return;
900
+ case "object":
901
+ inspector.innerHTML = diagnosticSummary + renderObjectInspector(formState, selection.id ?? "");
902
+ applyInspectorSectionState(inspector, inspectorSectionState);
903
+ decorateInspectorDiagnostics(selection, diagnostics);
904
+ return;
905
+ }
906
+ }
907
+ function renderSourcePane() {
908
+ if (!sourcePane) {
909
+ return;
910
+ }
911
+ if (sourcePane.value !== sourceText) {
912
+ sourcePane.value = sourceText;
913
+ }
914
+ }
915
+ function renderPreview(immediate = false) {
916
+ if (immediate) {
917
+ renderPreviewNow();
918
+ return;
919
+ }
920
+ schedulePreviewRender();
921
+ }
922
+ function renderStageOverlay() {
923
+ if (!viewer) {
924
+ return;
925
+ }
926
+ overlay.innerHTML = "";
927
+ if (viewMode === "3d") {
928
+ return;
929
+ }
930
+ const selectedObjectId = selection?.kind === "object"
931
+ ? selection.id ?? null
932
+ : selection?.kind === "event-pose"
933
+ ? selection.key ?? null
934
+ : null;
935
+ if (!selectedObjectId) {
936
+ return;
937
+ }
938
+ const details = viewer.getObjectDetails(selectedObjectId);
939
+ if (!details) {
940
+ return;
941
+ }
942
+ renderHintMarker(details.renderObject.x, details.renderObject.y, details.renderObject.objectId);
943
+ if (details.parent) {
944
+ renderHintMarker(details.parent.x, details.parent.y, `Parent: ${details.parent.objectId}`, true);
945
+ }
946
+ if (details.renderObject.anchorX !== undefined && details.renderObject.anchorY !== undefined) {
947
+ renderHintMarker(details.renderObject.anchorX, details.renderObject.anchorY, "Anchor", true);
948
+ }
949
+ if (details.object.placement?.mode === "orbit" && details.orbit) {
950
+ const phasePoint = projectStagePoint({
951
+ x: details.renderObject.x,
952
+ y: details.renderObject.y,
953
+ });
954
+ overlay.append(createHandleElement("orbit-phase", details.objectId, phasePoint, "Phase"));
955
+ const axisPoint = projectOrbitRadiusHandle(details);
956
+ overlay.append(createHandleElement("orbit-radius", details.objectId, axisPoint, "Size"));
957
+ }
958
+ if (details.object.placement?.mode === "at") {
959
+ overlay.append(createHandleElement("at-reference", details.objectId, projectStagePoint({ x: details.renderObject.x, y: details.renderObject.y }), "Reference"));
960
+ const badge = document.createElement("div");
961
+ badge.className = "wo-editor-hint wo-editor-hint-note";
962
+ badge.textContent = "Drag to an object center or nearby Lagrange point.";
963
+ overlay.append(badge);
964
+ }
965
+ if (details.object.placement?.mode === "surface") {
966
+ overlay.append(createHandleElement("surface-target", details.objectId, projectStagePoint({ x: details.renderObject.x, y: details.renderObject.y }), "Surface"));
967
+ const badge = document.createElement("div");
968
+ badge.className = "wo-editor-hint wo-editor-hint-note";
969
+ badge.textContent = "Drag onto another surface-capable body.";
970
+ overlay.append(badge);
971
+ }
972
+ if (details.object.placement?.mode === "free") {
973
+ overlay.append(createHandleElement("free-distance", details.objectId, projectStagePoint({ x: details.renderObject.x, y: details.renderObject.y }), "Offset"));
974
+ const badge = document.createElement("div");
975
+ badge.className = "wo-editor-hint wo-editor-hint-note";
976
+ badge.textContent = "Drag horizontally to change free offset.";
977
+ overlay.append(badge);
978
+ }
979
+ const placementDiagnostics = getPlacementDiagnosticsForSelection(selection, diagnostics).slice(0, 3);
980
+ if (placementDiagnostics.length > 0) {
981
+ const panel = document.createElement("div");
982
+ panel.className = "wo-editor-overlay-diagnostics";
983
+ panel.setAttribute("role", "status");
984
+ panel.setAttribute("aria-live", "polite");
985
+ panel.innerHTML = placementDiagnostics
986
+ .map((entry) => `<article class="wo-editor-overlay-diagnostic wo-editor-overlay-diagnostic-${escapeHtml(entry.diagnostic.severity)}">
987
+ <strong>${escapeHtml(entry.diagnostic.severity.toUpperCase())}</strong>
988
+ <p>${escapeHtml(entry.diagnostic.message)}</p>
989
+ </article>`)
990
+ .join("");
991
+ overlay.append(panel);
992
+ }
993
+ }
994
+ function renderHintMarker(worldX, worldY, label, subtle = false) {
995
+ const point = projectStagePoint({ x: worldX, y: worldY });
996
+ const marker = document.createElement("div");
997
+ marker.className = `wo-editor-hint${subtle ? " is-subtle" : ""}`;
998
+ marker.style.left = `${point.x}px`;
999
+ marker.style.top = `${point.y}px`;
1000
+ marker.textContent = label;
1001
+ overlay.append(marker);
1002
+ }
1003
+ function projectOrbitRadiusHandle(details) {
1004
+ const orbit = details.orbit;
1005
+ if (!orbit) {
1006
+ return projectStagePoint({ x: details.renderObject.x, y: details.renderObject.y });
1007
+ }
1008
+ const localPoint = {
1009
+ x: orbit.cx + (orbit.kind === "circle" ? orbit.radius ?? 0 : orbit.rx ?? 0),
1010
+ y: orbit.cy,
1011
+ };
1012
+ const rotatedPoint = rotatePoint(localPoint, { x: orbit.cx, y: orbit.cy }, orbit.rotationDeg);
1013
+ return projectStagePoint(rotatedPoint);
1014
+ }
1015
+ function projectStagePoint(point) {
1016
+ const scene = viewer.getScene();
1017
+ const state = viewer.getState();
1018
+ const center = {
1019
+ x: scene.width / 2,
1020
+ y: scene.height / 2,
1021
+ };
1022
+ const rotated = rotatePoint(point, center, state.rotationDeg);
1023
+ const viewportPoint = {
1024
+ x: center.x + (rotated.x - center.x) * state.scale + state.translateX,
1025
+ y: center.y + (rotated.y - center.y) * state.scale + state.translateY,
1026
+ };
1027
+ const svg = stage.querySelector("svg");
1028
+ if (!svg) {
1029
+ return viewportPoint;
1030
+ }
1031
+ const svgRect = svg.getBoundingClientRect();
1032
+ const stageRect = stageShell.getBoundingClientRect();
1033
+ return {
1034
+ x: svgRect.left -
1035
+ stageRect.left +
1036
+ (viewportPoint.x / Math.max(scene.width, 1)) * svgRect.width,
1037
+ y: svgRect.top -
1038
+ stageRect.top +
1039
+ (viewportPoint.y / Math.max(scene.height, 1)) * svgRect.height,
1040
+ };
1041
+ }
1042
+ function handleToolbarClick(event) {
1043
+ const button = event.target?.closest("[data-editor-action]");
1044
+ if (!button) {
1045
+ return;
1046
+ }
1047
+ switch (button.dataset.editorAction) {
1048
+ case "add-object": {
1049
+ const type = toolbar.querySelector("[data-editor-add-object-type]")
1050
+ ?.value;
1051
+ api.addObject(type ?? "planet");
1052
+ return;
1053
+ }
1054
+ case "add-viewpoint":
1055
+ api.addViewpoint();
1056
+ return;
1057
+ case "add-event":
1058
+ api.addEvent();
1059
+ return;
1060
+ case "add-annotation":
1061
+ api.addAnnotation();
1062
+ return;
1063
+ case "add-metadata":
1064
+ api.addMetadata();
1065
+ return;
1066
+ case "remove":
1067
+ api.removeSelection();
1068
+ return;
1069
+ case "undo":
1070
+ api.undo();
1071
+ return;
1072
+ case "redo":
1073
+ api.redo();
1074
+ return;
1075
+ case "format":
1076
+ clearSourceInputTimer();
1077
+ sourceText = canonicalSource;
1078
+ renderSourcePane();
1079
+ updateDirtyState();
1080
+ return;
1081
+ }
1082
+ }
1083
+ function handleOutlineClick(event) {
1084
+ const button = event.target?.closest("[data-path-kind]");
1085
+ if (!button) {
1086
+ return;
1087
+ }
1088
+ setSelection({
1089
+ kind: button.dataset.pathKind,
1090
+ id: button.dataset.pathId || undefined,
1091
+ key: button.dataset.pathKey || undefined,
1092
+ }, true, true);
1093
+ }
1094
+ function handleInspectorClick(event) {
1095
+ const pathButton = event.target?.closest("[data-path-kind]");
1096
+ if (pathButton) {
1097
+ setSelection({
1098
+ kind: pathButton.dataset.pathKind,
1099
+ id: pathButton.dataset.pathId || undefined,
1100
+ key: pathButton.dataset.pathKey || undefined,
1101
+ }, true, true);
1102
+ return;
1103
+ }
1104
+ const actionButton = event.target?.closest("[data-editor-action]");
1105
+ if (!actionButton) {
1106
+ return;
1107
+ }
1108
+ if (actionButton.dataset.editorAction === "add-event-pose") {
1109
+ const eventId = actionButton.dataset.editorEventId ||
1110
+ (selection?.kind === "event" || selection?.kind === "event-pose" ? selection.id ?? "" : "");
1111
+ if (!eventId) {
1112
+ return;
1113
+ }
1114
+ const nextDocument = addEventPose(atlasDocument, eventId);
1115
+ const createdEvent = nextDocument.events.find((entry) => entry.id === eventId);
1116
+ const createdPose = createdEvent?.positions.at(-1) ?? createdEvent?.positions[0];
1117
+ replaceAtlasDocument(nextDocument, true, createdPose
1118
+ ? { kind: "event-pose", id: eventId, key: createdPose.objectId }
1119
+ : { kind: "event", id: eventId });
1120
+ return;
1121
+ }
1122
+ }
1123
+ function handleInspectorInput() {
1124
+ applyInspectorState(false);
1125
+ }
1126
+ function handleInspectorChange() {
1127
+ applyInspectorState(true);
1128
+ }
1129
+ function applyInspectorState(commitHistory) {
1130
+ if (!selection || !inspector) {
1131
+ return;
1132
+ }
1133
+ switch (selection.kind) {
1134
+ case "system":
1135
+ replaceAtlasDocument(buildSystemDocumentFromInspector(), commitHistory, selection, false);
1136
+ return;
1137
+ case "defaults":
1138
+ replaceAtlasDocument(buildDefaultsDocumentFromInspector(), commitHistory, selection, false);
1139
+ return;
1140
+ case "metadata":
1141
+ replaceAtlasDocument(buildMetadataDocumentFromInspector(selection.key ?? ""), commitHistory, selection, false);
1142
+ return;
1143
+ case "viewpoint":
1144
+ replaceAtlasDocument(buildViewpointDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
1145
+ return;
1146
+ case "event":
1147
+ replaceAtlasDocument(buildEventDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
1148
+ return;
1149
+ case "event-pose":
1150
+ replaceAtlasDocument(buildEventPoseDocumentFromInspector(selection.id ?? "", selection.key ?? ""), commitHistory, selection, false);
1151
+ return;
1152
+ case "annotation":
1153
+ replaceAtlasDocument(buildAnnotationDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
1154
+ return;
1155
+ case "object":
1156
+ replaceAtlasDocument(buildObjectDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
1157
+ return;
1158
+ }
1159
+ }
1160
+ function handleSourceInput() {
1161
+ sourceText = sourcePane?.value ?? "";
1162
+ updateDirtyState();
1163
+ clearSourceInputTimer();
1164
+ sourceInputTimer = window.setTimeout(() => {
1165
+ sourceInputTimer = null;
1166
+ applySourceText(sourceText, false);
1167
+ }, SOURCE_INPUT_DEBOUNCE_MS);
1168
+ }
1169
+ function handleSourceCommit() {
1170
+ clearSourceInputTimer();
1171
+ const committed = applySourceText(sourcePane?.value ?? "", true);
1172
+ if (committed) {
1173
+ renderSourcePane();
1174
+ }
1175
+ }
1176
+ function handleEditorKeyDown(event) {
1177
+ if (event.key === "Escape" && dragState) {
1178
+ event.preventDefault();
1179
+ cancelActiveDrag();
1180
+ return;
1181
+ }
1182
+ if (!shortcutsEnabled || event.defaultPrevented || shouldIgnoreShortcutEvent(event)) {
1183
+ return;
1184
+ }
1185
+ const isUndo = (event.ctrlKey || event.metaKey) && !event.shiftKey && event.key.toLowerCase() === "z";
1186
+ const isRedo = (event.ctrlKey || event.metaKey) &&
1187
+ ((event.shiftKey && event.key.toLowerCase() === "z") || event.key.toLowerCase() === "y");
1188
+ if (isUndo) {
1189
+ event.preventDefault();
1190
+ api.undo();
1191
+ return;
1192
+ }
1193
+ if (isRedo) {
1194
+ event.preventDefault();
1195
+ api.redo();
1196
+ }
1197
+ }
1198
+ function handleOverlayPointerDown(event) {
1199
+ const handle = event.target?.closest("[data-handle-kind]");
1200
+ if (!handle) {
1201
+ return;
1202
+ }
1203
+ const objectId = handle.dataset.objectId;
1204
+ const kind = handle.dataset.handleKind;
1205
+ if (!objectId ||
1206
+ !["orbit-phase", "orbit-radius", "at-reference", "surface-target", "free-distance"].includes(kind)) {
1207
+ return;
1208
+ }
1209
+ clearSourceInputTimer();
1210
+ if (sourceText !== canonicalSource) {
1211
+ const committed = applySourceText(sourceText, true);
1212
+ if (!committed) {
1213
+ event.preventDefault();
1214
+ return;
1215
+ }
1216
+ }
1217
+ const details = viewer?.getObjectDetails(objectId) ?? null;
1218
+ dragState = {
1219
+ kind,
1220
+ objectId,
1221
+ pointerId: event.pointerId,
1222
+ path: selection ? { ...selection } : { kind: "object", id: objectId },
1223
+ startedFrom: createHistoryEntry(),
1224
+ changed: false,
1225
+ orbitRadiusContext: kind === "orbit-radius" && details
1226
+ ? createOrbitRadiusDragContext(atlasDocument, viewer.getScene(), details)
1227
+ : null,
1228
+ };
1229
+ handle.setPointerCapture?.(event.pointerId);
1230
+ event.preventDefault();
1231
+ }
1232
+ function handleWindowPointerMove(event) {
1233
+ if (!dragState ||
1234
+ dragState.pointerId !== event.pointerId ||
1235
+ !selection ||
1236
+ selectionKey(selection) !== selectionKey(dragState.path)) {
1237
+ return;
1238
+ }
1239
+ const details = viewer.getObjectDetails(dragState.objectId);
1240
+ if (!details) {
1241
+ return;
1242
+ }
1243
+ const pointer = getWorldPointFromClient(event.clientX, event.clientY);
1244
+ let nextDocument = atlasDocument;
1245
+ switch (dragState.kind) {
1246
+ case "orbit-phase":
1247
+ if (details.object.placement?.mode === "orbit" && details.orbit) {
1248
+ nextDocument = updateOrbitPhase(atlasDocument, dragState.path, dragState.objectId, details, pointer);
1249
+ }
1250
+ break;
1251
+ case "orbit-radius":
1252
+ if (details.object.placement?.mode === "orbit" && details.orbit) {
1253
+ nextDocument = updateOrbitRadius(atlasDocument, dragState.path, dragState.objectId, details, pointer, dragState.orbitRadiusContext ?? null);
1254
+ }
1255
+ break;
1256
+ case "at-reference":
1257
+ if (details.object.placement?.mode === "at") {
1258
+ nextDocument = updateAtReference(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), pointer);
1259
+ }
1260
+ break;
1261
+ case "surface-target":
1262
+ if (details.object.placement?.mode === "surface") {
1263
+ nextDocument = updateSurfaceTarget(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), pointer);
1264
+ }
1265
+ break;
1266
+ case "free-distance":
1267
+ if (details.object.placement?.mode === "free") {
1268
+ nextDocument = updateFreeDistance(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), details, pointer);
1269
+ }
1270
+ break;
1271
+ }
1272
+ if (nextDocument === atlasDocument) {
1273
+ return;
1274
+ }
1275
+ dragState.changed = true;
1276
+ atlasDocument = cloneAtlasDocument(nextDocument);
1277
+ diagnostics = collectDocumentDiagnostics(atlasDocument);
1278
+ syncViewer({ preserveCamera: true, applyViewpointSelection: false });
1279
+ renderDiagnostics();
1280
+ renderSourceDiagnostics();
1281
+ renderOutline();
1282
+ renderInspector();
1283
+ renderStageOverlay();
1284
+ renderStatusBar();
1285
+ updateLiveRegion();
1286
+ schedulePreviewRender();
1287
+ updateDirtyState();
1288
+ }
1289
+ function handleWindowPointerUp(event) {
1290
+ if (!dragState || dragState.pointerId !== event.pointerId) {
1291
+ return;
1292
+ }
1293
+ const completedDrag = dragState;
1294
+ if (!dragState.changed) {
1295
+ dragState = null;
1296
+ return;
1297
+ }
1298
+ history.push(dragState.startedFrom);
1299
+ future.length = 0;
1300
+ canonicalSource = formatAtlasSource(atlasDocument);
1301
+ sourceText = canonicalSource;
1302
+ dragState = null;
1303
+ renderAll();
1304
+ if (completedDrag.kind === "orbit-radius") {
1305
+ maybeFitOrbitResizeInView(completedDrag.objectId);
1306
+ }
1307
+ updateDirtyState();
1308
+ emitSnapshot();
1309
+ }
1310
+ function cancelActiveDrag() {
1311
+ if (!dragState) {
1312
+ return;
1313
+ }
1314
+ const startedFrom = dragState.startedFrom;
1315
+ dragState = null;
1316
+ atlasDocument = cloneAtlasDocument(startedFrom.atlasDocument);
1317
+ canonicalSource = startedFrom.source;
1318
+ sourceText = canonicalSource;
1319
+ diagnostics = collectDocumentDiagnostics(atlasDocument);
1320
+ setSelection(startedFrom.selection, false, false);
1321
+ syncViewer({ preserveCamera: true, applyViewpointSelection: selection?.kind === "viewpoint" });
1322
+ renderAll();
1323
+ updateDirtyState();
1324
+ emitSnapshot();
1325
+ }
1326
+ function getWorldPointFromClient(clientX, clientY) {
1327
+ const svg = stage.querySelector("svg");
1328
+ const scene = viewer.getScene();
1329
+ if (!svg) {
1330
+ return { x: scene.width / 2, y: scene.height / 2 };
1331
+ }
1332
+ const rect = svg.getBoundingClientRect();
1333
+ const viewportPoint = {
1334
+ x: ((clientX - rect.left) / Math.max(rect.width, 1)) * scene.width,
1335
+ y: ((clientY - rect.top) / Math.max(rect.height, 1)) * scene.height,
1336
+ };
1337
+ return invertViewerPoint(scene, viewer.getState(), viewportPoint);
1338
+ }
1339
+ function buildSystemDocumentFromInspector() {
1340
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1341
+ const form = inspector?.querySelector("form[data-editor-form='system']");
1342
+ if (!form || !nextDocument.system) {
1343
+ return nextDocument;
1344
+ }
1345
+ nextDocument.system.id = readTextInput(form, "system-id") || nextDocument.system.id;
1346
+ nextDocument.system.title = readOptionalTextInput(form, "system-title");
1347
+ return nextDocument;
1348
+ }
1349
+ function buildDefaultsDocumentFromInspector() {
1350
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1351
+ const form = inspector?.querySelector("form[data-editor-form='defaults']");
1352
+ if (!form || !nextDocument.system) {
1353
+ return nextDocument;
1354
+ }
1355
+ nextDocument.system.defaults.view =
1356
+ readTextInput(form, "defaults-view") || "topdown";
1357
+ nextDocument.system.defaults.scale = readOptionalTextInput(form, "defaults-scale");
1358
+ nextDocument.system.defaults.units = readOptionalTextInput(form, "defaults-units");
1359
+ nextDocument.system.defaults.preset =
1360
+ readOptionalTextInput(form, "defaults-preset") ?? null;
1361
+ nextDocument.system.defaults.theme = readOptionalTextInput(form, "defaults-theme");
1362
+ return nextDocument;
1363
+ }
1364
+ function buildMetadataDocumentFromInspector(currentKey) {
1365
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1366
+ const form = inspector?.querySelector("form[data-editor-form='metadata']");
1367
+ if (!form || !nextDocument.system) {
1368
+ return nextDocument;
1369
+ }
1370
+ const nextKey = readTextInput(form, "metadata-key") || currentKey;
1371
+ const nextValue = readOptionalTextInput(form, "metadata-value") ?? "";
1372
+ if (nextKey !== currentKey) {
1373
+ delete nextDocument.system.atlasMetadata[currentKey];
1374
+ nextDocument.system.atlasMetadata[nextKey] = nextValue;
1375
+ selection = { kind: "metadata", key: nextKey };
1376
+ return nextDocument;
1377
+ }
1378
+ nextDocument.system.atlasMetadata[currentKey] = nextValue;
1379
+ return nextDocument;
1380
+ }
1381
+ function buildViewpointDocumentFromInspector(currentId) {
1382
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1383
+ const form = inspector?.querySelector("form[data-editor-form='viewpoint']");
1384
+ const current = nextDocument.system?.viewpoints.find((viewpoint) => viewpoint.id === currentId);
1385
+ if (!form || !current || !nextDocument.system) {
1386
+ return nextDocument;
1387
+ }
1388
+ const nextId = readTextInput(form, "viewpoint-id") || current.id;
1389
+ const replacement = {
1390
+ ...current,
1391
+ id: nextId,
1392
+ label: readTextInput(form, "viewpoint-label") || current.label,
1393
+ summary: readOptionalTextInput(form, "viewpoint-summary") ?? "",
1394
+ focusObjectId: readOptionalTextInput(form, "viewpoint-focus"),
1395
+ selectedObjectId: readOptionalTextInput(form, "viewpoint-select"),
1396
+ projection: readTextInput(form, "viewpoint-projection") ||
1397
+ current.projection,
1398
+ preset: readOptionalTextInput(form, "viewpoint-preset") ??
1399
+ null,
1400
+ zoom: parseNullableNumber(readOptionalTextInput(form, "viewpoint-zoom")),
1401
+ rotationDeg: parseNullableNumber(readOptionalTextInput(form, "viewpoint-rotation")) ?? 0,
1402
+ camera: buildViewCameraFromForm(form),
1403
+ layers: {
1404
+ background: readCheckbox(form, "layer-background"),
1405
+ guides: readCheckbox(form, "layer-guides"),
1406
+ "orbits-back": readCheckbox(form, "layer-orbits-back"),
1407
+ "orbits-front": readCheckbox(form, "layer-orbits-front"),
1408
+ events: readCheckbox(form, "layer-events"),
1409
+ objects: readCheckbox(form, "layer-objects"),
1410
+ labels: readCheckbox(form, "layer-labels"),
1411
+ metadata: readCheckbox(form, "layer-metadata"),
1412
+ },
1413
+ events: splitTokens(readOptionalTextInput(form, "viewpoint-events")),
1414
+ filter: {
1415
+ query: readOptionalTextInput(form, "filter-query"),
1416
+ objectTypes: parseObjectTypes(readOptionalTextInput(form, "filter-object-types")),
1417
+ tags: splitTokens(readOptionalTextInput(form, "filter-tags")),
1418
+ groupIds: splitTokens(readOptionalTextInput(form, "filter-groups")),
1419
+ },
1420
+ };
1421
+ nextDocument.system.viewpoints = nextDocument.system.viewpoints
1422
+ .filter((viewpoint) => viewpoint.id !== current.id)
1423
+ .concat(replacement)
1424
+ .sort((left, right) => left.id.localeCompare(right.id));
1425
+ if (current.id !== replacement.id) {
1426
+ selection = { kind: "viewpoint", id: replacement.id };
1427
+ }
1428
+ return nextDocument;
1429
+ }
1430
+ function buildEventDocumentFromInspector(currentId) {
1431
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1432
+ const form = inspector?.querySelector("form[data-editor-form='event']");
1433
+ const current = nextDocument.events.find((entry) => entry.id === currentId);
1434
+ if (!form || !current) {
1435
+ return nextDocument;
1436
+ }
1437
+ const nextId = readTextInput(form, "event-id") || current.id;
1438
+ const replacement = {
1439
+ ...current,
1440
+ id: nextId,
1441
+ kind: readTextInput(form, "event-kind"),
1442
+ label: readTextInput(form, "event-label") || current.label,
1443
+ summary: readOptionalTextInput(form, "event-summary"),
1444
+ targetObjectId: readOptionalTextInput(form, "event-target"),
1445
+ participantObjectIds: splitTokens(readOptionalTextInput(form, "event-participants")),
1446
+ timing: readOptionalTextInput(form, "event-timing"),
1447
+ visibility: readOptionalTextInput(form, "event-visibility"),
1448
+ epoch: readOptionalTextInput(form, "event-epoch"),
1449
+ referencePlane: readOptionalTextInput(form, "event-referencePlane"),
1450
+ tags: splitTokens(readOptionalTextInput(form, "event-tags")),
1451
+ color: readOptionalTextInput(form, "event-color"),
1452
+ hidden: readCheckbox(form, "event-hidden"),
1453
+ };
1454
+ nextDocument.events = nextDocument.events
1455
+ .filter((entry) => entry.id !== current.id)
1456
+ .concat(replacement)
1457
+ .sort(compareEvents);
1458
+ syncEventViewpointReferences(nextDocument, current.id, replacement.id, splitTokens(readOptionalTextInput(form, "event-viewpoints")));
1459
+ if (current.id !== replacement.id) {
1460
+ selection = { kind: "event", id: replacement.id };
1461
+ }
1462
+ return nextDocument;
1463
+ }
1464
+ function buildEventPoseDocumentFromInspector(eventId, objectId) {
1465
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1466
+ const form = inspector?.querySelector("form[data-editor-form='event-pose']");
1467
+ const eventEntry = nextDocument.events.find((entry) => entry.id === eventId);
1468
+ const currentPose = eventEntry?.positions.find((entry) => entry.objectId === objectId);
1469
+ if (!form || !eventEntry || !currentPose) {
1470
+ return nextDocument;
1471
+ }
1472
+ const nextObjectId = readTextInput(form, "pose-object-id") || currentPose.objectId;
1473
+ const replacement = {
1474
+ objectId: nextObjectId,
1475
+ placement: buildPlacementFromPoseForm(form, currentPose),
1476
+ epoch: readOptionalTextInput(form, "pose-epoch"),
1477
+ referencePlane: readOptionalTextInput(form, "pose-referencePlane"),
1478
+ };
1479
+ const inner = parseOptionalUnit(readOptionalTextInput(form, "prop-inner"));
1480
+ const outer = parseOptionalUnit(readOptionalTextInput(form, "prop-outer"));
1481
+ if (inner) {
1482
+ replacement.inner = inner;
1483
+ }
1484
+ if (outer) {
1485
+ replacement.outer = outer;
1486
+ }
1487
+ eventEntry.positions = eventEntry.positions
1488
+ .filter((entry) => entry.objectId !== currentPose.objectId)
1489
+ .concat(replacement)
1490
+ .sort(compareEventPoses);
1491
+ if (eventEntry.targetObjectId !== replacement.objectId &&
1492
+ !eventEntry.participantObjectIds.includes(replacement.objectId)) {
1493
+ eventEntry.participantObjectIds.push(replacement.objectId);
1494
+ eventEntry.participantObjectIds.sort((left, right) => left.localeCompare(right));
1495
+ }
1496
+ if (currentPose.objectId !== replacement.objectId) {
1497
+ selection = { kind: "event-pose", id: eventId, key: replacement.objectId };
1498
+ }
1499
+ return nextDocument;
1500
+ }
1501
+ function buildAnnotationDocumentFromInspector(currentId) {
1502
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1503
+ const form = inspector?.querySelector("form[data-editor-form='annotation']");
1504
+ const current = nextDocument.system?.annotations.find((annotation) => annotation.id === currentId);
1505
+ if (!form || !current || !nextDocument.system) {
1506
+ return nextDocument;
1507
+ }
1508
+ const nextId = readTextInput(form, "annotation-id") || current.id;
1509
+ const replacement = {
1510
+ ...current,
1511
+ id: nextId,
1512
+ label: readTextInput(form, "annotation-label") || current.label,
1513
+ targetObjectId: readOptionalTextInput(form, "annotation-target"),
1514
+ body: readOptionalTextInput(form, "annotation-body") ?? "",
1515
+ tags: splitTokens(readOptionalTextInput(form, "annotation-tags")),
1516
+ sourceObjectId: readOptionalTextInput(form, "annotation-source"),
1517
+ };
1518
+ nextDocument.system.annotations = nextDocument.system.annotations
1519
+ .filter((annotation) => annotation.id !== current.id)
1520
+ .concat(replacement)
1521
+ .sort((left, right) => left.id.localeCompare(right.id));
1522
+ if (current.id !== replacement.id) {
1523
+ selection = { kind: "annotation", id: replacement.id };
1524
+ }
1525
+ return nextDocument;
1526
+ }
1527
+ function buildObjectDocumentFromInspector(currentId) {
1528
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1529
+ const form = inspector?.querySelector("form[data-editor-form='object']");
1530
+ const current = nextDocument.objects.find((object) => object.id === currentId);
1531
+ if (!form || !current) {
1532
+ return nextDocument;
1533
+ }
1534
+ const nextId = readTextInput(form, "object-id") || current.id;
1535
+ const replacement = {
1536
+ ...current,
1537
+ id: nextId,
1538
+ type: readTextInput(form, "object-type") || current.type,
1539
+ properties: { ...current.properties },
1540
+ info: { ...current.info },
1541
+ placement: buildPlacementFromForm(form, current),
1542
+ };
1543
+ for (const field of OBJECT_STRING_FIELDS) {
1544
+ setOptionalProperty(replacement.properties, field, readOptionalTextInput(form, `prop-${field}`));
1545
+ }
1546
+ for (const field of OBJECT_UNIT_FIELDS) {
1547
+ setOptionalProperty(replacement.properties, field, parseOptionalNormalizedValue(readOptionalTextInput(form, `prop-${field}`)));
1548
+ }
1549
+ for (const field of OBJECT_NUMBER_FIELDS) {
1550
+ setOptionalProperty(replacement.properties, field, parseNullableNumber(readOptionalTextInput(form, `prop-${field}`)));
1551
+ }
1552
+ setOptionalProperty(replacement.properties, "tags", splitTokens(readOptionalTextInput(form, "prop-tags")));
1553
+ replacement.properties.hidden = readCheckbox(form, "prop-hidden");
1554
+ const description = readOptionalTextInput(form, "info-description");
1555
+ if (description) {
1556
+ replacement.info.description = description;
1557
+ }
1558
+ else {
1559
+ delete replacement.info.description;
1560
+ }
1561
+ const updatedDocument = replaceObject(nextDocument, current.id, replacement);
1562
+ if (current.id !== replacement.id) {
1563
+ selection = { kind: "object", id: replacement.id };
1564
+ }
1565
+ return updatedDocument;
1566
+ }
1567
+ function renderStatusBar() {
1568
+ const errorCount = diagnostics.filter((entry) => entry.diagnostic.severity === "error").length;
1569
+ const warningCount = diagnostics.filter((entry) => entry.diagnostic.severity === "warning").length;
1570
+ const selectionLabel = selection ? describePath(selection) : "Nothing selected";
1571
+ statusBar.innerHTML = `
1572
+ <span class="wo-editor-status-pill${dirty ? " is-dirty" : " is-clean"}">${dirty ? "Unsaved changes" : "Saved"}</span>
1573
+ <span class="wo-editor-status-pill">Schema ${escapeHtml(atlasDocument.version)}</span>
1574
+ <span class="wo-editor-status-pill${errorCount > 0 ? " is-error" : warningCount > 0 ? " is-warning" : ""}">${errorCount} errors · ${warningCount} warnings</span>
1575
+ <span class="wo-editor-status-pill">${escapeHtml(selectionLabel)}</span>
1576
+ `;
1577
+ }
1578
+ function updateLiveRegion() {
1579
+ const errorCount = diagnostics.filter((entry) => entry.diagnostic.severity === "error").length;
1580
+ const warningCount = diagnostics.filter((entry) => entry.diagnostic.severity === "warning").length;
1581
+ liveRegion.textContent = `${dirty ? "Unsaved changes." : "Saved."} ${errorCount} errors, ${warningCount} warnings. ${selection ? describePath(selection) : "Nothing selected"}.`;
1582
+ }
1583
+ function schedulePreviewRender() {
1584
+ if (previewTimer !== null) {
1585
+ return;
1586
+ }
1587
+ previewTimer = window.setTimeout(() => {
1588
+ previewTimer = null;
1589
+ renderPreviewNow();
1590
+ }, PREVIEW_BATCH_DELAY_MS);
1591
+ }
1592
+ function renderPreviewNow() {
1593
+ if (!viewer) {
1594
+ return;
1595
+ }
1596
+ const nextSvg = viewer.exportSvg();
1597
+ if (previewVisual && nextSvg !== lastPreviewSvg) {
1598
+ previewVisual.innerHTML = nextSvg;
1599
+ lastPreviewSvg = nextSvg;
1600
+ }
1601
+ const nextMarkup = buildEmbedMarkup(getCurrentSourceForExport(), atlasDocument, viewMode);
1602
+ if (previewMarkup && nextMarkup !== lastPreviewMarkup) {
1603
+ previewMarkup.textContent = nextMarkup;
1604
+ lastPreviewMarkup = nextMarkup;
1605
+ }
1606
+ }
1607
+ function maybeFitOrbitResizeInView(objectId) {
1608
+ if (!viewer) {
1609
+ return;
1610
+ }
1611
+ const details = viewer.getObjectDetails(objectId);
1612
+ if (!details) {
1613
+ return;
1614
+ }
1615
+ const visibleBounds = getViewerVisibleBounds(viewer.getScene(), viewer.getState());
1616
+ const margin = 36 / Math.max(viewer.getState().scale, 0.001);
1617
+ const point = details.renderObject;
1618
+ if (point.x < visibleBounds.minX + margin ||
1619
+ point.x > visibleBounds.maxX - margin ||
1620
+ point.y < visibleBounds.minY + margin ||
1621
+ point.y > visibleBounds.maxY - margin) {
1622
+ viewer.fitToSystem();
1623
+ }
1624
+ }
1625
+ function shouldIgnoreShortcutEvent(event) {
1626
+ const target = event.target;
1627
+ if (!target) {
1628
+ return false;
1629
+ }
1630
+ const tagName = target.tagName.toLowerCase();
1631
+ return target.isContentEditable || tagName === "input" || tagName === "textarea" || tagName === "select";
1632
+ }
1633
+ function getSelectionDiagnostics(currentSelection, entries) {
1634
+ if (!currentSelection) {
1635
+ return [];
1636
+ }
1637
+ const currentKey = selectionKey(currentSelection);
1638
+ return entries.filter((entry) => {
1639
+ const entryKey = selectionKey(entry.path);
1640
+ if (entryKey && entryKey === currentKey) {
1641
+ return true;
1642
+ }
1643
+ return currentSelection.kind === "object" && entry.diagnostic.objectId === currentSelection.id;
1644
+ });
1645
+ }
1646
+ function getPlacementDiagnosticsForSelection(currentSelection, entries) {
1647
+ return getSelectionDiagnostics(currentSelection, entries).filter((entry) => {
1648
+ const field = entry.diagnostic.field ?? "";
1649
+ return (PLACEMENT_DIAGNOSTIC_FIELDS.has(field) ||
1650
+ entry.diagnostic.message.toLowerCase().includes("placement") ||
1651
+ entry.diagnostic.message.toLowerCase().includes("orbit") ||
1652
+ entry.diagnostic.message.toLowerCase().includes("surface") ||
1653
+ entry.diagnostic.message.toLowerCase().includes("lagrange") ||
1654
+ entry.diagnostic.message.toLowerCase().includes("anchor"));
1655
+ });
1656
+ }
1657
+ function renderInspectorDiagnosticSummary(currentSelection, entries) {
1658
+ const selectionDiagnostics = getSelectionDiagnostics(currentSelection, entries);
1659
+ if (selectionDiagnostics.length === 0) {
1660
+ return "";
1661
+ }
1662
+ return `<div class="wo-editor-inspector-summary">
1663
+ ${selectionDiagnostics
1664
+ .slice(0, 4)
1665
+ .map((entry) => `<article class="wo-editor-diagnostic wo-editor-diagnostic-${escapeHtml(entry.diagnostic.severity)}">
1666
+ <strong>${escapeHtml(entry.diagnostic.severity.toUpperCase())}</strong>
1667
+ <span>${escapeHtml(entry.diagnostic.field ?? describePath(currentSelection))}</span>
1668
+ <p>${escapeHtml(entry.diagnostic.message)}</p>
1669
+ </article>`)
1670
+ .join("")}
1671
+ </div>`;
1672
+ }
1673
+ function decorateInspectorDiagnostics(currentSelection, entries) {
1674
+ if (!inspector || !currentSelection) {
1675
+ return;
1676
+ }
1677
+ const selectionDiagnostics = getSelectionDiagnostics(currentSelection, entries);
1678
+ const fieldMap = new Map();
1679
+ for (const entry of selectionDiagnostics) {
1680
+ for (const inputName of mapDiagnosticFieldToInputNames(currentSelection, entry.diagnostic.field)) {
1681
+ const currentEntries = fieldMap.get(inputName) ?? [];
1682
+ currentEntries.push(entry);
1683
+ fieldMap.set(inputName, currentEntries);
1684
+ }
1685
+ }
1686
+ for (const [inputName, fieldDiagnostics] of fieldMap) {
1687
+ const input = inspector.querySelector(`[name="${CSS.escape(inputName)}"]`);
1688
+ const field = input?.closest(".wo-editor-field, .wo-editor-checkbox");
1689
+ if (!input || !field) {
1690
+ continue;
1691
+ }
1692
+ const hasError = fieldDiagnostics.some((entry) => entry.diagnostic.severity === "error");
1693
+ const hasWarning = fieldDiagnostics.some((entry) => entry.diagnostic.severity === "warning");
1694
+ field.classList.add(hasError ? "has-error" : "has-warning");
1695
+ input.setAttribute("aria-invalid", hasError ? "true" : "false");
1696
+ const note = document.createElement("div");
1697
+ note.className = `wo-editor-field-note${hasError ? " is-error" : hasWarning ? " is-warning" : ""}`;
1698
+ note.textContent = fieldDiagnostics[0]?.diagnostic.message ?? "";
1699
+ field.append(note);
1700
+ }
1701
+ }
1702
+ }
1703
+ function resolveInitialEditorState(options) {
1704
+ if (options.atlasDocument) {
1705
+ const atlasDocument = cloneAtlasDocument(options.atlasDocument);
1706
+ return {
1707
+ atlasDocument,
1708
+ source: formatAtlasSource(atlasDocument),
1709
+ diagnostics: collectDocumentDiagnostics(atlasDocument),
1710
+ };
1711
+ }
1712
+ if (options.source) {
1713
+ const loaded = loadWorldOrbitSourceWithDiagnostics(options.source);
1714
+ if (loaded.ok && loaded.value) {
1715
+ const atlasDocument = loaded.value.atlasDocument ?? upgradeDocumentToV2(loaded.value.document);
1716
+ return {
1717
+ atlasDocument,
1718
+ source: formatAtlasSource(atlasDocument),
1719
+ diagnostics: mergeDiagnostics(resolveAtlasDiagnostics(atlasDocument, loaded.diagnostics), collectDocumentDiagnostics(atlasDocument)),
1720
+ };
1721
+ }
1722
+ }
1723
+ const atlasDocument = createEmptyAtlasDocument("WorldOrbit");
1724
+ return {
1725
+ atlasDocument,
1726
+ source: formatAtlasSource(atlasDocument),
1727
+ diagnostics: collectDocumentDiagnostics(atlasDocument),
1728
+ };
1729
+ }
1730
+ function formatAtlasSource(document) {
1731
+ return formatDocument(document, { schema: document.version });
1732
+ }
1733
+ function buildEditorMarkup() {
1734
+ const previewOpen = shouldPreviewSectionBeOpenByDefault();
1735
+ return `<section class="wo-editor-shell">
1736
+ <div class="wo-editor-toolbar" data-editor-toolbar></div>
1737
+ <div class="wo-editor-status" data-editor-status role="status" aria-live="polite"></div>
1738
+ <div class="wo-editor-main">
1739
+ <aside class="wo-editor-sidebar" data-editor-pane="sidebar">
1740
+ <div class="wo-editor-panel">${renderPanelSection("Atlas", "atlas", `<div class="wo-editor-outline" data-editor-outline></div>`)}</div>
1741
+ <div class="wo-editor-panel">${renderPanelSection("Diagnostics", "diagnostics", `<div class="wo-editor-diagnostics" data-editor-diagnostics></div>`)}</div>
1742
+ </aside>
1743
+ <div class="wo-editor-stage-panel">
1744
+ <div class="wo-editor-stage-shell" data-editor-stage-shell>
1745
+ <div class="wo-editor-stage" data-editor-stage></div>
1746
+ <div class="wo-editor-overlay" data-editor-overlay></div>
1747
+ </div>
1748
+ <div class="wo-editor-panel" data-editor-pane="preview">
1749
+ ${renderPanelSection("Preview", "preview", `<div class="wo-editor-preview">
1750
+ <div class="wo-editor-preview-card">
1751
+ <h3>Static SVG</h3>
1752
+ <div class="wo-editor-preview-visual" data-editor-preview-visual></div>
1753
+ </div>
1754
+ <div class="wo-editor-preview-card">
1755
+ <h3>Embed Markup</h3>
1756
+ <pre class="wo-editor-preview-markup" data-editor-preview-markup></pre>
1757
+ </div>
1758
+ </div>`, previewOpen)}
1759
+ </div>
1760
+ </div>
1761
+ <aside class="wo-editor-panel wo-editor-inspector" data-editor-pane="inspector">
1762
+ ${renderPanelSection("Inspector", "inspector", `<div data-editor-inspector></div>`)}
1763
+ </aside>
1764
+ </div>
1765
+ <div class="wo-editor-panel wo-editor-text-panel" data-editor-pane="text">
1766
+ ${renderPanelSection("Source", "source", `<textarea
1767
+ class="wo-editor-source"
1768
+ data-editor-source
1769
+ spellcheck="false"
1770
+ aria-describedby="worldorbit-editor-source-diagnostics"
1771
+ ></textarea>
1772
+ <div
1773
+ class="wo-editor-source-diagnostics"
1774
+ id="worldorbit-editor-source-diagnostics"
1775
+ data-editor-source-diagnostics
1776
+ aria-live="polite"
1777
+ ></div>`)}
1778
+ </div>
1779
+ <div class="wo-editor-live-region" data-editor-live aria-live="polite" aria-atomic="true"></div>
1780
+ </section>`;
1781
+ }
1782
+ function renderPanelSection(title, sectionId, content, open = true) {
1783
+ return `<details class="wo-editor-panel-section" data-editor-panel-section="${escapeHtml(sectionId)}"${open ? " open" : ""}>
1784
+ <summary><span>${escapeHtml(title)}</span></summary>
1785
+ <div class="wo-editor-panel-section-body">${content}</div>
1786
+ </details>`;
1787
+ }
1788
+ function renderInspectorSection(formId, sectionId, title, content, open = false) {
1789
+ const stateKey = inspectorSectionStateKey(formId, sectionId);
1790
+ return `<details class="wo-editor-inspector-section" data-editor-form-section="${escapeHtml(sectionId)}" data-editor-form-id="${escapeHtml(formId)}" data-editor-section-state="${escapeHtml(stateKey)}"${open ? " open" : ""}>
1791
+ <summary><span>${escapeHtml(title)}</span></summary>
1792
+ <div class="wo-editor-inspector-section-body">${content}</div>
1793
+ </details>`;
1794
+ }
1795
+ function renderFieldLabel(label, name) {
1796
+ return `<span class="wo-editor-field-label">${escapeHtml(label)}${renderFieldHelp(name)}</span>`;
1797
+ }
1798
+ function renderFieldHelp(name) {
1799
+ const help = FIELD_HELP[name];
1800
+ if (!help) {
1801
+ return "";
1802
+ }
1803
+ return `<details class="wo-editor-field-help">
1804
+ <summary aria-label="Explain ${escapeAttribute(name)}">?</summary>
1805
+ <div class="wo-editor-field-help-card">
1806
+ <p>${escapeHtml(help.description)}</p>
1807
+ ${(help.references?.length ?? 0) > 0
1808
+ ? `<div class="wo-editor-field-help-chips">${(help.references ?? [])
1809
+ .map((entry) => `<span>${escapeHtml(entry)}</span>`)
1810
+ .join("")}</div>`
1811
+ : ""}
1812
+ </div>
1813
+ </details>`;
1814
+ }
1815
+ function captureInspectorSectionState(container, state) {
1816
+ for (const section of container.querySelectorAll("[data-editor-section-state]")) {
1817
+ const key = section.dataset.editorSectionState;
1818
+ if (!key) {
1819
+ continue;
1820
+ }
1821
+ state.set(key, section.open);
1822
+ }
1823
+ }
1824
+ function applyInspectorSectionState(container, state) {
1825
+ for (const section of container.querySelectorAll("[data-editor-section-state]")) {
1826
+ const key = section.dataset.editorSectionState;
1827
+ if (!key || !state.has(key)) {
1828
+ continue;
1829
+ }
1830
+ section.open = state.get(key) === true;
1831
+ }
1832
+ }
1833
+ function inspectorSectionStateKey(formId, sectionId) {
1834
+ return `${formId}:${sectionId}`;
1835
+ }
1836
+ function shouldPreviewSectionBeOpenByDefault() {
1837
+ if (typeof window === "undefined") {
1838
+ return true;
1839
+ }
1840
+ if (typeof window.matchMedia === "function") {
1841
+ return !window.matchMedia("(max-width: 1280px)").matches;
1842
+ }
1843
+ return window.innerWidth > 1280;
1844
+ }
1845
+ function renderOutlineButton(path, label, activeKey, diagnosticBuckets) {
1846
+ const key = selectionKey(path);
1847
+ const bucket = key ? diagnosticBuckets.get(key) : null;
1848
+ const badge = bucket && (bucket.errors > 0 || bucket.warnings > 0)
1849
+ ? `<span class="wo-editor-outline-badge${bucket.errors > 0 ? " is-error" : " is-warning"}">${bucket.errors > 0 ? bucket.errors : bucket.warnings}</span>`
1850
+ : "";
1851
+ return `<button type="button" class="wo-editor-outline-item${key === activeKey ? " is-active" : ""}" data-path-kind="${escapeHtml(path.kind)}"${path.id ? ` data-path-id="${escapeHtml(path.id)}"` : ""}${path.key ? ` data-path-key="${escapeHtml(path.key)}"` : ""}><span>${escapeHtml(label)}</span>${badge}</button>`;
1852
+ }
1853
+ function renderEventOutlineItems(eventEntry, activeKey, diagnosticBuckets) {
1854
+ return `<div class="wo-editor-outline-group">
1855
+ ${renderOutlineButton({ kind: "event", id: eventEntry.id }, eventEntry.label || eventEntry.id, activeKey, diagnosticBuckets)}
1856
+ ${eventEntry.positions.length > 0
1857
+ ? `<div class="wo-editor-outline-children">${[...eventEntry.positions]
1858
+ .sort(compareEventPoses)
1859
+ .map((pose) => renderOutlineButton({ kind: "event-pose", id: eventEntry.id, key: pose.objectId }, pose.objectId, activeKey, diagnosticBuckets))
1860
+ .join("")}</div>`
1861
+ : ""}
1862
+ </div>`;
1863
+ }
1864
+ function renderSystemInspector(formState) {
1865
+ return `<form class="wo-editor-form" data-editor-form="system">
1866
+ <h2>System</h2>
1867
+ ${renderInspectorSection("system", "basics", "Basics", `${renderTextField("System ID", "system-id", formState.system?.id ?? "")}
1868
+ ${renderTextField("Title", "system-title", formState.system?.title ?? "")}`, true)}
1869
+ </form>`;
1870
+ }
1871
+ function renderDefaultsInspector(formState) {
1872
+ const defaults = formState.system?.defaults;
1873
+ return `<form class="wo-editor-form" data-editor-form="defaults">
1874
+ <h2>Defaults</h2>
1875
+ ${renderInspectorSection("defaults", "basics", "Basics", `${renderSelectField("Projection", "defaults-view", [
1876
+ ["topdown", "Topdown"],
1877
+ ["isometric", "Isometric"],
1878
+ ["orthographic", "Orthographic"],
1879
+ ["perspective", "Perspective"],
1880
+ ], defaults?.view ?? "topdown")}
1881
+ ${renderTextField("Scale preset", "defaults-scale", defaults?.scale ?? "")}
1882
+ ${renderTextField("Units", "defaults-units", defaults?.units ?? "")}
1883
+ ${renderSelectField("Render preset", "defaults-preset", [
1884
+ ["", "Document default"],
1885
+ ["diagram", "Diagram"],
1886
+ ["presentation", "Presentation"],
1887
+ ["atlas-card", "Atlas Card"],
1888
+ ["markdown", "Markdown"],
1889
+ ], defaults?.preset ?? "")}
1890
+ ${renderTextField("Theme", "defaults-theme", defaults?.theme ?? "")}`, true)}
1891
+ </form>`;
1892
+ }
1893
+ function renderMetadataInspector(formState, key) {
1894
+ const value = formState.system?.atlasMetadata[key] ?? "";
1895
+ return `<form class="wo-editor-form" data-editor-form="metadata">
1896
+ <h2>Metadata</h2>
1897
+ ${renderInspectorSection("metadata", "entry", "Entry", `${renderTextField("Key", "metadata-key", key)}
1898
+ ${renderTextAreaField("Value", "metadata-value", value)}`, true)}
1899
+ </form>`;
1900
+ }
1901
+ function renderViewpointInspector(formState, id) {
1902
+ const viewpoint = formState.viewpoints.find((entry) => entry.id === id);
1903
+ if (!viewpoint) {
1904
+ return `<p class="wo-editor-empty">Viewpoint not found.</p>`;
1905
+ }
1906
+ return `<form class="wo-editor-form" data-editor-form="viewpoint">
1907
+ <h2>Viewpoint</h2>
1908
+ ${renderInspectorSection("viewpoint", "basics", "Basics", `${renderTextField("ID", "viewpoint-id", viewpoint.id)}
1909
+ ${renderTextField("Label", "viewpoint-label", viewpoint.label)}
1910
+ ${renderTextAreaField("Summary", "viewpoint-summary", viewpoint.summary)}
1911
+ ${renderTextField("Focus object", "viewpoint-focus", viewpoint.focusObjectId ?? "")}
1912
+ ${renderTextField("Selected object", "viewpoint-select", viewpoint.selectedObjectId ?? "")}
1913
+ ${renderSelectField("Projection", "viewpoint-projection", [
1914
+ ["topdown", "Topdown"],
1915
+ ["isometric", "Isometric"],
1916
+ ["orthographic", "Orthographic"],
1917
+ ["perspective", "Perspective"],
1918
+ ], viewpoint.projection)}
1919
+ ${renderSelectField("Preset", "viewpoint-preset", [
1920
+ ["", "Document default"],
1921
+ ["diagram", "Diagram"],
1922
+ ["presentation", "Presentation"],
1923
+ ["atlas-card", "Atlas Card"],
1924
+ ["markdown", "Markdown"],
1925
+ ], viewpoint.preset ?? "")}
1926
+ ${renderTextField("Zoom", "viewpoint-zoom", viewpoint.zoom === null ? "" : String(viewpoint.zoom))}
1927
+ ${renderTextField("Rotation", "viewpoint-rotation", String(viewpoint.rotationDeg))}`, true)}
1928
+ ${renderInspectorSection("viewpoint", "camera", "Camera", `${renderTextField("Azimuth", "viewpoint-camera-azimuth", viewpoint.camera?.azimuth === null || viewpoint.camera?.azimuth === undefined ? "" : String(viewpoint.camera.azimuth))}
1929
+ ${renderTextField("Elevation", "viewpoint-camera-elevation", viewpoint.camera?.elevation === null || viewpoint.camera?.elevation === undefined ? "" : String(viewpoint.camera.elevation))}
1930
+ ${renderTextField("Roll", "viewpoint-camera-roll", viewpoint.camera?.roll === null || viewpoint.camera?.roll === undefined ? "" : String(viewpoint.camera.roll))}
1931
+ ${renderTextField("Distance", "viewpoint-camera-distance", viewpoint.camera?.distance === null || viewpoint.camera?.distance === undefined ? "" : String(viewpoint.camera.distance))}
1932
+ <p class="wo-editor-inline-note">Rotation stays a 2D screen-rotation hint. The camera block stores Schema 2.5 view direction and framing.</p>`)}
1933
+ ${renderInspectorSection("viewpoint", "layers", "Layers", `<fieldset class="wo-editor-fieldset">
1934
+ <legend>Layers</legend>
1935
+ ${renderCheckboxField("Background", "layer-background", viewpoint.layers.background !== false)}
1936
+ ${renderCheckboxField("Guides", "layer-guides", viewpoint.layers.guides !== false)}
1937
+ ${renderCheckboxField("Orbits back", "layer-orbits-back", viewpoint.layers["orbits-back"] !== false)}
1938
+ ${renderCheckboxField("Orbits front", "layer-orbits-front", viewpoint.layers["orbits-front"] !== false)}
1939
+ ${renderCheckboxField("Events", "layer-events", viewpoint.layers.events !== false)}
1940
+ ${renderCheckboxField("Objects", "layer-objects", viewpoint.layers.objects !== false)}
1941
+ ${renderCheckboxField("Labels", "layer-labels", viewpoint.layers.labels !== false)}
1942
+ ${renderCheckboxField("Metadata", "layer-metadata", viewpoint.layers.metadata !== false)}
1943
+ </fieldset>`)}
1944
+ ${renderInspectorSection("viewpoint", "filter", "Filter", `${renderTextField("Filter query", "filter-query", viewpoint.filter?.query ?? "")}
1945
+ ${renderTextField("Filter object types", "filter-object-types", viewpoint.filter?.objectTypes.join(" ") ?? "")}
1946
+ ${renderTextField("Filter tags", "filter-tags", viewpoint.filter?.tags.join(" ") ?? "")}
1947
+ ${renderTextField("Filter groups", "filter-groups", viewpoint.filter?.groupIds.join(" ") ?? "")}
1948
+ ${renderTextField("Events", "viewpoint-events", viewpoint.events.join(" "))}`)}
1949
+ </form>`;
1950
+ }
1951
+ function renderEventInspector(formState, id) {
1952
+ const eventEntry = formState.events.find((entry) => entry.id === id);
1953
+ if (!eventEntry) {
1954
+ return `<p class="wo-editor-empty">Event not found.</p>`;
1955
+ }
1956
+ const linkedViewpoints = formState.viewpoints
1957
+ .filter((viewpoint) => viewpoint.events.includes(eventEntry.id))
1958
+ .map((viewpoint) => viewpoint.id)
1959
+ .join(" ");
1960
+ return `<form class="wo-editor-form" data-editor-form="event">
1961
+ <h2>Event</h2>
1962
+ ${renderInspectorSection("event", "basics", "Basics", `${renderTextField("ID", "event-id", eventEntry.id)}
1963
+ ${renderTextField("Kind", "event-kind", eventEntry.kind)}
1964
+ ${renderTextField("Label", "event-label", eventEntry.label)}
1965
+ ${renderTextAreaField("Summary", "event-summary", eventEntry.summary ?? "")}
1966
+ ${renderTextField("Target object", "event-target", eventEntry.targetObjectId ?? "")}
1967
+ ${renderTextField("Participants", "event-participants", eventEntry.participantObjectIds.join(" "))}
1968
+ ${renderTextField("Timing", "event-timing", eventEntry.timing ?? "")}
1969
+ ${renderTextField("Visibility", "event-visibility", eventEntry.visibility ?? "")}
1970
+ ${renderTextField("Epoch", "event-epoch", eventEntry.epoch ?? "")}
1971
+ ${renderTextField("Reference plane", "event-referencePlane", eventEntry.referencePlane ?? "")}
1972
+ ${renderTextField("Tags", "event-tags", eventEntry.tags.join(" "))}
1973
+ ${renderTextField("Color", "event-color", eventEntry.color ?? "")}
1974
+ ${renderCheckboxField("Hidden", "event-hidden", eventEntry.hidden === true)}`, true)}
1975
+ ${renderInspectorSection("event", "viewpoints", "Viewpoints", `${renderTextField("Viewpoints", "event-viewpoints", linkedViewpoints)}`)}
1976
+ ${renderInspectorSection("event", "positions", "Positions", `${eventEntry.positions.length > 0
1977
+ ? `<div class="wo-editor-inline-list">${eventEntry.positions
1978
+ .map((pose) => renderOutlineButton({ kind: "event-pose", id: eventEntry.id, key: pose.objectId }, pose.objectId, null, new Map()))
1979
+ .join("")}</div>`
1980
+ : `<p class="wo-editor-empty">No event poses yet.</p>`}
1981
+ <div class="wo-editor-inline-actions">
1982
+ <button type="button" data-editor-action="add-event-pose" data-editor-event-id="${escapeHtml(eventEntry.id)}">Add pose</button>
1983
+ </div>`)}
1984
+ </form>`;
1985
+ }
1986
+ function renderEventPoseInspector(formState, eventId, objectId) {
1987
+ const eventEntry = formState.events.find((entry) => entry.id === eventId);
1988
+ const pose = eventEntry?.positions.find((entry) => entry.objectId === objectId);
1989
+ if (!eventEntry || !pose) {
1990
+ return `<p class="wo-editor-empty">Event pose not found.</p>`;
1991
+ }
1992
+ const placementMode = pose.placement?.mode ?? "";
1993
+ const placementTarget = pose.placement?.mode === "orbit" || pose.placement?.mode === "surface" || pose.placement?.mode === "at"
1994
+ ? pose.placement.target
1995
+ : "";
1996
+ const freeValue = pose.placement?.mode === "free"
1997
+ ? pose.placement.distance
1998
+ ? formatUnitValue(pose.placement.distance)
1999
+ : pose.placement.descriptor ?? ""
2000
+ : "";
2001
+ return `<form class="wo-editor-form" data-editor-form="event-pose">
2002
+ <h2>Event Pose</h2>
2003
+ <p class="wo-editor-inline-note">Event <strong>${escapeHtml(eventEntry.label || eventEntry.id)}</strong></p>
2004
+ ${renderInspectorSection("event-pose", "identity", "Identity", `${renderTextField("Object", "pose-object-id", pose.objectId)}
2005
+ <div class="wo-editor-inline-actions">
2006
+ <button type="button" data-path-kind="event" data-path-id="${escapeHtml(eventEntry.id)}">Select event</button>
2007
+ </div>`, true)}
2008
+ ${renderInspectorSection("event-pose", "placement", "Placement", `${renderSelectField("Placement mode", "placement-mode", [
2009
+ ["", "None"],
2010
+ ["orbit", "Orbit"],
2011
+ ["at", "At"],
2012
+ ["surface", "Surface"],
2013
+ ["free", "Free"],
2014
+ ], placementMode)}
2015
+ ${renderTextField("Placement target", "placement-target", placementTarget)}
2016
+ ${renderTextField("Free value", "placement-free", freeValue)}
2017
+ ${renderTextField("Distance", "placement-distance", pose.placement?.mode === "orbit" && pose.placement.distance ? formatUnitValue(pose.placement.distance) : "")}
2018
+ ${renderTextField("Semi-major", "placement-semiMajor", pose.placement?.mode === "orbit" && pose.placement.semiMajor ? formatUnitValue(pose.placement.semiMajor) : "")}
2019
+ ${renderTextField("Eccentricity", "placement-eccentricity", pose.placement?.mode === "orbit" && pose.placement.eccentricity !== undefined ? String(pose.placement.eccentricity) : "")}
2020
+ ${renderTextField("Period", "placement-period", pose.placement?.mode === "orbit" && pose.placement.period ? formatUnitValue(pose.placement.period) : "")}
2021
+ ${renderTextField("Angle", "placement-angle", pose.placement?.mode === "orbit" && pose.placement.angle ? formatUnitValue(pose.placement.angle) : "")}
2022
+ ${renderTextField("Inclination", "placement-inclination", pose.placement?.mode === "orbit" && pose.placement.inclination ? formatUnitValue(pose.placement.inclination) : "")}
2023
+ ${renderTextField("Phase", "placement-phase", pose.placement?.mode === "orbit" && pose.placement.phase ? formatUnitValue(pose.placement.phase) : "")}
2024
+ ${renderTextField("Inner", "prop-inner", pose.inner ? formatUnitValue(pose.inner) : "")}
2025
+ ${renderTextField("Outer", "prop-outer", pose.outer ? formatUnitValue(pose.outer) : "")}`, true)}
2026
+ ${renderInspectorSection("event-pose", "context", "Context", `${renderTextField("Epoch", "pose-epoch", pose.epoch ?? "")}
2027
+ ${renderTextField("Reference plane", "pose-referencePlane", pose.referencePlane ?? "")}
2028
+ <p class="wo-editor-inline-note">Falls back to event, then object, then system context when left empty.</p>`)}
2029
+ </form>`;
2030
+ }
2031
+ function renderAnnotationInspector(formState, id) {
2032
+ const annotation = formState.system?.annotations.find((entry) => entry.id === id);
2033
+ if (!annotation) {
2034
+ return `<p class="wo-editor-empty">Annotation not found.</p>`;
2035
+ }
2036
+ return `<form class="wo-editor-form" data-editor-form="annotation">
2037
+ <h2>Annotation</h2>
2038
+ ${renderInspectorSection("annotation", "entry", "Entry", `${renderTextField("ID", "annotation-id", annotation.id)}
2039
+ ${renderTextField("Label", "annotation-label", annotation.label)}
2040
+ ${renderTextField("Target object", "annotation-target", annotation.targetObjectId ?? "")}
2041
+ ${renderTextField("Source object", "annotation-source", annotation.sourceObjectId ?? "")}
2042
+ ${renderTextAreaField("Body", "annotation-body", annotation.body)}
2043
+ ${renderTextField("Tags", "annotation-tags", annotation.tags.join(" "))}`, true)}
2044
+ </form>`;
2045
+ }
2046
+ function renderObjectInspector(formState, id) {
2047
+ const object = formState.objects.find((entry) => entry.id === id);
2048
+ if (!object) {
2049
+ return `<p class="wo-editor-empty">Object not found.</p>`;
2050
+ }
2051
+ const placementMode = object.placement?.mode ?? "";
2052
+ const placementTarget = object.placement?.mode === "orbit" || object.placement?.mode === "surface" || object.placement?.mode === "at"
2053
+ ? object.placement.target
2054
+ : "";
2055
+ const freeValue = object.placement?.mode === "free"
2056
+ ? object.placement.distance
2057
+ ? formatUnitValue(object.placement.distance)
2058
+ : object.placement.descriptor ?? ""
2059
+ : "";
2060
+ return `<form class="wo-editor-form" data-editor-form="object">
2061
+ <h2>Object</h2>
2062
+ ${renderInspectorSection("object", "identity", "Identity", `${renderTextField("ID", "object-id", object.id)}
2063
+ ${renderSelectField("Type", "object-type", OBJECT_TYPES.map((type) => [type, humanizeIdentifier(type)]), object.type)}`, true)}
2064
+ ${renderInspectorSection("object", "placement", "Placement", `${renderSelectField("Placement mode", "placement-mode", [
2065
+ ["", "None"],
2066
+ ["orbit", "Orbit"],
2067
+ ["at", "At"],
2068
+ ["surface", "Surface"],
2069
+ ["free", "Free"],
2070
+ ], placementMode)}
2071
+ ${renderTextField("Placement target", "placement-target", placementTarget)}
2072
+ ${renderTextField("Free value", "placement-free", freeValue)}
2073
+ ${renderTextField("Distance", "placement-distance", object.placement?.mode === "orbit" && object.placement.distance ? formatUnitValue(object.placement.distance) : "")}
2074
+ ${renderTextField("Semi-major", "placement-semiMajor", object.placement?.mode === "orbit" && object.placement.semiMajor ? formatUnitValue(object.placement.semiMajor) : "")}
2075
+ ${renderTextField("Eccentricity", "placement-eccentricity", object.placement?.mode === "orbit" && object.placement.eccentricity !== undefined ? String(object.placement.eccentricity) : "")}
2076
+ ${renderTextField("Period", "placement-period", object.placement?.mode === "orbit" && object.placement.period ? formatUnitValue(object.placement.period) : "")}
2077
+ ${renderTextField("Angle", "placement-angle", object.placement?.mode === "orbit" && object.placement.angle ? formatUnitValue(object.placement.angle) : "")}
2078
+ ${renderTextField("Inclination", "placement-inclination", object.placement?.mode === "orbit" && object.placement.inclination ? formatUnitValue(object.placement.inclination) : "")}
2079
+ ${renderTextField("Phase", "placement-phase", object.placement?.mode === "orbit" && object.placement.phase ? formatUnitValue(object.placement.phase) : "")}`, true)}
2080
+ ${renderInspectorSection("object", "properties", "Properties", `<fieldset class="wo-editor-fieldset">
2081
+ <legend>Properties</legend>
2082
+ ${renderTextField("Kind", "prop-kind", readStringProperty(object.properties.kind))}
2083
+ ${renderTextField("Class", "prop-class", readStringProperty(object.properties.class))}
2084
+ ${renderTextField("Culture", "prop-culture", readStringProperty(object.properties.culture))}
2085
+ ${renderTextField("Tags", "prop-tags", readTagsProperty(object.properties.tags))}
2086
+ ${renderTextField("Color", "prop-color", readStringProperty(object.properties.color))}
2087
+ ${renderTextField("Image", "prop-image", readStringProperty(object.properties.image))}
2088
+ ${renderCheckboxField("Hidden", "prop-hidden", object.properties.hidden === true)}
2089
+ ${renderTextField("Radius", "prop-radius", readUnitProperty(object.properties.radius))}
2090
+ ${renderTextField("Mass", "prop-mass", readUnitProperty(object.properties.mass))}
2091
+ ${renderTextField("Density", "prop-density", readUnitProperty(object.properties.density))}
2092
+ ${renderTextField("Gravity", "prop-gravity", readUnitProperty(object.properties.gravity))}
2093
+ ${renderTextField("Temperature", "prop-temperature", readUnitProperty(object.properties.temperature))}
2094
+ ${renderTextField("Albedo", "prop-albedo", readNumberProperty(object.properties.albedo))}
2095
+ ${renderTextField("Atmosphere", "prop-atmosphere", readStringProperty(object.properties.atmosphere))}
2096
+ ${renderTextField("Inner", "prop-inner", readUnitProperty(object.properties.inner))}
2097
+ ${renderTextField("Outer", "prop-outer", readUnitProperty(object.properties.outer))}
2098
+ ${renderTextField("On", "prop-on", readStringProperty(object.properties.on))}
2099
+ ${renderTextField("Source", "prop-source", readStringProperty(object.properties.source))}
2100
+ ${renderTextField("Cycle", "prop-cycle", readUnitProperty(object.properties.cycle))}
2101
+ </fieldset>`)}
2102
+ ${renderInspectorSection("object", "info", "Info", `${renderTextAreaField("Description", "info-description", object.info.description ?? "")}`)}
2103
+ </form>`;
2104
+ }
2105
+ function renderTextField(label, name, value) {
2106
+ return `<label class="wo-editor-field">${renderFieldLabel(label, name)}<input name="${escapeHtml(name)}" value="${escapeAttribute(value)}" /></label>`;
2107
+ }
2108
+ function renderTextAreaField(label, name, value) {
2109
+ return `<label class="wo-editor-field">${renderFieldLabel(label, name)}<textarea name="${escapeHtml(name)}">${escapeHtml(value)}</textarea></label>`;
2110
+ }
2111
+ function renderSelectField(label, name, options, selectedValue) {
2112
+ return `<label class="wo-editor-field">${renderFieldLabel(label, name)}<select name="${escapeHtml(name)}">${options
2113
+ .map(([value, optionLabel]) => `<option value="${escapeHtml(value)}"${value === selectedValue ? " selected" : ""}>${escapeHtml(optionLabel)}</option>`)
2114
+ .join("")}</select></label>`;
2115
+ }
2116
+ function renderCheckboxField(label, name, checked) {
2117
+ return `<label class="wo-editor-checkbox"><input type="checkbox" name="${escapeHtml(name)}"${checked ? " checked" : ""} />${renderFieldLabel(label, name)}</label>`;
2118
+ }
2119
+ function createHandleElement(kind, objectId, point, label) {
2120
+ const element = document.createElement("button");
2121
+ element.type = "button";
2122
+ element.className = "wo-editor-handle";
2123
+ element.dataset.handleKind = kind;
2124
+ element.dataset.objectId = objectId;
2125
+ element.style.left = `${point.x}px`;
2126
+ element.style.top = `${point.y}px`;
2127
+ element.textContent = label;
2128
+ return element;
2129
+ }
2130
+ function readTextInput(form, name) {
2131
+ return form.elements.namedItem(name)?.value.trim() ?? "";
2132
+ }
2133
+ function readOptionalTextInput(form, name) {
2134
+ const value = readTextInput(form, name);
2135
+ return value ? value : null;
2136
+ }
2137
+ function readCheckbox(form, name) {
2138
+ return form.elements.namedItem(name)?.checked ?? false;
2139
+ }
2140
+ function buildPlacementFromForm(form, current) {
2141
+ return buildPlacementFromValues(form, current.placement, current.id);
2142
+ }
2143
+ function buildPlacementFromPoseForm(form, current) {
2144
+ return buildPlacementFromValues(form, current.placement, current.objectId);
2145
+ }
2146
+ function buildPlacementFromValues(form, currentPlacement, fallbackTarget) {
2147
+ const mode = readTextInput(form, "placement-mode");
2148
+ const target = readOptionalTextInput(form, "placement-target");
2149
+ switch (mode) {
2150
+ case "orbit":
2151
+ return {
2152
+ mode,
2153
+ target: target ??
2154
+ (currentPlacement?.mode === "orbit"
2155
+ ? currentPlacement.target
2156
+ : fallbackTarget),
2157
+ distance: parseOptionalUnit(readOptionalTextInput(form, "placement-distance")),
2158
+ semiMajor: parseOptionalUnit(readOptionalTextInput(form, "placement-semiMajor")),
2159
+ eccentricity: parseNullableNumber(readOptionalTextInput(form, "placement-eccentricity")) ?? undefined,
2160
+ period: parseOptionalUnit(readOptionalTextInput(form, "placement-period")),
2161
+ angle: parseOptionalUnit(readOptionalTextInput(form, "placement-angle")),
2162
+ inclination: parseOptionalUnit(readOptionalTextInput(form, "placement-inclination")),
2163
+ phase: parseOptionalUnit(readOptionalTextInput(form, "placement-phase")),
2164
+ };
2165
+ case "at":
2166
+ return {
2167
+ mode,
2168
+ target: target ?? fallbackTarget,
2169
+ reference: parseAtReferenceString(target ?? fallbackTarget),
2170
+ };
2171
+ case "surface":
2172
+ return {
2173
+ mode,
2174
+ target: target ?? fallbackTarget,
2175
+ };
2176
+ case "free": {
2177
+ const freeValue = readOptionalTextInput(form, "placement-free");
2178
+ const distance = parseOptionalUnit(freeValue);
2179
+ return {
2180
+ mode,
2181
+ distance: distance ?? undefined,
2182
+ descriptor: distance ? undefined : freeValue ?? undefined,
2183
+ };
2184
+ }
2185
+ default:
2186
+ return null;
2187
+ }
2188
+ }
2189
+ function setOptionalProperty(properties, key, value) {
2190
+ const emptyArray = Array.isArray(value) && value.length === 0;
2191
+ if (value === null || value === undefined || emptyArray || value === "") {
2192
+ delete properties[key];
2193
+ return;
2194
+ }
2195
+ properties[key] = value;
2196
+ }
2197
+ function parseOptionalNormalizedValue(value) {
2198
+ if (!value) {
2199
+ return null;
2200
+ }
2201
+ const unit = parseOptionalUnit(value);
2202
+ return unit ?? value;
2203
+ }
2204
+ function parseOptionalUnit(value) {
2205
+ if (!value) {
2206
+ return undefined;
2207
+ }
2208
+ const match = value.match(/^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/);
2209
+ if (!match) {
2210
+ return undefined;
2211
+ }
2212
+ return {
2213
+ value: Number(match[1]),
2214
+ unit: match[2] ?? null,
2215
+ };
2216
+ }
2217
+ function parseNullableNumber(value) {
2218
+ if (!value) {
2219
+ return null;
2220
+ }
2221
+ const parsed = Number(value);
2222
+ return Number.isFinite(parsed) ? parsed : null;
2223
+ }
2224
+ function buildViewCameraFromForm(form) {
2225
+ const camera = {
2226
+ azimuth: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-azimuth")),
2227
+ elevation: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-elevation")),
2228
+ roll: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-roll")),
2229
+ distance: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-distance")),
2230
+ };
2231
+ return camera.azimuth !== null ||
2232
+ camera.elevation !== null ||
2233
+ camera.roll !== null ||
2234
+ camera.distance !== null
2235
+ ? camera
2236
+ : null;
2237
+ }
2238
+ function parseObjectTypes(value) {
2239
+ const tokens = splitTokens(value);
2240
+ return tokens.filter((token) => OBJECT_TYPES.includes(token));
2241
+ }
2242
+ function splitTokens(value) {
2243
+ return value
2244
+ ?.split(/[\s,]+/)
2245
+ .map((entry) => entry.trim())
2246
+ .filter(Boolean) ?? [];
2247
+ }
2248
+ function createNewObject(type, id, document) {
2249
+ const orbitTarget = document.objects.find((object) => object.type === "star")?.id ??
2250
+ document.objects[0]?.id ??
2251
+ id;
2252
+ return {
2253
+ type,
2254
+ id,
2255
+ properties: {},
2256
+ placement: type === "structure" || type === "phenomenon"
2257
+ ? {
2258
+ mode: "at",
2259
+ target: `${orbitTarget}:L4`,
2260
+ reference: parseAtReferenceString(`${orbitTarget}:L4`),
2261
+ }
2262
+ : {
2263
+ mode: "orbit",
2264
+ target: orbitTarget,
2265
+ distance: { value: 1, unit: "au" },
2266
+ },
2267
+ info: {},
2268
+ };
2269
+ }
2270
+ function insertObject(document, object) {
2271
+ const next = cloneAtlasDocument(document);
2272
+ next.objects = next.objects
2273
+ .filter((entry) => entry.id !== object.id)
2274
+ .concat(object)
2275
+ .sort(compareObjects);
2276
+ return next;
2277
+ }
2278
+ function replaceObject(document, currentId, object) {
2279
+ const next = cloneAtlasDocument(document);
2280
+ next.objects = next.objects
2281
+ .filter((entry) => entry.id !== currentId)
2282
+ .concat(object)
2283
+ .sort(compareObjects);
2284
+ if (currentId !== object.id) {
2285
+ renameObjectReferences(next, currentId, object.id);
2286
+ }
2287
+ return next;
2288
+ }
2289
+ function renameObjectReferences(document, fromId, toId) {
2290
+ for (const object of document.objects) {
2291
+ if (object.id === toId) {
2292
+ continue;
2293
+ }
2294
+ if (object.placement?.mode === "orbit" && object.placement.target === fromId) {
2295
+ object.placement.target = toId;
2296
+ }
2297
+ if (object.placement?.mode === "surface" && object.placement.target === fromId) {
2298
+ object.placement.target = toId;
2299
+ }
2300
+ if (object.placement?.mode === "at") {
2301
+ const reference = object.placement.reference;
2302
+ if (reference.kind === "anchor" && reference.objectId === fromId) {
2303
+ reference.objectId = toId;
2304
+ }
2305
+ if (reference.kind === "lagrange") {
2306
+ if (reference.primary === fromId) {
2307
+ reference.primary = toId;
2308
+ }
2309
+ if (reference.secondary === fromId) {
2310
+ reference.secondary = toId;
2311
+ }
2312
+ }
2313
+ object.placement.target = formatAtReference(reference);
2314
+ }
2315
+ }
2316
+ for (const viewpoint of document.system?.viewpoints ?? []) {
2317
+ if (viewpoint.focusObjectId === fromId) {
2318
+ viewpoint.focusObjectId = toId;
2319
+ }
2320
+ if (viewpoint.selectedObjectId === fromId) {
2321
+ viewpoint.selectedObjectId = toId;
2322
+ }
2323
+ }
2324
+ for (const annotation of document.system?.annotations ?? []) {
2325
+ if (annotation.targetObjectId === fromId) {
2326
+ annotation.targetObjectId = toId;
2327
+ }
2328
+ if (annotation.sourceObjectId === fromId) {
2329
+ annotation.sourceObjectId = toId;
2330
+ }
2331
+ }
2332
+ for (const eventEntry of document.events) {
2333
+ if (eventEntry.targetObjectId === fromId) {
2334
+ eventEntry.targetObjectId = toId;
2335
+ }
2336
+ eventEntry.participantObjectIds = eventEntry.participantObjectIds.map((entry) => entry === fromId ? toId : entry);
2337
+ for (const pose of eventEntry.positions) {
2338
+ if (pose.objectId === fromId) {
2339
+ pose.objectId = toId;
2340
+ }
2341
+ if (pose.placement?.mode === "orbit" && pose.placement.target === fromId) {
2342
+ pose.placement.target = toId;
2343
+ }
2344
+ if (pose.placement?.mode === "surface" && pose.placement.target === fromId) {
2345
+ pose.placement.target = toId;
2346
+ }
2347
+ if (pose.placement?.mode === "at") {
2348
+ const reference = pose.placement.reference;
2349
+ if (reference.kind === "anchor" && reference.objectId === fromId) {
2350
+ reference.objectId = toId;
2351
+ }
2352
+ if (reference.kind === "lagrange") {
2353
+ if (reference.primary === fromId) {
2354
+ reference.primary = toId;
2355
+ }
2356
+ if (reference.secondary === fromId) {
2357
+ reference.secondary = toId;
2358
+ }
2359
+ }
2360
+ pose.placement.target = formatAtReference(reference);
2361
+ }
2362
+ }
2363
+ eventEntry.positions.sort(compareEventPoses);
2364
+ }
2365
+ }
2366
+ function removeSelectedNode(document, selection) {
2367
+ const next = removeAtlasDocumentNode(document, selection);
2368
+ if (selection.kind === "event" && selection.id) {
2369
+ for (const viewpoint of next.system?.viewpoints ?? []) {
2370
+ viewpoint.events = viewpoint.events.filter((eventId) => eventId !== selection.id);
2371
+ }
2372
+ return next;
2373
+ }
2374
+ if (selection.kind !== "object" || !selection.id) {
2375
+ return next;
2376
+ }
2377
+ for (const object of next.objects) {
2378
+ if (object.placement?.mode === "orbit" && object.placement.target === selection.id) {
2379
+ object.placement = null;
2380
+ }
2381
+ if (object.placement?.mode === "surface" && object.placement.target === selection.id) {
2382
+ object.placement = null;
2383
+ }
2384
+ if (object.placement?.mode === "at") {
2385
+ const reference = object.placement.reference;
2386
+ const touchesSelection = (reference.kind === "anchor" && reference.objectId === selection.id) ||
2387
+ (reference.kind === "lagrange" &&
2388
+ (reference.primary === selection.id || reference.secondary === selection.id));
2389
+ if (touchesSelection) {
2390
+ object.placement = null;
2391
+ }
2392
+ }
2393
+ }
2394
+ for (const viewpoint of next.system?.viewpoints ?? []) {
2395
+ if (viewpoint.focusObjectId === selection.id) {
2396
+ viewpoint.focusObjectId = null;
2397
+ }
2398
+ if (viewpoint.selectedObjectId === selection.id) {
2399
+ viewpoint.selectedObjectId = null;
2400
+ }
2401
+ }
2402
+ for (const annotation of next.system?.annotations ?? []) {
2403
+ if (annotation.targetObjectId === selection.id) {
2404
+ annotation.targetObjectId = null;
2405
+ }
2406
+ if (annotation.sourceObjectId === selection.id) {
2407
+ annotation.sourceObjectId = null;
2408
+ }
2409
+ }
2410
+ for (const eventEntry of next.events) {
2411
+ if (eventEntry.targetObjectId === selection.id) {
2412
+ eventEntry.targetObjectId = null;
2413
+ }
2414
+ eventEntry.participantObjectIds = eventEntry.participantObjectIds.filter((entry) => entry !== selection.id);
2415
+ eventEntry.positions = eventEntry.positions.filter((entry) => entry.objectId !== selection.id);
2416
+ for (const pose of eventEntry.positions) {
2417
+ if (pose.placement?.mode === "orbit" && pose.placement.target === selection.id) {
2418
+ pose.placement = null;
2419
+ }
2420
+ if (pose.placement?.mode === "surface" && pose.placement.target === selection.id) {
2421
+ pose.placement = null;
2422
+ }
2423
+ if (pose.placement?.mode === "at") {
2424
+ const reference = pose.placement.reference;
2425
+ const touchesSelection = (reference.kind === "anchor" && reference.objectId === selection.id) ||
2426
+ (reference.kind === "lagrange" &&
2427
+ (reference.primary === selection.id || reference.secondary === selection.id));
2428
+ if (touchesSelection) {
2429
+ pose.placement = null;
2430
+ }
2431
+ }
2432
+ }
2433
+ }
2434
+ return next;
2435
+ }
2436
+ function findEditablePlacementOwner(document, path, objectId) {
2437
+ if (path.kind === "event-pose" && path.id && path.key) {
2438
+ const pose = findEventPose(document, path.id, path.key);
2439
+ if (pose?.placement) {
2440
+ return { placement: pose.placement };
2441
+ }
2442
+ return null;
2443
+ }
2444
+ const object = findObject(document, objectId);
2445
+ if (object?.placement) {
2446
+ return { placement: object.placement };
2447
+ }
2448
+ return null;
2449
+ }
2450
+ function updateOrbitPhase(document, path, objectId, details, pointer) {
2451
+ const orbit = details.orbit;
2452
+ if (!orbit || details.object.placement?.mode !== "orbit") {
2453
+ return document;
2454
+ }
2455
+ const unrotated = rotatePoint(pointer, { x: orbit.cx, y: orbit.cy }, -orbit.rotationDeg);
2456
+ const rx = orbit.kind === "circle" ? orbit.radius ?? 1 : orbit.rx ?? 1;
2457
+ const ry = orbit.kind === "circle" ? orbit.radius ?? 1 : orbit.ry ?? 1;
2458
+ const radians = Math.atan2((unrotated.y - orbit.cy) / Math.max(ry, 1), (unrotated.x - orbit.cx) / Math.max(rx, 1));
2459
+ const phaseDeg = normalizeDegrees((radians * 180) / Math.PI);
2460
+ const next = cloneAtlasDocument(document);
2461
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
2462
+ if (!placementOwner || placementOwner.placement.mode !== "orbit") {
2463
+ return document;
2464
+ }
2465
+ placementOwner.placement.phase = {
2466
+ value: roundNumber(phaseDeg, 2),
2467
+ unit: "deg",
2468
+ };
2469
+ return next;
2470
+ }
2471
+ function updateOrbitRadius(document, path, objectId, details, pointer, dragContext) {
2472
+ const orbit = details.orbit;
2473
+ if (!orbit || details.object.placement?.mode !== "orbit" || !dragContext) {
2474
+ return document;
2475
+ }
2476
+ const unrotated = rotatePoint(pointer, { x: orbit.cx, y: orbit.cy }, -orbit.rotationDeg);
2477
+ const nextDisplayedRadius = Math.max(Math.abs(unrotated.x - orbit.cx), 24);
2478
+ const nextBaseRadius = Math.max(nextDisplayedRadius - dragContext.radiusOffsetPx, dragContext.innerPx);
2479
+ const nextMetric = orbitRadiusPxToMetric(nextBaseRadius, dragContext.innerPx, dragContext.stepPx);
2480
+ const next = cloneAtlasDocument(document);
2481
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
2482
+ if (!placementOwner || placementOwner.placement.mode !== "orbit") {
2483
+ return document;
2484
+ }
2485
+ const currentValue = placementOwner.placement.semiMajor ??
2486
+ placementOwner.placement.distance ?? {
2487
+ value: 1,
2488
+ unit: "au",
2489
+ };
2490
+ const scaled = distanceMetricToUnitValue(Math.max(nextMetric, 0), dragContext.preferredUnit ?? currentValue.unit);
2491
+ if (placementOwner.placement.semiMajor) {
2492
+ placementOwner.placement.semiMajor = scaled;
2493
+ }
2494
+ else {
2495
+ placementOwner.placement.distance = scaled;
2496
+ }
2497
+ return next;
2498
+ }
2499
+ function updateAtReference(document, path, objectId, scene, pointer) {
2500
+ const candidate = findNearestAtCandidate(scene, objectId, pointer);
2501
+ if (!candidate) {
2502
+ return document;
2503
+ }
2504
+ const next = cloneAtlasDocument(document);
2505
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
2506
+ if (!placementOwner || placementOwner.placement.mode !== "at") {
2507
+ return document;
2508
+ }
2509
+ placementOwner.placement.reference = candidate.reference;
2510
+ placementOwner.placement.target = formatAtReference(candidate.reference);
2511
+ return next;
2512
+ }
2513
+ function updateSurfaceTarget(document, path, objectId, scene, pointer) {
2514
+ const target = findNearestSceneObject(scene, objectId, pointer, (entry) => SURFACE_TARGET_TYPES.has(entry.object.type));
2515
+ if (!target) {
2516
+ return document;
2517
+ }
2518
+ const next = cloneAtlasDocument(document);
2519
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
2520
+ if (!placementOwner || placementOwner.placement.mode !== "surface") {
2521
+ return document;
2522
+ }
2523
+ placementOwner.placement.target = target.objectId;
2524
+ return next;
2525
+ }
2526
+ function createOrbitRadiusDragContext(document, scene, details) {
2527
+ if (details.object.placement?.mode !== "orbit" || !details.orbit || !details.parent) {
2528
+ return null;
2529
+ }
2530
+ const targetId = details.object.placement.target;
2531
+ const siblingCount = scene.objects.filter((entry) => entry.object.placement?.mode === "orbit" &&
2532
+ entry.object.placement.target === targetId &&
2533
+ !entry.hidden).length;
2534
+ const spacingFactor = layoutPresetSpacingForScene(scene.layoutPreset);
2535
+ const stepPx = (siblingCount > 2 ? 54 : 64) * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
2536
+ const innerPx = details.parent.radius +
2537
+ 56 * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
2538
+ const currentValue = details.object.placement.semiMajor ?? details.object.placement.distance ?? null;
2539
+ const currentMetric = unitValueToDistanceMetric(currentValue);
2540
+ const displayedRadius = details.orbit.kind === "circle" ? details.orbit.radius ?? 1 : details.orbit.rx ?? 1;
2541
+ const baseRadius = orbitMetricToRadiusPx(currentMetric ?? 0, innerPx, stepPx);
2542
+ return {
2543
+ innerPx,
2544
+ stepPx,
2545
+ radiusOffsetPx: displayedRadius - baseRadius,
2546
+ preferredUnit: currentValue?.unit ?? null,
2547
+ };
2548
+ }
2549
+ function updateFreeDistance(document, path, objectId, scene, details, pointer) {
2550
+ if (details.object.placement?.mode !== "free") {
2551
+ return document;
2552
+ }
2553
+ const railX = scene.width - scene.padding - 140;
2554
+ const offsetPx = Math.max(0, railX - pointer.x);
2555
+ const next = cloneAtlasDocument(document);
2556
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
2557
+ if (!placementOwner || placementOwner.placement.mode !== "free") {
2558
+ return document;
2559
+ }
2560
+ const preferredUnit = normalizeFreeDistanceUnit(placementOwner.placement.distance?.unit ?? null);
2561
+ const metric = offsetPx / Math.max(FREE_DISTANCE_PIXEL_FACTOR * scene.scaleModel.freePlacementMultiplier, 1);
2562
+ if (metric < 0.01) {
2563
+ placementOwner.placement.distance = undefined;
2564
+ if (!placementOwner.placement.descriptor) {
2565
+ delete placementOwner.placement.descriptor;
2566
+ }
2567
+ return next;
2568
+ }
2569
+ placementOwner.placement.distance = distanceMetricToUnitValue(metric, preferredUnit);
2570
+ delete placementOwner.placement.descriptor;
2571
+ return next;
2572
+ }
2573
+ function findNearestSceneObject(scene, selectedObjectId, pointer, predicate = () => true) {
2574
+ let nearest = null;
2575
+ let nearestDistance = Number.POSITIVE_INFINITY;
2576
+ for (const entry of scene.objects) {
2577
+ if (entry.hidden || entry.objectId === selectedObjectId || !predicate(entry)) {
2578
+ continue;
2579
+ }
2580
+ const distance = Math.hypot(pointer.x - entry.x, pointer.y - entry.y);
2581
+ if (distance < nearestDistance) {
2582
+ nearest = entry;
2583
+ nearestDistance = distance;
2584
+ }
2585
+ }
2586
+ return nearestDistance <= 140 ? nearest : null;
2587
+ }
2588
+ function findNearestAtCandidate(scene, selectedObjectId, pointer) {
2589
+ let directObjectCandidate = null;
2590
+ for (const entry of scene.objects) {
2591
+ if (entry.hidden || entry.objectId === selectedObjectId) {
2592
+ continue;
2593
+ }
2594
+ const distance = Math.hypot(pointer.x - entry.x, pointer.y - entry.y);
2595
+ const snapRadius = Math.max(entry.visualRadius + 16, 28);
2596
+ if (distance <= snapRadius) {
2597
+ if (!directObjectCandidate || distance < directObjectCandidate.distance) {
2598
+ directObjectCandidate = {
2599
+ reference: parseAtReferenceString(entry.objectId),
2600
+ x: entry.x,
2601
+ y: entry.y,
2602
+ distance,
2603
+ };
2604
+ }
2605
+ }
2606
+ }
2607
+ if (directObjectCandidate) {
2608
+ return {
2609
+ reference: directObjectCandidate.reference,
2610
+ x: directObjectCandidate.x,
2611
+ y: directObjectCandidate.y,
2612
+ };
2613
+ }
2614
+ const candidates = [];
2615
+ for (const entry of scene.objects) {
2616
+ if (entry.hidden || entry.objectId === selectedObjectId) {
2617
+ continue;
2618
+ }
2619
+ candidates.push({
2620
+ reference: parseAtReferenceString(entry.objectId),
2621
+ x: entry.x,
2622
+ y: entry.y,
2623
+ });
2624
+ }
2625
+ for (const orbit of scene.orbitVisuals) {
2626
+ if (orbit.hidden || orbit.objectId === selectedObjectId) {
2627
+ continue;
2628
+ }
2629
+ const parent = scene.objects.find((entry) => entry.objectId === orbit.parentId && !entry.hidden);
2630
+ const secondary = scene.objects.find((entry) => entry.objectId === orbit.objectId && !entry.hidden);
2631
+ if (!parent || !secondary) {
2632
+ continue;
2633
+ }
2634
+ for (const point of ["L1", "L2", "L3", "L4", "L5"]) {
2635
+ const position = computeLagrangeCandidatePosition(parent, secondary, point);
2636
+ candidates.push({
2637
+ reference: parseAtReferenceString(`${orbit.objectId}:${point}`),
2638
+ x: position.x,
2639
+ y: position.y,
2640
+ });
2641
+ }
2642
+ }
2643
+ let nearest = null;
2644
+ let nearestDistance = Number.POSITIVE_INFINITY;
2645
+ for (const candidate of candidates) {
2646
+ const distance = Math.hypot(pointer.x - candidate.x, pointer.y - candidate.y);
2647
+ if (distance < nearestDistance) {
2648
+ nearest = candidate;
2649
+ nearestDistance = distance;
2650
+ }
2651
+ }
2652
+ return nearestDistance <= 140 ? nearest : null;
2653
+ }
2654
+ function computeLagrangeCandidatePosition(primary, secondary, point) {
2655
+ const dx = secondary.x - primary.x;
2656
+ const dy = secondary.y - primary.y;
2657
+ const distance = Math.hypot(dx, dy) || 1;
2658
+ const ux = dx / distance;
2659
+ const uy = dy / distance;
2660
+ const nx = -uy;
2661
+ const ny = ux;
2662
+ const offset = clampNumber(distance * 0.25, 24, 68);
2663
+ switch (point) {
2664
+ case "L1":
2665
+ return {
2666
+ x: secondary.x - ux * offset,
2667
+ y: secondary.y - uy * offset,
2668
+ };
2669
+ case "L2":
2670
+ return {
2671
+ x: secondary.x + ux * offset,
2672
+ y: secondary.y + uy * offset,
2673
+ };
2674
+ case "L3":
2675
+ return {
2676
+ x: primary.x - ux * offset,
2677
+ y: primary.y - uy * offset,
2678
+ };
2679
+ case "L4":
2680
+ return {
2681
+ x: secondary.x + (ux * 0.5 - nx * 0.8660254) * offset,
2682
+ y: secondary.y + (uy * 0.5 - ny * 0.8660254) * offset,
2683
+ };
2684
+ case "L5":
2685
+ return {
2686
+ x: secondary.x + (ux * 0.5 + nx * 0.8660254) * offset,
2687
+ y: secondary.y + (uy * 0.5 + ny * 0.8660254) * offset,
2688
+ };
2689
+ }
2690
+ }
2691
+ function parseAtReferenceString(target) {
2692
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
2693
+ if (pairedMatch) {
2694
+ return {
2695
+ kind: "lagrange",
2696
+ primary: pairedMatch[1],
2697
+ secondary: pairedMatch[2],
2698
+ point: pairedMatch[3],
2699
+ };
2700
+ }
2701
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
2702
+ if (simpleMatch) {
2703
+ return {
2704
+ kind: "lagrange",
2705
+ primary: simpleMatch[1],
2706
+ secondary: null,
2707
+ point: simpleMatch[2],
2708
+ };
2709
+ }
2710
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
2711
+ if (anchorMatch) {
2712
+ return {
2713
+ kind: "anchor",
2714
+ objectId: anchorMatch[1],
2715
+ anchor: anchorMatch[2],
2716
+ };
2717
+ }
2718
+ return {
2719
+ kind: "named",
2720
+ name: target,
2721
+ };
2722
+ }
2723
+ function formatAtReference(reference) {
2724
+ switch (reference.kind) {
2725
+ case "lagrange":
2726
+ return reference.secondary
2727
+ ? `${reference.primary}-${reference.secondary}:${reference.point}`
2728
+ : `${reference.primary}:${reference.point}`;
2729
+ case "anchor":
2730
+ return `${reference.objectId}:${reference.anchor}`;
2731
+ case "named":
2732
+ return reference.name;
2733
+ }
2734
+ }
2735
+ function collectDocumentDiagnostics(document) {
2736
+ return validateAtlasDocumentWithDiagnostics(document);
2737
+ }
2738
+ function mergeDiagnostics(primary, secondary) {
2739
+ const seen = new Set();
2740
+ const merged = [];
2741
+ for (const entry of [...primary, ...secondary]) {
2742
+ const key = `${entry.diagnostic.code}:${entry.diagnostic.message}:${selectionKey(entry.path)}`;
2743
+ if (seen.has(key)) {
2744
+ continue;
2745
+ }
2746
+ seen.add(key);
2747
+ merged.push(cloneResolvedDiagnostic(entry));
2748
+ }
2749
+ return merged;
2750
+ }
2751
+ function buildDiagnosticBuckets(entries) {
2752
+ const buckets = new Map();
2753
+ for (const entry of entries) {
2754
+ const key = selectionKey(entry.path) ??
2755
+ (entry.diagnostic.objectId ? selectionKey({ kind: "object", id: entry.diagnostic.objectId }) : null);
2756
+ if (!key) {
2757
+ continue;
2758
+ }
2759
+ const bucket = buckets.get(key) ?? { errors: 0, warnings: 0 };
2760
+ if (entry.diagnostic.severity === "error") {
2761
+ bucket.errors += 1;
2762
+ }
2763
+ else if (entry.diagnostic.severity === "warning") {
2764
+ bucket.warnings += 1;
2765
+ }
2766
+ buckets.set(key, bucket);
2767
+ }
2768
+ return buckets;
2769
+ }
2770
+ function formatDiagnosticLocation(entry) {
2771
+ if (entry.diagnostic.line !== undefined && entry.diagnostic.column !== undefined) {
2772
+ return `Line ${entry.diagnostic.line}:${entry.diagnostic.column}`;
2773
+ }
2774
+ if (entry.diagnostic.line !== undefined) {
2775
+ return `Line ${entry.diagnostic.line}`;
2776
+ }
2777
+ return null;
2778
+ }
2779
+ function mapDiagnosticFieldToInputNames(selection, field) {
2780
+ if (!field) {
2781
+ return [];
2782
+ }
2783
+ switch (selection.kind) {
2784
+ case "system":
2785
+ if (field === "id") {
2786
+ return ["system-id"];
2787
+ }
2788
+ if (field === "title") {
2789
+ return ["system-title"];
2790
+ }
2791
+ return [];
2792
+ case "defaults":
2793
+ switch (field) {
2794
+ case "view":
2795
+ return ["defaults-view"];
2796
+ case "scale":
2797
+ return ["defaults-scale"];
2798
+ case "units":
2799
+ return ["defaults-units"];
2800
+ case "preset":
2801
+ return ["defaults-preset"];
2802
+ case "theme":
2803
+ return ["defaults-theme"];
2804
+ default:
2805
+ return [];
2806
+ }
2807
+ case "metadata":
2808
+ return field === "key" ? ["metadata-key"] : ["metadata-value"];
2809
+ case "group":
2810
+ switch (field) {
2811
+ case "id":
2812
+ return ["group-id"];
2813
+ case "label":
2814
+ return ["group-label"];
2815
+ case "summary":
2816
+ return ["group-summary"];
2817
+ case "color":
2818
+ return ["group-color"];
2819
+ case "tags":
2820
+ return ["group-tags"];
2821
+ case "hidden":
2822
+ return ["group-hidden"];
2823
+ default:
2824
+ return [];
2825
+ }
2826
+ case "viewpoint":
2827
+ switch (field) {
2828
+ case "id":
2829
+ return ["viewpoint-id"];
2830
+ case "label":
2831
+ return ["viewpoint-label"];
2832
+ case "summary":
2833
+ return ["viewpoint-summary"];
2834
+ case "focusObjectId":
2835
+ return ["viewpoint-focus"];
2836
+ case "selectedObjectId":
2837
+ return ["viewpoint-select"];
2838
+ case "projection":
2839
+ return ["viewpoint-projection"];
2840
+ case "preset":
2841
+ return ["viewpoint-preset"];
2842
+ case "zoom":
2843
+ return ["viewpoint-zoom"];
2844
+ case "rotationDeg":
2845
+ return ["viewpoint-rotation"];
2846
+ case "camera":
2847
+ return [
2848
+ "viewpoint-camera-azimuth",
2849
+ "viewpoint-camera-elevation",
2850
+ "viewpoint-camera-roll",
2851
+ "viewpoint-camera-distance",
2852
+ ];
2853
+ case "camera.azimuth":
2854
+ return ["viewpoint-camera-azimuth"];
2855
+ case "camera.elevation":
2856
+ return ["viewpoint-camera-elevation"];
2857
+ case "camera.roll":
2858
+ return ["viewpoint-camera-roll"];
2859
+ case "camera.distance":
2860
+ return ["viewpoint-camera-distance"];
2861
+ case "events":
2862
+ return ["viewpoint-events"];
2863
+ default:
2864
+ return [];
2865
+ }
2866
+ case "event":
2867
+ switch (field) {
2868
+ case "id":
2869
+ return ["event-id"];
2870
+ case "kind":
2871
+ return ["event-kind"];
2872
+ case "label":
2873
+ return ["event-label"];
2874
+ case "summary":
2875
+ return ["event-summary"];
2876
+ case "targetObjectId":
2877
+ case "target":
2878
+ return ["event-target"];
2879
+ case "participantObjectIds":
2880
+ case "participants":
2881
+ return ["event-participants"];
2882
+ case "timing":
2883
+ return ["event-timing"];
2884
+ case "visibility":
2885
+ return ["event-visibility"];
2886
+ case "epoch":
2887
+ return ["event-epoch"];
2888
+ case "referencePlane":
2889
+ return ["event-referencePlane"];
2890
+ case "tags":
2891
+ return ["event-tags"];
2892
+ case "color":
2893
+ return ["event-color"];
2894
+ case "hidden":
2895
+ return ["event-hidden"];
2896
+ default:
2897
+ return [];
2898
+ }
2899
+ case "event-pose":
2900
+ if (field === "objectId") {
2901
+ return ["pose-object-id"];
2902
+ }
2903
+ if (field === "placement") {
2904
+ return ["placement-mode"];
2905
+ }
2906
+ if (field === "reference" || field === "target") {
2907
+ return ["placement-target"];
2908
+ }
2909
+ if (field === "descriptor") {
2910
+ return ["placement-free"];
2911
+ }
2912
+ if (PLACEMENT_DIAGNOSTIC_FIELDS.has(field)) {
2913
+ return [`placement-${field}`];
2914
+ }
2915
+ if (field === "inner" || field === "outer") {
2916
+ return [`prop-${field}`];
2917
+ }
2918
+ if (field === "epoch") {
2919
+ return ["pose-epoch"];
2920
+ }
2921
+ if (field === "referencePlane") {
2922
+ return ["pose-referencePlane"];
2923
+ }
2924
+ return [];
2925
+ case "annotation":
2926
+ switch (field) {
2927
+ case "id":
2928
+ return ["annotation-id"];
2929
+ case "label":
2930
+ return ["annotation-label"];
2931
+ case "targetObjectId":
2932
+ return ["annotation-target"];
2933
+ case "sourceObjectId":
2934
+ return ["annotation-source"];
2935
+ case "body":
2936
+ return ["annotation-body"];
2937
+ case "tags":
2938
+ return ["annotation-tags"];
2939
+ default:
2940
+ return [];
2941
+ }
2942
+ case "relation":
2943
+ switch (field) {
2944
+ case "id":
2945
+ return ["relation-id"];
2946
+ case "from":
2947
+ return ["relation-from"];
2948
+ case "to":
2949
+ return ["relation-to"];
2950
+ case "kind":
2951
+ return ["relation-kind"];
2952
+ case "label":
2953
+ return ["relation-label"];
2954
+ case "summary":
2955
+ return ["relation-summary"];
2956
+ case "tags":
2957
+ return ["relation-tags"];
2958
+ case "color":
2959
+ return ["relation-color"];
2960
+ case "hidden":
2961
+ return ["relation-hidden"];
2962
+ default:
2963
+ return [];
2964
+ }
2965
+ case "object":
2966
+ if (field === "id") {
2967
+ return ["object-id"];
2968
+ }
2969
+ if (field === "type") {
2970
+ return ["object-type"];
2971
+ }
2972
+ if (field === "placement") {
2973
+ return ["placement-mode"];
2974
+ }
2975
+ if (field === "description") {
2976
+ return ["info-description"];
2977
+ }
2978
+ if (field === "reference") {
2979
+ return ["placement-target"];
2980
+ }
2981
+ if (field === "descriptor") {
2982
+ return ["placement-free"];
2983
+ }
2984
+ if (field === "target") {
2985
+ return ["placement-target"];
2986
+ }
2987
+ if (PLACEMENT_DIAGNOSTIC_FIELDS.has(field)) {
2988
+ return [`placement-${field}`];
2989
+ }
2990
+ return [`prop-${field}`];
2991
+ }
2992
+ }
2993
+ function buildEmbedMarkup(source, document, viewMode) {
2994
+ return renderWorldOrbitBlock(source, {
2995
+ mode: viewMode === "3d" ? "interactive-3d" : "interactive-2d",
2996
+ preset: document.system?.defaults.preset ?? "atlas-card",
2997
+ projection: document.system?.defaults.view ?? "topdown",
2998
+ });
2999
+ }
3000
+ function describePath(path) {
3001
+ switch (path.kind) {
3002
+ case "system":
3003
+ return "System";
3004
+ case "defaults":
3005
+ return "Defaults";
3006
+ case "metadata":
3007
+ return `Metadata: ${path.key ?? ""}`;
3008
+ case "group":
3009
+ return `Group: ${path.id ?? ""}`;
3010
+ case "event":
3011
+ return `Event: ${path.id ?? ""}`;
3012
+ case "event-pose":
3013
+ return `Event Pose: ${path.id ?? ""} / ${path.key ?? ""}`;
3014
+ case "object":
3015
+ return `Object: ${path.id ?? ""}`;
3016
+ case "viewpoint":
3017
+ return `Viewpoint: ${path.id ?? ""}`;
3018
+ case "annotation":
3019
+ return `Annotation: ${path.id ?? ""}`;
3020
+ case "relation":
3021
+ return `Relation: ${path.id ?? ""}`;
3022
+ }
3023
+ }
3024
+ function selectionKey(path) {
3025
+ return path ? `${path.kind}:${path.id ?? ""}:${path.key ?? ""}` : null;
3026
+ }
3027
+ function selectionEventId(path) {
3028
+ if (!path) {
3029
+ return null;
3030
+ }
3031
+ return path.kind === "event" || path.kind === "event-pose" ? path.id ?? null : null;
3032
+ }
3033
+ function compareObjects(left, right) {
3034
+ return left.id.localeCompare(right.id);
3035
+ }
3036
+ function compareEvents(left, right) {
3037
+ return left.id.localeCompare(right.id);
3038
+ }
3039
+ function compareEventPoses(left, right) {
3040
+ return left.objectId.localeCompare(right.objectId);
3041
+ }
3042
+ function findEvent(document, eventId) {
3043
+ return document.events.find((entry) => entry.id === eventId) ?? null;
3044
+ }
3045
+ function findEventPose(document, eventId, objectId) {
3046
+ return findEvent(document, eventId)?.positions.find((entry) => entry.objectId === objectId) ?? null;
3047
+ }
3048
+ function findObject(document, objectId) {
3049
+ return document.objects.find((entry) => entry.id === objectId) ?? null;
3050
+ }
3051
+ function addEventPose(document, eventId) {
3052
+ const next = cloneAtlasDocument(document);
3053
+ const eventEntry = next.events.find((entry) => entry.id === eventId);
3054
+ if (!eventEntry) {
3055
+ return document;
3056
+ }
3057
+ const baseObject = next.objects.find((object) => !eventEntry.positions.some((pose) => pose.objectId === object.id)) ??
3058
+ next.objects[0];
3059
+ if (!baseObject) {
3060
+ return document;
3061
+ }
3062
+ if (eventEntry.targetObjectId !== baseObject.id &&
3063
+ !eventEntry.participantObjectIds.includes(baseObject.id)) {
3064
+ eventEntry.participantObjectIds.push(baseObject.id);
3065
+ eventEntry.participantObjectIds.sort((left, right) => left.localeCompare(right));
3066
+ }
3067
+ eventEntry.positions.push(createEventPoseFromObject(baseObject));
3068
+ eventEntry.positions.sort(compareEventPoses);
3069
+ return next;
3070
+ }
3071
+ function createEventPoseFromObject(object) {
3072
+ return {
3073
+ objectId: object.id,
3074
+ placement: object.placement ? structuredClone(object.placement) : null,
3075
+ inner: readUnitValue(object.properties.inner),
3076
+ outer: readUnitValue(object.properties.outer),
3077
+ };
3078
+ }
3079
+ function syncEventViewpointReferences(document, previousEventId, nextEventId, viewpointIds) {
3080
+ const desired = new Set(viewpointIds);
3081
+ for (const viewpoint of document.system?.viewpoints ?? []) {
3082
+ const currentIds = new Set(viewpoint.events);
3083
+ currentIds.delete(previousEventId);
3084
+ currentIds.delete(nextEventId);
3085
+ if (desired.has(viewpoint.id)) {
3086
+ currentIds.add(nextEventId);
3087
+ }
3088
+ viewpoint.events = [...currentIds].sort((left, right) => left.localeCompare(right));
3089
+ }
3090
+ }
3091
+ function createUniqueId(prefix, existing) {
3092
+ const safePrefix = prefix.trim() || "item";
3093
+ let counter = 1;
3094
+ let candidate = safePrefix;
3095
+ while (existing.includes(candidate)) {
3096
+ counter += 1;
3097
+ candidate = `${safePrefix}-${counter}`;
3098
+ }
3099
+ return candidate;
3100
+ }
3101
+ function humanizeIdentifier(value) {
3102
+ return value
3103
+ .split(/[-_]+/)
3104
+ .filter(Boolean)
3105
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
3106
+ .join(" ");
3107
+ }
3108
+ function cloneResolvedDiagnostic(diagnostic) {
3109
+ return {
3110
+ diagnostic: { ...diagnostic.diagnostic },
3111
+ path: diagnostic.path ? { ...diagnostic.path } : null,
3112
+ };
3113
+ }
3114
+ function readStringProperty(value) {
3115
+ return typeof value === "string" ? value : "";
3116
+ }
3117
+ function readTagsProperty(value) {
3118
+ return Array.isArray(value) ? value.join(" ") : "";
3119
+ }
3120
+ function readUnitProperty(value) {
3121
+ return value && typeof value === "object" && "value" in value
3122
+ ? formatUnitValue(value)
3123
+ : "";
3124
+ }
3125
+ function readUnitValue(value) {
3126
+ return value && typeof value === "object" && "value" in value ? value : undefined;
3127
+ }
3128
+ function readNumberProperty(value) {
3129
+ return typeof value === "number" ? String(value) : "";
3130
+ }
3131
+ function formatUnitValue(value) {
3132
+ return `${value.value}${value.unit ?? ""}`;
3133
+ }
3134
+ function roundNumber(value, decimals) {
3135
+ const factor = 10 ** decimals;
3136
+ return Math.round(value * factor) / factor;
3137
+ }
3138
+ function clampNumber(value, min, max) {
3139
+ return Math.min(Math.max(value, min), max);
3140
+ }
3141
+ function normalizeDegrees(value) {
3142
+ let normalized = value % 360;
3143
+ if (normalized < 0) {
3144
+ normalized += 360;
3145
+ }
3146
+ return normalized;
3147
+ }
3148
+ function normalizeFreeDistanceUnit(unit) {
3149
+ switch (unit) {
3150
+ case "au":
3151
+ case "km":
3152
+ case "re":
3153
+ case "sol":
3154
+ case null:
3155
+ return unit;
3156
+ default:
3157
+ return null;
3158
+ }
3159
+ }
3160
+ function unitValueToDistanceMetric(value) {
3161
+ if (!value) {
3162
+ return null;
3163
+ }
3164
+ switch (value.unit) {
3165
+ case "au":
3166
+ return value.value;
3167
+ case "km":
3168
+ return value.value / AU_IN_KM;
3169
+ case "re":
3170
+ return (value.value * EARTH_RADIUS_IN_KM) / AU_IN_KM;
3171
+ case "sol":
3172
+ return (value.value * SOLAR_RADIUS_IN_KM) / AU_IN_KM;
3173
+ default:
3174
+ return value.value;
3175
+ }
3176
+ }
3177
+ function distanceMetricToUnitValue(metric, unit) {
3178
+ switch (unit) {
3179
+ case "km":
3180
+ return { value: roundNumber(metric * AU_IN_KM, 0), unit };
3181
+ case "re":
3182
+ return { value: roundNumber((metric * AU_IN_KM) / EARTH_RADIUS_IN_KM, 3), unit };
3183
+ case "sol":
3184
+ return { value: roundNumber((metric * AU_IN_KM) / SOLAR_RADIUS_IN_KM, 4), unit };
3185
+ case "au":
3186
+ return { value: roundNumber(metric, 3), unit };
3187
+ default:
3188
+ return { value: roundNumber(metric, 2), unit: null };
3189
+ }
3190
+ }
3191
+ function orbitMetricToRadiusPx(metric, innerPx, stepPx) {
3192
+ return innerPx + stepPx * log2(Math.max(metric, 0) + 1);
3193
+ }
3194
+ function orbitRadiusPxToMetric(radiusPx, innerPx, stepPx) {
3195
+ if (radiusPx <= innerPx) {
3196
+ return 0;
3197
+ }
3198
+ return Math.pow(2, (radiusPx - innerPx) / Math.max(stepPx, 0.0001)) - 1;
3199
+ }
3200
+ function layoutPresetSpacingForScene(layoutPreset) {
3201
+ switch (layoutPreset) {
3202
+ case "compact":
3203
+ return 0.84;
3204
+ case "presentation":
3205
+ return 1.2;
3206
+ default:
3207
+ return 1;
3208
+ }
3209
+ }
3210
+ function log2(value) {
3211
+ return Math.log(value) / Math.log(2);
3212
+ }
3213
+ function escapeHtml(value) {
3214
+ return value
3215
+ .replaceAll("&", "&amp;")
3216
+ .replaceAll("<", "&lt;")
3217
+ .replaceAll(">", "&gt;")
3218
+ .replaceAll('"', "&quot;");
3219
+ }
3220
+ function escapeAttribute(value) {
3221
+ return escapeHtml(value);
3222
+ }
3223
+ function ensureBrowserEnvironment(container) {
3224
+ if (typeof window === "undefined" || typeof document === "undefined") {
3225
+ throw new Error("createWorldOrbitEditor can only run in a browser environment.");
3226
+ }
3227
+ if (!(container instanceof HTMLElement)) {
3228
+ throw new Error("WorldOrbit editor requires an HTMLElement container.");
3229
+ }
3230
+ }
3231
+ function queryRequired(container, selector) {
3232
+ const found = container.querySelector(selector);
3233
+ if (!found) {
3234
+ throw new Error(`WorldOrbit editor failed to initialize selector "${selector}".`);
3235
+ }
3236
+ return found;
3237
+ }
3238
+ function installEditorStyles() {
3239
+ if (document.getElementById(STYLE_ID)) {
3240
+ return;
3241
+ }
3242
+ const style = document.createElement("style");
3243
+ style.id = STYLE_ID;
3244
+ style.textContent = `
3245
+ .wo-editor {
3246
+ --wo-editor-sidebar-width: 280px;
3247
+ --wo-editor-inspector-width: 360px;
3248
+ --wo-editor-source-height: 280px;
3249
+ }
3250
+ .wo-editor-shell { display: grid; gap: 16px; min-width: 0; }
3251
+ .wo-editor-toolbar { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; min-width: 0; }
3252
+ .wo-editor-toolbar-group { display: flex; gap: 10px; flex-wrap: wrap; min-width: 0; }
3253
+ .wo-editor-toolbar button, .wo-editor-toolbar select {
3254
+ border: 1px solid rgba(240, 180, 100, 0.2);
3255
+ border-radius: 999px;
3256
+ background: rgba(240, 180, 100, 0.08);
3257
+ color: #edf6ff;
3258
+ font: 600 13px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
3259
+ padding: 10px 14px;
3260
+ }
3261
+ .wo-editor button:focus-visible,
3262
+ .wo-editor select:focus-visible,
3263
+ .wo-editor input:focus-visible,
3264
+ .wo-editor textarea:focus-visible {
3265
+ outline: 2px solid rgba(255, 214, 138, 0.95);
3266
+ outline-offset: 2px;
3267
+ }
3268
+ .wo-editor-toolbar button:disabled { opacity: 0.45; cursor: not-allowed; }
3269
+ .wo-editor-status {
3270
+ display: flex;
3271
+ flex-wrap: wrap;
3272
+ gap: 10px;
3273
+ min-width: 0;
3274
+ }
3275
+ .wo-editor-status-pill {
3276
+ border: 1px solid rgba(255,255,255,0.08);
3277
+ border-radius: 999px;
3278
+ background: rgba(255,255,255,0.04);
3279
+ color: #edf6ff;
3280
+ font: 600 12px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
3281
+ padding: 8px 12px;
3282
+ }
3283
+ .wo-editor-status-pill.is-clean { border-color: rgba(120, 255, 195, 0.22); color: #c7ffe5; }
3284
+ .wo-editor-status-pill.is-dirty { border-color: rgba(240, 180, 100, 0.28); color: #ffd58a; }
3285
+ .wo-editor-status-pill.is-warning { border-color: rgba(240, 180, 100, 0.28); color: #ffd58a; }
3286
+ .wo-editor-status-pill.is-error { border-color: rgba(255, 120, 120, 0.3); color: #ffb2b2; }
3287
+ .wo-editor-main {
3288
+ display: grid;
3289
+ grid-template-columns:
3290
+ minmax(220px, var(--wo-editor-sidebar-width))
3291
+ minmax(0, 1fr)
3292
+ minmax(280px, var(--wo-editor-inspector-width));
3293
+ gap: 16px;
3294
+ min-width: 0;
3295
+ align-items: start;
3296
+ }
3297
+ .wo-editor-sidebar, .wo-editor-stage-panel, .wo-editor-preview { display: grid; gap: 16px; min-width: 0; }
3298
+ .wo-editor-panel {
3299
+ border-radius: 24px;
3300
+ border: 1px solid rgba(255,255,255,0.08);
3301
+ background: rgba(7, 16, 25, 0.72);
3302
+ padding: 18px;
3303
+ min-width: 0;
3304
+ }
3305
+ .wo-editor-panel h2 {
3306
+ margin: 0 0 14px;
3307
+ color: #edf6ff;
3308
+ font: 700 14px/1.2 "Segoe UI Variable Display", "Segoe UI", sans-serif;
3309
+ text-transform: uppercase;
3310
+ letter-spacing: 0.08em;
3311
+ }
3312
+ .wo-editor-panel h3 {
3313
+ margin: 0 0 12px;
3314
+ color: rgba(237, 246, 255, 0.82);
3315
+ font: 700 12px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
3316
+ text-transform: uppercase;
3317
+ letter-spacing: 0.08em;
3318
+ }
3319
+ .wo-editor-panel-section,
3320
+ .wo-editor-inspector-section {
3321
+ min-width: 0;
3322
+ }
3323
+ .wo-editor-panel-section > summary,
3324
+ .wo-editor-inspector-section > summary {
3325
+ display: flex;
3326
+ align-items: center;
3327
+ justify-content: space-between;
3328
+ gap: 10px;
3329
+ cursor: pointer;
3330
+ list-style: none;
3331
+ color: #edf6ff;
3332
+ font: 700 14px/1.2 "Segoe UI Variable Display", "Segoe UI", sans-serif;
3333
+ text-transform: uppercase;
3334
+ letter-spacing: 0.08em;
3335
+ }
3336
+ .wo-editor-inspector-section > summary {
3337
+ font: 700 12px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
3338
+ color: rgba(237, 246, 255, 0.9);
3339
+ }
3340
+ .wo-editor-panel-section > summary::-webkit-details-marker,
3341
+ .wo-editor-inspector-section > summary::-webkit-details-marker,
3342
+ .wo-editor-field-help > summary::-webkit-details-marker {
3343
+ display: none;
3344
+ }
3345
+ .wo-editor-panel-section > summary::after,
3346
+ .wo-editor-inspector-section > summary::after {
3347
+ content: "▾";
3348
+ color: rgba(240, 180, 100, 0.86);
3349
+ font-size: 12px;
3350
+ transition: transform 0.18s ease;
3351
+ }
3352
+ .wo-editor-panel-section:not([open]) > summary::after,
3353
+ .wo-editor-inspector-section:not([open]) > summary::after {
3354
+ transform: rotate(-90deg);
3355
+ }
3356
+ .wo-editor-panel-section-body {
3357
+ display: grid;
3358
+ gap: 16px;
3359
+ margin-top: 14px;
3360
+ min-width: 0;
3361
+ }
3362
+ .wo-editor-inspector-section-body {
3363
+ display: grid;
3364
+ gap: 12px;
3365
+ margin-top: 12px;
3366
+ min-width: 0;
3367
+ }
3368
+ .wo-editor[data-wo-show-inspector="false"] [data-editor-pane="inspector"] { display: none; }
3369
+ .wo-editor[data-wo-show-text-pane="false"] [data-editor-pane="text"] { display: none; }
3370
+ .wo-editor[data-wo-show-preview="false"] [data-editor-pane="preview"] { display: none; }
3371
+ .wo-editor-stage-shell {
3372
+ position: relative;
3373
+ min-width: 0;
3374
+ border-radius: 26px;
3375
+ overflow: hidden;
3376
+ border: 1px solid rgba(255,255,255,0.08);
3377
+ background: rgba(8, 17, 28, 0.9);
3378
+ }
3379
+ .wo-editor-stage { min-height: 620px; min-width: 0; }
3380
+ .wo-editor-overlay { position: absolute; inset: 0; pointer-events: none; }
3381
+ .wo-editor-overlay > * { pointer-events: auto; }
3382
+ .wo-editor-handle {
3383
+ position: absolute;
3384
+ transform: translate(-50%, -50%);
3385
+ border: 0;
3386
+ border-radius: 999px;
3387
+ background: rgba(255, 214, 138, 0.92);
3388
+ color: #071019;
3389
+ cursor: grab;
3390
+ font: 700 11px/1 "Segoe UI Variable", "Segoe UI", sans-serif;
3391
+ padding: 8px 10px;
3392
+ box-shadow: 0 10px 24px rgba(0,0,0,0.24);
3393
+ }
3394
+ .wo-editor-hint {
3395
+ position: absolute;
3396
+ transform: translate(-50%, -50%);
3397
+ padding: 4px 8px;
3398
+ border-radius: 999px;
3399
+ background: rgba(18, 39, 58, 0.84);
3400
+ color: #edf6ff;
3401
+ font: 600 11px/1.3 "Segoe UI Variable", "Segoe UI", sans-serif;
3402
+ white-space: nowrap;
3403
+ box-shadow: 0 6px 18px rgba(0,0,0,0.22);
3404
+ }
3405
+ .wo-editor-hint.is-subtle { background: rgba(18, 39, 58, 0.64); color: rgba(237, 246, 255, 0.78); }
3406
+ .wo-editor-hint-note { left: 16px; top: 16px; transform: none; }
3407
+ .wo-editor-overlay-diagnostics {
3408
+ position: absolute;
3409
+ right: 16px;
3410
+ top: 16px;
3411
+ display: grid;
3412
+ gap: 8px;
3413
+ max-width: min(320px, calc(100% - 32px));
3414
+ }
3415
+ .wo-editor-overlay-diagnostic {
3416
+ border-radius: 14px;
3417
+ padding: 10px 12px;
3418
+ background: rgba(10, 20, 32, 0.92);
3419
+ box-shadow: 0 12px 30px rgba(0,0,0,0.28);
3420
+ }
3421
+ .wo-editor-overlay-diagnostic strong {
3422
+ display: block;
3423
+ margin-bottom: 4px;
3424
+ font: 700 11px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
3425
+ letter-spacing: 0.08em;
3426
+ text-transform: uppercase;
3427
+ }
3428
+ .wo-editor-overlay-diagnostic p {
3429
+ margin: 0;
3430
+ color: #edf6ff;
3431
+ font: 500 12px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
3432
+ }
3433
+ .wo-editor-overlay-diagnostic-error { border: 1px solid rgba(255, 120, 120, 0.28); }
3434
+ .wo-editor-overlay-diagnostic-warning { border: 1px solid rgba(240, 180, 100, 0.24); }
3435
+ .wo-editor-outline { display: grid; gap: 14px; }
3436
+ .wo-editor-outline-section { display: grid; gap: 8px; }
3437
+ .wo-editor-outline-group { display: grid; gap: 6px; }
3438
+ .wo-editor-outline-children {
3439
+ display: grid;
3440
+ gap: 6px;
3441
+ padding-left: 16px;
3442
+ }
3443
+ .wo-editor-outline-section h3 {
3444
+ margin: 0;
3445
+ color: rgba(237,246,255,0.68);
3446
+ font: 600 11px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
3447
+ text-transform: uppercase;
3448
+ letter-spacing: 0.08em;
3449
+ }
3450
+ .wo-editor-outline-item {
3451
+ border: 1px solid transparent;
3452
+ border-radius: 16px;
3453
+ background: rgba(255,255,255,0.04);
3454
+ color: #edf6ff;
3455
+ cursor: pointer;
3456
+ font: 500 13px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
3457
+ padding: 10px 12px;
3458
+ text-align: left;
3459
+ display: flex;
3460
+ align-items: center;
3461
+ justify-content: space-between;
3462
+ gap: 10px;
3463
+ }
3464
+ .wo-editor-outline-item.is-active {
3465
+ border-color: rgba(255, 214, 138, 0.34);
3466
+ background: rgba(255, 214, 138, 0.12);
3467
+ color: #ffdda9;
3468
+ }
3469
+ .wo-editor-outline-badge {
3470
+ min-width: 22px;
3471
+ border-radius: 999px;
3472
+ background: rgba(240, 180, 100, 0.16);
3473
+ color: #ffd58a;
3474
+ font: 700 11px/1 "Segoe UI Variable", "Segoe UI", sans-serif;
3475
+ padding: 5px 7px;
3476
+ text-align: center;
3477
+ }
3478
+ .wo-editor-outline-badge.is-error {
3479
+ background: rgba(255, 120, 120, 0.18);
3480
+ color: #ffb2b2;
3481
+ }
3482
+ .wo-editor-inline-list { display: grid; gap: 8px; }
3483
+ .wo-editor-inline-actions {
3484
+ display: flex;
3485
+ flex-wrap: wrap;
3486
+ gap: 10px;
3487
+ margin-top: 12px;
3488
+ }
3489
+ .wo-editor-inline-actions button {
3490
+ border: 1px solid rgba(255,255,255,0.14);
3491
+ border-radius: 999px;
3492
+ background: rgba(255,255,255,0.06);
3493
+ color: #edf6ff;
3494
+ cursor: pointer;
3495
+ font: 600 12px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
3496
+ padding: 8px 12px;
3497
+ }
3498
+ .wo-editor-inline-note {
3499
+ margin: 0 0 12px;
3500
+ color: rgba(237,246,255,0.72);
3501
+ font: 500 12px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
3502
+ }
3503
+ .wo-editor-diagnostics { display: grid; gap: 10px; }
3504
+ .wo-editor-diagnostic {
3505
+ display: grid;
3506
+ gap: 4px;
3507
+ border-radius: 16px;
3508
+ padding: 12px;
3509
+ background: rgba(255,255,255,0.04);
3510
+ color: #edf6ff;
3511
+ font: 500 12px/1.45 "Segoe UI Variable", "Segoe UI", sans-serif;
3512
+ }
3513
+ .wo-editor-diagnostic strong { font-size: 11px; letter-spacing: 0.08em; }
3514
+ .wo-editor-diagnostic p { margin: 0; color: rgba(237,246,255,0.82); }
3515
+ .wo-editor-diagnostic-warning { border: 1px solid rgba(240, 180, 100, 0.22); }
3516
+ .wo-editor-diagnostic-error { border: 1px solid rgba(255, 120, 120, 0.24); }
3517
+ .wo-editor-inspector-summary {
3518
+ display: grid;
3519
+ gap: 10px;
3520
+ margin-bottom: 14px;
3521
+ }
3522
+ .wo-editor-form { display: grid; gap: 12px; min-width: 0; }
3523
+ .wo-editor-inspector-section {
3524
+ border: 1px solid rgba(255,255,255,0.08);
3525
+ border-radius: 18px;
3526
+ padding: 14px;
3527
+ background: rgba(255,255,255,0.02);
3528
+ }
3529
+ .wo-editor-field { display: grid; gap: 6px; }
3530
+ .wo-editor-field-label,
3531
+ .wo-editor-fieldset legend {
3532
+ display: flex;
3533
+ align-items: center;
3534
+ justify-content: space-between;
3535
+ gap: 10px;
3536
+ }
3537
+ .wo-editor-field-label,
3538
+ .wo-editor-checkbox .wo-editor-field-label,
3539
+ .wo-editor-fieldset legend {
3540
+ color: rgba(237,246,255,0.72);
3541
+ font: 600 11px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
3542
+ text-transform: uppercase;
3543
+ letter-spacing: 0.08em;
3544
+ }
3545
+ .wo-editor-field input, .wo-editor-field select, .wo-editor-field textarea, .wo-editor-source {
3546
+ width: 100%;
3547
+ min-width: 0;
3548
+ border: 1px solid rgba(255,255,255,0.08);
3549
+ border-radius: 14px;
3550
+ background: rgba(12, 26, 39, 0.84);
3551
+ color: #edf6ff;
3552
+ font: 500 13px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
3553
+ padding: 10px 12px;
3554
+ box-sizing: border-box;
3555
+ }
3556
+ .wo-editor-field.has-error input,
3557
+ .wo-editor-field.has-error select,
3558
+ .wo-editor-field.has-error textarea,
3559
+ .wo-editor-checkbox.has-error {
3560
+ border-color: rgba(255, 120, 120, 0.44);
3561
+ }
3562
+ .wo-editor-field.has-warning input,
3563
+ .wo-editor-field.has-warning select,
3564
+ .wo-editor-field.has-warning textarea,
3565
+ .wo-editor-checkbox.has-warning {
3566
+ border-color: rgba(240, 180, 100, 0.42);
3567
+ }
3568
+ .wo-editor-field textarea, .wo-editor-source { min-height: 110px; resize: vertical; }
3569
+ .wo-editor-source {
3570
+ min-height: var(--wo-editor-source-height);
3571
+ font-family: "Cascadia Code", "Consolas", monospace;
3572
+ white-space: pre;
3573
+ overflow: auto;
3574
+ }
3575
+ .wo-editor-field-note {
3576
+ color: rgba(237,246,255,0.72);
3577
+ font: 500 12px/1.45 "Segoe UI Variable", "Segoe UI", sans-serif;
3578
+ }
3579
+ .wo-editor-field-note.is-error { color: #ffb2b2; }
3580
+ .wo-editor-field-note.is-warning { color: #ffd58a; }
3581
+ .wo-editor-fieldset {
3582
+ display: grid;
3583
+ gap: 10px;
3584
+ border: 1px solid rgba(255,255,255,0.08);
3585
+ border-radius: 18px;
3586
+ padding: 14px;
3587
+ min-width: 0;
3588
+ }
3589
+ .wo-editor-checkbox {
3590
+ display: flex;
3591
+ gap: 10px;
3592
+ align-items: flex-start;
3593
+ color: #edf6ff;
3594
+ font: 500 13px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
3595
+ }
3596
+ .wo-editor-checkbox input {
3597
+ margin-top: 2px;
3598
+ }
3599
+ .wo-editor-field-help {
3600
+ position: relative;
3601
+ flex: 0 0 auto;
3602
+ }
3603
+ .wo-editor-field-help > summary {
3604
+ display: inline-flex;
3605
+ align-items: center;
3606
+ justify-content: center;
3607
+ width: 20px;
3608
+ height: 20px;
3609
+ border-radius: 999px;
3610
+ border: 1px solid rgba(240, 180, 100, 0.28);
3611
+ background: rgba(240, 180, 100, 0.12);
3612
+ color: #ffdda9;
3613
+ cursor: pointer;
3614
+ font: 700 11px/1 "Segoe UI Variable", "Segoe UI", sans-serif;
3615
+ list-style: none;
3616
+ }
3617
+ .wo-editor-field-help-card {
3618
+ display: grid;
3619
+ gap: 8px;
3620
+ margin-top: 8px;
3621
+ padding: 10px 12px;
3622
+ border-radius: 14px;
3623
+ border: 1px solid rgba(240, 180, 100, 0.2);
3624
+ background: rgba(12, 26, 39, 0.92);
3625
+ color: rgba(237, 246, 255, 0.86);
3626
+ text-transform: none;
3627
+ letter-spacing: normal;
3628
+ font: 500 12px/1.45 "Segoe UI Variable", "Segoe UI", sans-serif;
3629
+ }
3630
+ .wo-editor-field-help-card p {
3631
+ margin: 0;
3632
+ }
3633
+ .wo-editor-field-help-chips {
3634
+ display: flex;
3635
+ flex-wrap: wrap;
3636
+ gap: 6px;
3637
+ }
3638
+ .wo-editor-field-help-chips span {
3639
+ border-radius: 999px;
3640
+ padding: 4px 8px;
3641
+ background: rgba(255,255,255,0.06);
3642
+ color: #edf6ff;
3643
+ font: 600 11px/1.3 "Segoe UI Variable", "Segoe UI", sans-serif;
3644
+ text-transform: none;
3645
+ letter-spacing: normal;
3646
+ }
3647
+ .wo-editor-preview { grid-template-columns: repeat(2, minmax(0, 1fr)); }
3648
+ .wo-editor-preview-card {
3649
+ border-radius: 18px;
3650
+ border: 1px solid rgba(255,255,255,0.08);
3651
+ background: rgba(255,255,255,0.03);
3652
+ padding: 14px;
3653
+ min-width: 0;
3654
+ }
3655
+ .wo-editor-preview-visual {
3656
+ min-height: 240px;
3657
+ overflow: auto;
3658
+ }
3659
+ .wo-editor-preview-markup {
3660
+ margin: 0;
3661
+ min-height: 240px;
3662
+ white-space: pre-wrap;
3663
+ overflow-wrap: anywhere;
3664
+ color: #edf6ff;
3665
+ font: 12px/1.5 "Cascadia Code", "Consolas", monospace;
3666
+ }
3667
+ .wo-editor-source-diagnostics {
3668
+ display: grid;
3669
+ gap: 10px;
3670
+ margin-top: 14px;
3671
+ }
3672
+ .wo-editor-empty {
3673
+ margin: 0;
3674
+ color: rgba(237,246,255,0.68);
3675
+ font: 500 12px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
3676
+ }
3677
+ .wo-editor-live-region {
3678
+ position: absolute;
3679
+ width: 1px;
3680
+ height: 1px;
3681
+ padding: 0;
3682
+ margin: -1px;
3683
+ overflow: hidden;
3684
+ clip: rect(0, 0, 0, 0);
3685
+ white-space: nowrap;
3686
+ border: 0;
3687
+ }
3688
+ @media (max-width: 1280px) {
3689
+ .wo-editor-main { grid-template-columns: minmax(220px, 260px) minmax(0, 1fr); }
3690
+ .wo-editor-inspector { grid-column: 1 / -1; }
3691
+ .wo-editor-preview { grid-template-columns: 1fr; }
3692
+ }
3693
+ @media (max-width: 960px) {
3694
+ .wo-editor-main { grid-template-columns: 1fr; }
3695
+ .wo-editor-stage { min-height: 440px; }
3696
+ .wo-editor-status { flex-direction: column; }
3697
+ }
3698
+ `;
3699
+ document.head.append(style);
3700
+ }