worldorbit 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE.md +5 -0
  2. package/README.md +250 -0
  3. package/dist/browser/core/dist/index.js +4009 -0
  4. package/dist/browser/markdown/dist/index.js +3951 -0
  5. package/dist/browser/viewer/dist/index.js +5981 -0
  6. package/dist/constants.d.ts +8 -0
  7. package/dist/constants.js +84 -0
  8. package/dist/errors.d.ts +7 -0
  9. package/dist/errors.js +16 -0
  10. package/dist/index.d.ts +18 -0
  11. package/dist/index.js +25 -0
  12. package/dist/normalize.d.ts +2 -0
  13. package/dist/normalize.js +243 -0
  14. package/dist/parse.d.ts +2 -0
  15. package/dist/parse.js +126 -0
  16. package/dist/render.d.ts +6 -0
  17. package/dist/render.js +683 -0
  18. package/dist/tokenize.d.ts +4 -0
  19. package/dist/tokenize.js +68 -0
  20. package/dist/types.d.ts +208 -0
  21. package/dist/types.js +1 -0
  22. package/dist/unpkg/core/dist/index.js +4081 -0
  23. package/dist/unpkg/markdown/dist/index.js +3979 -0
  24. package/dist/unpkg/test.html +1 -0
  25. package/dist/unpkg/viewer/dist/index.js +6038 -0
  26. package/dist/unpkg/worldorbit-core.min.js +5 -0
  27. package/dist/unpkg/worldorbit-markdown.min.js +81 -0
  28. package/dist/unpkg/worldorbit-viewer.min.js +232 -0
  29. package/dist/unpkg/worldorbit.d.ts +2 -0
  30. package/dist/unpkg/worldorbit.js +2 -0
  31. package/dist/unpkg/worldorbit.min.js +236 -0
  32. package/dist/validate.d.ts +2 -0
  33. package/dist/validate.js +31 -0
  34. package/dist/viewer-state.d.ts +16 -0
  35. package/dist/viewer-state.js +130 -0
  36. package/dist/viewer.d.ts +2 -0
  37. package/dist/viewer.js +434 -0
  38. package/package.json +64 -0
  39. package/packages/core/README.md +13 -0
  40. package/packages/core/dist/atlas-edit.d.ts +11 -0
  41. package/packages/core/dist/atlas-edit.js +210 -0
  42. package/packages/core/dist/diagnostics.d.ts +10 -0
  43. package/packages/core/dist/diagnostics.js +109 -0
  44. package/packages/core/dist/draft-parse.d.ts +3 -0
  45. package/packages/core/dist/draft-parse.js +642 -0
  46. package/packages/core/dist/draft.d.ts +15 -0
  47. package/packages/core/dist/draft.js +343 -0
  48. package/packages/core/dist/errors.d.ts +7 -0
  49. package/packages/core/dist/errors.js +16 -0
  50. package/packages/core/dist/format.d.ts +4 -0
  51. package/packages/core/dist/format.js +364 -0
  52. package/packages/core/dist/index.d.ts +28 -0
  53. package/packages/core/dist/index.js +44 -0
  54. package/packages/core/dist/load.d.ts +4 -0
  55. package/packages/core/dist/load.js +130 -0
  56. package/packages/core/dist/markdown.d.ts +2 -0
  57. package/packages/core/dist/markdown.js +37 -0
  58. package/packages/core/dist/normalize.d.ts +2 -0
  59. package/packages/core/dist/normalize.js +304 -0
  60. package/packages/core/dist/parse.d.ts +2 -0
  61. package/packages/core/dist/parse.js +133 -0
  62. package/packages/core/dist/scene.d.ts +3 -0
  63. package/packages/core/dist/scene.js +1484 -0
  64. package/packages/core/dist/schema.d.ts +8 -0
  65. package/packages/core/dist/schema.js +298 -0
  66. package/packages/core/dist/tokenize.d.ts +4 -0
  67. package/packages/core/dist/tokenize.js +68 -0
  68. package/packages/core/dist/types.d.ts +382 -0
  69. package/packages/core/dist/types.js +1 -0
  70. package/packages/core/dist/validate.d.ts +2 -0
  71. package/packages/core/dist/validate.js +56 -0
  72. package/packages/editor/dist/editor.d.ts +2 -0
  73. package/packages/editor/dist/editor.js +2620 -0
  74. package/packages/editor/dist/index.d.ts +2 -0
  75. package/packages/editor/dist/index.js +1 -0
  76. package/packages/editor/dist/types.d.ts +53 -0
  77. package/packages/editor/dist/types.js +1 -0
  78. package/packages/markdown/README.md +9 -0
  79. package/packages/markdown/dist/html.d.ts +3 -0
  80. package/packages/markdown/dist/html.js +57 -0
  81. package/packages/markdown/dist/index.d.ts +4 -0
  82. package/packages/markdown/dist/index.js +3 -0
  83. package/packages/markdown/dist/rehype.d.ts +10 -0
  84. package/packages/markdown/dist/rehype.js +49 -0
  85. package/packages/markdown/dist/remark.d.ts +9 -0
  86. package/packages/markdown/dist/remark.js +28 -0
  87. package/packages/markdown/dist/types.d.ts +11 -0
  88. package/packages/markdown/dist/types.js +1 -0
  89. package/packages/viewer/README.md +12 -0
  90. package/packages/viewer/dist/atlas-state.d.ts +12 -0
  91. package/packages/viewer/dist/atlas-state.js +251 -0
  92. package/packages/viewer/dist/atlas-viewer.d.ts +2 -0
  93. package/packages/viewer/dist/atlas-viewer.js +448 -0
  94. package/packages/viewer/dist/custom-element.d.ts +1 -0
  95. package/packages/viewer/dist/custom-element.js +64 -0
  96. package/packages/viewer/dist/embed.d.ts +20 -0
  97. package/packages/viewer/dist/embed.js +138 -0
  98. package/packages/viewer/dist/index.d.ts +9 -0
  99. package/packages/viewer/dist/index.js +8 -0
  100. package/packages/viewer/dist/minimap.d.ts +3 -0
  101. package/packages/viewer/dist/minimap.js +63 -0
  102. package/packages/viewer/dist/render.d.ts +6 -0
  103. package/packages/viewer/dist/render.js +585 -0
  104. package/packages/viewer/dist/theme.d.ts +4 -0
  105. package/packages/viewer/dist/theme.js +98 -0
  106. package/packages/viewer/dist/tooltip.d.ts +3 -0
  107. package/packages/viewer/dist/tooltip.js +154 -0
  108. package/packages/viewer/dist/types.d.ts +256 -0
  109. package/packages/viewer/dist/types.js +1 -0
  110. package/packages/viewer/dist/viewer-state.d.ts +19 -0
  111. package/packages/viewer/dist/viewer-state.js +162 -0
  112. package/packages/viewer/dist/viewer.d.ts +2 -0
  113. package/packages/viewer/dist/viewer.js +1156 -0
@@ -0,0 +1,2620 @@
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 { 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
+ export function createWorldOrbitEditor(container, options = {}) {
65
+ ensureBrowserEnvironment(container);
66
+ installEditorStyles();
67
+ const initial = resolveInitialEditorState(options);
68
+ let atlasDocument = initial.atlasDocument;
69
+ let canonicalSource = initial.source;
70
+ let sourceText = canonicalSource;
71
+ let savedSource = canonicalSource;
72
+ let diagnostics = initial.diagnostics;
73
+ let selection = atlasDocument.objects[0]
74
+ ? { kind: "object", id: atlasDocument.objects[0].id }
75
+ : { kind: "system" };
76
+ const history = [];
77
+ const future = [];
78
+ let dragState = null;
79
+ let ignoreViewerSelection = false;
80
+ let destroyed = false;
81
+ let dirty = false;
82
+ let sourceInputTimer = null;
83
+ let previewTimer = null;
84
+ let lastPreviewSvg = "";
85
+ let lastPreviewMarkup = "";
86
+ const showTextPane = options.showTextPane ?? true;
87
+ const showInspector = options.showInspector ?? true;
88
+ const showPreview = options.showPreview ?? true;
89
+ const shortcutsEnabled = options.shortcuts ?? true;
90
+ container.classList.add("wo-editor");
91
+ container.dataset.woShowInspector = String(showInspector);
92
+ container.dataset.woShowTextPane = String(showTextPane);
93
+ container.dataset.woShowPreview = String(showPreview);
94
+ container.innerHTML = buildEditorMarkup();
95
+ const toolbar = queryRequired(container, "[data-editor-toolbar]");
96
+ const outline = queryRequired(container, "[data-editor-outline]");
97
+ const stageShell = queryRequired(container, "[data-editor-stage-shell]");
98
+ const stage = queryRequired(container, "[data-editor-stage]");
99
+ const overlay = queryRequired(container, "[data-editor-overlay]");
100
+ const diagnosticsPanel = queryRequired(container, "[data-editor-diagnostics]");
101
+ const sourceDiagnostics = queryRequired(container, "[data-editor-source-diagnostics]");
102
+ const statusBar = queryRequired(container, "[data-editor-status]");
103
+ const liveRegion = queryRequired(container, "[data-editor-live]");
104
+ const inspector = queryRequired(container, "[data-editor-inspector]");
105
+ const sourcePane = queryRequired(container, "[data-editor-source]");
106
+ const previewVisual = queryRequired(container, "[data-editor-preview-visual]");
107
+ const previewMarkup = queryRequired(container, "[data-editor-preview-markup]");
108
+ let viewer = null;
109
+ viewer = createInteractiveViewer(stage, {
110
+ source: canonicalSource,
111
+ width: options.viewerWidth ?? 1120,
112
+ height: options.viewerHeight ?? 680,
113
+ preset: atlasDocument.system?.defaults.preset ?? "atlas-card",
114
+ projection: "document",
115
+ minimap: true,
116
+ tooltipMode: "hover",
117
+ onSelectionChange(selectedObject) {
118
+ if (ignoreViewerSelection || !selectedObject) {
119
+ if (!ignoreViewerSelection && selection?.kind === "object") {
120
+ setSelection(null, false, true);
121
+ }
122
+ return;
123
+ }
124
+ setSelection({ kind: "object", id: selectedObject.objectId }, false, true);
125
+ },
126
+ onViewChange() {
127
+ renderStageOverlay();
128
+ },
129
+ });
130
+ toolbar.addEventListener("click", handleToolbarClick);
131
+ outline.addEventListener("click", handleOutlineClick);
132
+ overlay.addEventListener("pointerdown", handleOverlayPointerDown);
133
+ inspector?.addEventListener("input", handleInspectorInput);
134
+ inspector?.addEventListener("change", handleInspectorChange);
135
+ sourcePane?.addEventListener("input", handleSourceInput);
136
+ sourcePane?.addEventListener("change", handleSourceCommit);
137
+ sourcePane?.addEventListener("blur", handleSourceCommit);
138
+ container.addEventListener("keydown", handleEditorKeyDown);
139
+ window.addEventListener("pointermove", handleWindowPointerMove);
140
+ window.addEventListener("pointerup", handleWindowPointerUp);
141
+ window.addEventListener("pointercancel", handleWindowPointerUp);
142
+ const api = {
143
+ setSource(nextSource) {
144
+ const applied = applySourceText(nextSource, false);
145
+ if (applied) {
146
+ resetHistory();
147
+ renderToolbar();
148
+ }
149
+ },
150
+ setAtlasDocument(document) {
151
+ replaceAtlasDocument(document, false, selection, false);
152
+ resetHistory();
153
+ renderToolbar();
154
+ },
155
+ getSource() {
156
+ return canonicalSource;
157
+ },
158
+ getAtlasDocument() {
159
+ return cloneAtlasDocument(atlasDocument);
160
+ },
161
+ getDiagnostics() {
162
+ return diagnostics.map(cloneResolvedDiagnostic);
163
+ },
164
+ getSelection() {
165
+ return selection ? { path: { ...selection } } : null;
166
+ },
167
+ isDirty() {
168
+ return dirty;
169
+ },
170
+ markSaved() {
171
+ markSavedInternal(true);
172
+ },
173
+ selectPath(path) {
174
+ setSelection(path, true, true);
175
+ },
176
+ canUndo() {
177
+ return history.length > 0;
178
+ },
179
+ canRedo() {
180
+ return future.length > 0;
181
+ },
182
+ undo() {
183
+ const entry = history.pop();
184
+ if (!entry) {
185
+ return false;
186
+ }
187
+ future.unshift(createHistoryEntry());
188
+ restoreHistoryEntry(entry);
189
+ return true;
190
+ },
191
+ redo() {
192
+ const entry = future.shift();
193
+ if (!entry) {
194
+ return false;
195
+ }
196
+ history.push(createHistoryEntry());
197
+ restoreHistoryEntry(entry);
198
+ return true;
199
+ },
200
+ addObject(type = "planet") {
201
+ const id = createUniqueId(type, atlasDocument.objects.map((object) => object.id));
202
+ const created = createNewObject(type, id, atlasDocument);
203
+ const nextDocument = insertObject(atlasDocument, created);
204
+ replaceAtlasDocument(nextDocument, true, { kind: "object", id });
205
+ return id;
206
+ },
207
+ addViewpoint() {
208
+ const id = createUniqueId("viewpoint", atlasDocument.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []);
209
+ const created = {
210
+ id,
211
+ label: humanizeIdentifier(id),
212
+ summary: "",
213
+ focusObjectId: null,
214
+ selectedObjectId: null,
215
+ projection: atlasDocument.system?.defaults.view ?? "topdown",
216
+ preset: atlasDocument.system?.defaults.preset ?? null,
217
+ zoom: null,
218
+ rotationDeg: 0,
219
+ layers: {},
220
+ filter: null,
221
+ };
222
+ const nextDocument = cloneAtlasDocument(atlasDocument);
223
+ nextDocument.system?.viewpoints.push(created);
224
+ nextDocument.system?.viewpoints.sort((left, right) => left.id.localeCompare(right.id));
225
+ replaceAtlasDocument(nextDocument, true, { kind: "viewpoint", id });
226
+ return id;
227
+ },
228
+ addAnnotation() {
229
+ const id = createUniqueId("annotation", atlasDocument.system?.annotations.map((annotation) => annotation.id) ?? []);
230
+ const created = {
231
+ id,
232
+ label: humanizeIdentifier(id),
233
+ targetObjectId: null,
234
+ body: "",
235
+ tags: [],
236
+ sourceObjectId: null,
237
+ };
238
+ const nextDocument = cloneAtlasDocument(atlasDocument);
239
+ nextDocument.system?.annotations.push(created);
240
+ nextDocument.system?.annotations.sort((left, right) => left.id.localeCompare(right.id));
241
+ replaceAtlasDocument(nextDocument, true, { kind: "annotation", id });
242
+ return id;
243
+ },
244
+ addMetadata(key, value) {
245
+ const metadataKey = key?.trim() || createUniqueId("metadata", Object.keys(atlasDocument.system?.atlasMetadata ?? {}));
246
+ const nextDocument = cloneAtlasDocument(atlasDocument);
247
+ if (nextDocument.system) {
248
+ nextDocument.system.atlasMetadata[metadataKey] = value ?? "";
249
+ }
250
+ replaceAtlasDocument(nextDocument, true, { kind: "metadata", key: metadataKey });
251
+ return metadataKey;
252
+ },
253
+ removeSelection() {
254
+ if (!selection || selection.kind === "system" || selection.kind === "defaults") {
255
+ return false;
256
+ }
257
+ const nextDocument = removeSelectedNode(atlasDocument, selection);
258
+ replaceAtlasDocument(nextDocument, true, { kind: "system" });
259
+ return true;
260
+ },
261
+ exportSvg() {
262
+ return viewer.exportSvg();
263
+ },
264
+ exportEmbedMarkup() {
265
+ return buildEmbedMarkup(getCurrentSourceForExport(), atlasDocument);
266
+ },
267
+ destroy() {
268
+ if (destroyed) {
269
+ return;
270
+ }
271
+ destroyed = true;
272
+ toolbar.removeEventListener("click", handleToolbarClick);
273
+ outline.removeEventListener("click", handleOutlineClick);
274
+ overlay.removeEventListener("pointerdown", handleOverlayPointerDown);
275
+ inspector?.removeEventListener("input", handleInspectorInput);
276
+ inspector?.removeEventListener("change", handleInspectorChange);
277
+ sourcePane?.removeEventListener("input", handleSourceInput);
278
+ sourcePane?.removeEventListener("change", handleSourceCommit);
279
+ sourcePane?.removeEventListener("blur", handleSourceCommit);
280
+ container.removeEventListener("keydown", handleEditorKeyDown);
281
+ window.removeEventListener("pointermove", handleWindowPointerMove);
282
+ window.removeEventListener("pointerup", handleWindowPointerUp);
283
+ window.removeEventListener("pointercancel", handleWindowPointerUp);
284
+ clearSourceInputTimer();
285
+ clearPreviewTimer();
286
+ viewer.destroy();
287
+ container.innerHTML = "";
288
+ container.classList.remove("wo-editor");
289
+ },
290
+ };
291
+ renderAll(true);
292
+ updateDirtyState(true);
293
+ return api;
294
+ function createHistoryEntry() {
295
+ return {
296
+ atlasDocument: cloneAtlasDocument(atlasDocument),
297
+ selection: selection ? { ...selection } : null,
298
+ source: canonicalSource,
299
+ };
300
+ }
301
+ function resetHistory() {
302
+ history.length = 0;
303
+ future.length = 0;
304
+ renderToolbar();
305
+ }
306
+ function clearSourceInputTimer() {
307
+ if (sourceInputTimer !== null) {
308
+ window.clearTimeout(sourceInputTimer);
309
+ sourceInputTimer = null;
310
+ }
311
+ }
312
+ function clearPreviewTimer() {
313
+ if (previewTimer !== null) {
314
+ window.clearTimeout(previewTimer);
315
+ previewTimer = null;
316
+ }
317
+ }
318
+ function getCurrentSourceForExport() {
319
+ if (dragState?.changed) {
320
+ return formatDocument(atlasDocument, { schema: "2.0" });
321
+ }
322
+ return canonicalSource;
323
+ }
324
+ function updateDirtyState(forceEmit = false) {
325
+ const nextDirty = sourceText !== savedSource || canonicalSource !== savedSource || dragState?.changed === true;
326
+ if (!forceEmit && nextDirty === dirty) {
327
+ return;
328
+ }
329
+ dirty = nextDirty;
330
+ renderStatusBar();
331
+ options.onDirtyChange?.(dirty);
332
+ }
333
+ function markSavedInternal(emit = false) {
334
+ sourceText = canonicalSource;
335
+ renderSourcePane();
336
+ savedSource = canonicalSource;
337
+ updateDirtyState(emit);
338
+ }
339
+ function restoreHistoryEntry(entry) {
340
+ clearSourceInputTimer();
341
+ atlasDocument = cloneAtlasDocument(entry.atlasDocument);
342
+ canonicalSource = entry.source;
343
+ sourceText = canonicalSource;
344
+ diagnostics = collectDocumentDiagnostics(atlasDocument);
345
+ selection = normalizeSelection(entry.selection);
346
+ syncViewer({ preserveCamera: true, applyViewpointSelection: selection?.kind === "viewpoint" });
347
+ setSelection(selection, false, false);
348
+ renderAll();
349
+ updateDirtyState();
350
+ emitSnapshot();
351
+ }
352
+ function replaceAtlasDocument(nextDocument, pushHistory, nextSelection, preserveSourceText = false) {
353
+ if (pushHistory) {
354
+ history.push(createHistoryEntry());
355
+ future.length = 0;
356
+ }
357
+ clearSourceInputTimer();
358
+ atlasDocument = cloneAtlasDocument(nextDocument);
359
+ canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
360
+ if (!preserveSourceText) {
361
+ sourceText = canonicalSource;
362
+ }
363
+ diagnostics = collectDocumentDiagnostics(atlasDocument);
364
+ selection = normalizeSelection(nextSelection);
365
+ syncViewer({
366
+ preserveCamera: selection?.kind !== "viewpoint",
367
+ applyViewpointSelection: selection?.kind === "viewpoint",
368
+ });
369
+ setSelection(selection, false, false);
370
+ renderAll();
371
+ updateDirtyState();
372
+ emitSnapshot();
373
+ }
374
+ function applySourceText(nextSourceText, commitHistory) {
375
+ sourceText = nextSourceText;
376
+ if (sourcePane && sourcePane.value !== nextSourceText) {
377
+ sourcePane.value = nextSourceText;
378
+ }
379
+ const loaded = loadWorldOrbitSourceWithDiagnostics(nextSourceText);
380
+ if (!loaded.ok || !loaded.value) {
381
+ diagnostics = loaded.diagnostics.map((diagnostic) => ({
382
+ diagnostic,
383
+ path: null,
384
+ }));
385
+ renderDiagnostics();
386
+ renderSourceDiagnostics();
387
+ renderOutline();
388
+ renderInspector();
389
+ renderStatusBar();
390
+ updateLiveRegion();
391
+ updateDirtyState();
392
+ options.onDiagnosticsChange?.(diagnostics.map(cloneResolvedDiagnostic));
393
+ return false;
394
+ }
395
+ const nextDocument = loaded.value.atlasDocument ?? upgradeDocumentToV2(loaded.value.document);
396
+ const loadedDiagnostics = resolveAtlasDiagnostics(nextDocument, loaded.diagnostics);
397
+ if (commitHistory) {
398
+ history.push(createHistoryEntry());
399
+ future.length = 0;
400
+ sourceText = formatDocument(nextDocument, { schema: "2.0" });
401
+ }
402
+ atlasDocument = cloneAtlasDocument(nextDocument);
403
+ canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
404
+ diagnostics = mergeDiagnostics(loadedDiagnostics, collectDocumentDiagnostics(atlasDocument));
405
+ selection = normalizeSelection(selection);
406
+ syncViewer({
407
+ preserveCamera: selection?.kind !== "viewpoint",
408
+ applyViewpointSelection: selection?.kind === "viewpoint",
409
+ });
410
+ renderAll();
411
+ updateDirtyState();
412
+ emitSnapshot();
413
+ return true;
414
+ }
415
+ function syncViewer(options = {}) {
416
+ if (!viewer) {
417
+ return;
418
+ }
419
+ const previousState = viewer.getState();
420
+ const currentRenderOptions = viewer.getRenderOptions();
421
+ const nextPreset = atlasDocument.system?.defaults.preset ?? "atlas-card";
422
+ ignoreViewerSelection = true;
423
+ if (currentRenderOptions.preset !== nextPreset || currentRenderOptions.projection !== "document") {
424
+ viewer.setRenderOptions({
425
+ preset: nextPreset,
426
+ projection: "document",
427
+ });
428
+ }
429
+ viewer.setDocument(materializeAtlasDocument(atlasDocument));
430
+ if (options.applyViewpointSelection && selection?.kind === "viewpoint" && selection.id) {
431
+ viewer.goToViewpoint(selection.id);
432
+ }
433
+ else if (options.preserveCamera !== false) {
434
+ viewer.setState({
435
+ ...previousState,
436
+ selectedObjectId: selection?.kind === "object" ? selection.id ?? null : null,
437
+ });
438
+ }
439
+ else if (selection?.kind === "object" && selection.id) {
440
+ viewer.focusObject(selection.id);
441
+ }
442
+ ignoreViewerSelection = false;
443
+ }
444
+ function emitSnapshot() {
445
+ const snapshot = {
446
+ source: canonicalSource,
447
+ atlasDocument: cloneAtlasDocument(atlasDocument),
448
+ diagnostics: diagnostics.map(cloneResolvedDiagnostic),
449
+ selection: selection ? { path: { ...selection } } : null,
450
+ };
451
+ options.onDiagnosticsChange?.(snapshot.diagnostics);
452
+ options.onSelectionChange?.(snapshot.selection);
453
+ options.onChange?.(snapshot);
454
+ }
455
+ function setSelection(nextSelection, syncViewerSelection, emit = true) {
456
+ selection = normalizeSelection(nextSelection);
457
+ if (syncViewerSelection) {
458
+ ignoreViewerSelection = true;
459
+ if (selection?.kind === "object" && selection.id) {
460
+ viewer.focusObject(selection.id);
461
+ }
462
+ else if (selection?.kind === "viewpoint" && selection.id) {
463
+ viewer.goToViewpoint(selection.id);
464
+ }
465
+ ignoreViewerSelection = false;
466
+ }
467
+ renderToolbar();
468
+ renderOutline();
469
+ renderInspector();
470
+ renderStageOverlay();
471
+ renderStatusBar();
472
+ updateLiveRegion();
473
+ if (emit) {
474
+ options.onSelectionChange?.(selection ? { path: { ...selection } } : null);
475
+ }
476
+ }
477
+ function normalizeSelection(nextSelection) {
478
+ if (!nextSelection) {
479
+ return null;
480
+ }
481
+ const node = getAtlasDocumentNode(atlasDocument, nextSelection);
482
+ if (node === null || node === undefined) {
483
+ return nextSelection.kind === "system" || nextSelection.kind === "defaults"
484
+ ? nextSelection
485
+ : null;
486
+ }
487
+ return nextSelection;
488
+ }
489
+ function renderAll(immediatePreview = false) {
490
+ renderToolbar();
491
+ renderOutline();
492
+ renderDiagnostics();
493
+ renderSourceDiagnostics();
494
+ renderInspector();
495
+ renderSourcePane();
496
+ renderPreview(immediatePreview);
497
+ renderStageOverlay();
498
+ renderStatusBar();
499
+ updateLiveRegion();
500
+ }
501
+ function renderToolbar() {
502
+ const objectType = toolbar.querySelector("[data-editor-add-object-type]")
503
+ ?.value ?? "planet";
504
+ toolbar.innerHTML = `
505
+ <div class="wo-editor-toolbar-group">
506
+ <select data-editor-add-object-type>
507
+ ${OBJECT_TYPES.map((type) => `<option value="${escapeHtml(type)}"${type === objectType ? " selected" : ""}>${escapeHtml(humanizeIdentifier(type))}</option>`).join("")}
508
+ </select>
509
+ <button type="button" data-editor-action="add-object">Add object</button>
510
+ <button type="button" data-editor-action="add-viewpoint">Add viewpoint</button>
511
+ <button type="button" data-editor-action="add-annotation">Add annotation</button>
512
+ <button type="button" data-editor-action="add-metadata">Add metadata</button>
513
+ </div>
514
+ <div class="wo-editor-toolbar-group">
515
+ <button type="button" data-editor-action="remove"${!selection || selection.kind === "system" || selection.kind === "defaults" ? " disabled" : ""}>Remove</button>
516
+ <button type="button" data-editor-action="undo"${history.length === 0 ? " disabled" : ""}>Undo</button>
517
+ <button type="button" data-editor-action="redo"${future.length === 0 ? " disabled" : ""}>Redo</button>
518
+ <button type="button" data-editor-action="format">Format source</button>
519
+ </div>
520
+ `;
521
+ }
522
+ function renderOutline() {
523
+ const activeKey = selectionKey(selection);
524
+ const diagnosticBuckets = buildDiagnosticBuckets(diagnostics);
525
+ const metadataEntries = Object.entries(atlasDocument.system?.atlasMetadata ?? {}).sort(([left], [right]) => left.localeCompare(right));
526
+ outline.innerHTML = `
527
+ <div class="wo-editor-outline-section">
528
+ <h3>Atlas</h3>
529
+ ${renderOutlineButton({ kind: "system" }, "System", activeKey, diagnosticBuckets)}
530
+ ${renderOutlineButton({ kind: "defaults" }, "Defaults", activeKey, diagnosticBuckets)}
531
+ </div>
532
+ <div class="wo-editor-outline-section">
533
+ <h3>Metadata</h3>
534
+ ${metadataEntries.length > 0
535
+ ? metadataEntries
536
+ .map(([key]) => renderOutlineButton({ kind: "metadata", key }, key, activeKey, diagnosticBuckets))
537
+ .join("")
538
+ : `<p class="wo-editor-empty">No atlas metadata yet.</p>`}
539
+ </div>
540
+ <div class="wo-editor-outline-section">
541
+ <h3>Viewpoints</h3>
542
+ ${(atlasDocument.system?.viewpoints.length ?? 0) > 0
543
+ ? atlasDocument.system?.viewpoints
544
+ .map((viewpoint) => renderOutlineButton({ kind: "viewpoint", id: viewpoint.id }, viewpoint.label, activeKey, diagnosticBuckets))
545
+ .join("")
546
+ : `<p class="wo-editor-empty">No viewpoints yet.</p>`}
547
+ </div>
548
+ <div class="wo-editor-outline-section">
549
+ <h3>Annotations</h3>
550
+ ${(atlasDocument.system?.annotations.length ?? 0) > 0
551
+ ? atlasDocument.system?.annotations
552
+ .map((annotation) => renderOutlineButton({ kind: "annotation", id: annotation.id }, annotation.label, activeKey, diagnosticBuckets))
553
+ .join("")
554
+ : `<p class="wo-editor-empty">No annotations yet.</p>`}
555
+ </div>
556
+ <div class="wo-editor-outline-section">
557
+ <h3>Objects</h3>
558
+ ${atlasDocument.objects.length > 0
559
+ ? atlasDocument.objects
560
+ .map((object) => renderOutlineButton({ kind: "object", id: object.id }, `${object.id} - ${object.type}`, activeKey, diagnosticBuckets))
561
+ .join("")
562
+ : `<p class="wo-editor-empty">No objects yet.</p>`}
563
+ </div>
564
+ `;
565
+ }
566
+ function renderDiagnostics() {
567
+ diagnosticsPanel.innerHTML =
568
+ diagnostics.length === 0
569
+ ? `<p class="wo-editor-empty">No diagnostics.</p>`
570
+ : diagnostics
571
+ .map((entry) => {
572
+ const pathLabel = entry.path ? describePath(entry.path) : "Source";
573
+ const location = formatDiagnosticLocation(entry);
574
+ return `<article class="wo-editor-diagnostic wo-editor-diagnostic-${escapeHtml(entry.diagnostic.severity)}">
575
+ <strong>${escapeHtml(entry.diagnostic.severity.toUpperCase())}</strong>
576
+ <span>${escapeHtml(pathLabel)}${location ? ` · ${escapeHtml(location)}` : ""}</span>
577
+ <p>${escapeHtml(entry.diagnostic.message)}</p>
578
+ </article>`;
579
+ })
580
+ .join("");
581
+ }
582
+ function renderSourceDiagnostics() {
583
+ const sourceEntries = diagnostics.filter((entry) => !entry.path || entry.diagnostic.line !== undefined);
584
+ sourceDiagnostics.innerHTML =
585
+ sourceEntries.length === 0
586
+ ? `<p class="wo-editor-empty">No source diagnostics.</p>`
587
+ : sourceEntries
588
+ .map((entry) => {
589
+ const location = formatDiagnosticLocation(entry) ?? "Source";
590
+ return `<article class="wo-editor-diagnostic wo-editor-diagnostic-${escapeHtml(entry.diagnostic.severity)}">
591
+ <strong>${escapeHtml(entry.diagnostic.severity.toUpperCase())}</strong>
592
+ <span>${escapeHtml(location)}</span>
593
+ <p>${escapeHtml(entry.diagnostic.message)}</p>
594
+ </article>`;
595
+ })
596
+ .join("");
597
+ }
598
+ function renderInspector() {
599
+ if (!inspector) {
600
+ return;
601
+ }
602
+ const formState = {
603
+ selection: selection ? { path: { ...selection } } : null,
604
+ system: atlasDocument.system,
605
+ viewpoints: atlasDocument.system?.viewpoints ?? [],
606
+ objects: atlasDocument.objects,
607
+ };
608
+ if (!selection) {
609
+ inspector.innerHTML = `<p class="wo-editor-empty">Select an atlas node or object to edit it.</p>`;
610
+ return;
611
+ }
612
+ const diagnosticSummary = renderInspectorDiagnosticSummary(selection, diagnostics);
613
+ switch (selection.kind) {
614
+ case "system":
615
+ inspector.innerHTML = diagnosticSummary + renderSystemInspector(formState);
616
+ decorateInspectorDiagnostics(selection, diagnostics);
617
+ return;
618
+ case "defaults":
619
+ inspector.innerHTML = diagnosticSummary + renderDefaultsInspector(formState);
620
+ decorateInspectorDiagnostics(selection, diagnostics);
621
+ return;
622
+ case "metadata":
623
+ inspector.innerHTML = diagnosticSummary + renderMetadataInspector(formState, selection.key ?? "");
624
+ decorateInspectorDiagnostics(selection, diagnostics);
625
+ return;
626
+ case "viewpoint":
627
+ inspector.innerHTML = diagnosticSummary + renderViewpointInspector(formState, selection.id ?? "");
628
+ decorateInspectorDiagnostics(selection, diagnostics);
629
+ return;
630
+ case "annotation":
631
+ inspector.innerHTML = diagnosticSummary + renderAnnotationInspector(formState, selection.id ?? "");
632
+ decorateInspectorDiagnostics(selection, diagnostics);
633
+ return;
634
+ case "object":
635
+ inspector.innerHTML = diagnosticSummary + renderObjectInspector(formState, selection.id ?? "");
636
+ decorateInspectorDiagnostics(selection, diagnostics);
637
+ return;
638
+ }
639
+ }
640
+ function renderSourcePane() {
641
+ if (!sourcePane) {
642
+ return;
643
+ }
644
+ if (sourcePane.value !== sourceText) {
645
+ sourcePane.value = sourceText;
646
+ }
647
+ }
648
+ function renderPreview(immediate = false) {
649
+ if (immediate) {
650
+ renderPreviewNow();
651
+ return;
652
+ }
653
+ schedulePreviewRender();
654
+ }
655
+ function renderStageOverlay() {
656
+ if (!viewer) {
657
+ return;
658
+ }
659
+ overlay.innerHTML = "";
660
+ if (selection?.kind !== "object" || !selection.id) {
661
+ return;
662
+ }
663
+ const details = viewer.getObjectDetails(selection.id);
664
+ if (!details) {
665
+ return;
666
+ }
667
+ renderHintMarker(details.renderObject.x, details.renderObject.y, details.renderObject.objectId);
668
+ if (details.parent) {
669
+ renderHintMarker(details.parent.x, details.parent.y, `Parent: ${details.parent.objectId}`, true);
670
+ }
671
+ if (details.renderObject.anchorX !== undefined && details.renderObject.anchorY !== undefined) {
672
+ renderHintMarker(details.renderObject.anchorX, details.renderObject.anchorY, "Anchor", true);
673
+ }
674
+ if (details.object.placement?.mode === "orbit" && details.orbit) {
675
+ const phasePoint = projectStagePoint({
676
+ x: details.renderObject.x,
677
+ y: details.renderObject.y,
678
+ });
679
+ overlay.append(createHandleElement("orbit-phase", details.objectId, phasePoint, "Phase"));
680
+ const axisPoint = projectOrbitRadiusHandle(details);
681
+ overlay.append(createHandleElement("orbit-radius", details.objectId, axisPoint, "Size"));
682
+ }
683
+ if (details.object.placement?.mode === "at") {
684
+ overlay.append(createHandleElement("at-reference", details.objectId, projectStagePoint({ x: details.renderObject.x, y: details.renderObject.y }), "Reference"));
685
+ const badge = document.createElement("div");
686
+ badge.className = "wo-editor-hint wo-editor-hint-note";
687
+ badge.textContent = "Drag to an object center or nearby Lagrange point.";
688
+ overlay.append(badge);
689
+ }
690
+ if (details.object.placement?.mode === "surface") {
691
+ overlay.append(createHandleElement("surface-target", details.objectId, projectStagePoint({ x: details.renderObject.x, y: details.renderObject.y }), "Surface"));
692
+ const badge = document.createElement("div");
693
+ badge.className = "wo-editor-hint wo-editor-hint-note";
694
+ badge.textContent = "Drag onto another surface-capable body.";
695
+ overlay.append(badge);
696
+ }
697
+ if (details.object.placement?.mode === "free") {
698
+ overlay.append(createHandleElement("free-distance", details.objectId, projectStagePoint({ x: details.renderObject.x, y: details.renderObject.y }), "Offset"));
699
+ const badge = document.createElement("div");
700
+ badge.className = "wo-editor-hint wo-editor-hint-note";
701
+ badge.textContent = "Drag horizontally to change free offset.";
702
+ overlay.append(badge);
703
+ }
704
+ const placementDiagnostics = getPlacementDiagnosticsForSelection(selection, diagnostics).slice(0, 3);
705
+ if (placementDiagnostics.length > 0) {
706
+ const panel = document.createElement("div");
707
+ panel.className = "wo-editor-overlay-diagnostics";
708
+ panel.setAttribute("role", "status");
709
+ panel.setAttribute("aria-live", "polite");
710
+ panel.innerHTML = placementDiagnostics
711
+ .map((entry) => `<article class="wo-editor-overlay-diagnostic wo-editor-overlay-diagnostic-${escapeHtml(entry.diagnostic.severity)}">
712
+ <strong>${escapeHtml(entry.diagnostic.severity.toUpperCase())}</strong>
713
+ <p>${escapeHtml(entry.diagnostic.message)}</p>
714
+ </article>`)
715
+ .join("");
716
+ overlay.append(panel);
717
+ }
718
+ }
719
+ function renderHintMarker(worldX, worldY, label, subtle = false) {
720
+ const point = projectStagePoint({ x: worldX, y: worldY });
721
+ const marker = document.createElement("div");
722
+ marker.className = `wo-editor-hint${subtle ? " is-subtle" : ""}`;
723
+ marker.style.left = `${point.x}px`;
724
+ marker.style.top = `${point.y}px`;
725
+ marker.textContent = label;
726
+ overlay.append(marker);
727
+ }
728
+ function projectOrbitRadiusHandle(details) {
729
+ const orbit = details.orbit;
730
+ if (!orbit) {
731
+ return projectStagePoint({ x: details.renderObject.x, y: details.renderObject.y });
732
+ }
733
+ const localPoint = {
734
+ x: orbit.cx + (orbit.kind === "circle" ? orbit.radius ?? 0 : orbit.rx ?? 0),
735
+ y: orbit.cy,
736
+ };
737
+ const rotatedPoint = rotatePoint(localPoint, { x: orbit.cx, y: orbit.cy }, orbit.rotationDeg);
738
+ return projectStagePoint(rotatedPoint);
739
+ }
740
+ function projectStagePoint(point) {
741
+ const scene = viewer.getScene();
742
+ const state = viewer.getState();
743
+ const center = {
744
+ x: scene.width / 2,
745
+ y: scene.height / 2,
746
+ };
747
+ const rotated = rotatePoint(point, center, state.rotationDeg);
748
+ const viewportPoint = {
749
+ x: center.x + (rotated.x - center.x) * state.scale + state.translateX,
750
+ y: center.y + (rotated.y - center.y) * state.scale + state.translateY,
751
+ };
752
+ const svg = stage.querySelector("svg");
753
+ if (!svg) {
754
+ return viewportPoint;
755
+ }
756
+ const svgRect = svg.getBoundingClientRect();
757
+ const stageRect = stageShell.getBoundingClientRect();
758
+ return {
759
+ x: svgRect.left -
760
+ stageRect.left +
761
+ (viewportPoint.x / Math.max(scene.width, 1)) * svgRect.width,
762
+ y: svgRect.top -
763
+ stageRect.top +
764
+ (viewportPoint.y / Math.max(scene.height, 1)) * svgRect.height,
765
+ };
766
+ }
767
+ function handleToolbarClick(event) {
768
+ const button = event.target?.closest("[data-editor-action]");
769
+ if (!button) {
770
+ return;
771
+ }
772
+ switch (button.dataset.editorAction) {
773
+ case "add-object": {
774
+ const type = toolbar.querySelector("[data-editor-add-object-type]")
775
+ ?.value;
776
+ api.addObject(type ?? "planet");
777
+ return;
778
+ }
779
+ case "add-viewpoint":
780
+ api.addViewpoint();
781
+ return;
782
+ case "add-annotation":
783
+ api.addAnnotation();
784
+ return;
785
+ case "add-metadata":
786
+ api.addMetadata();
787
+ return;
788
+ case "remove":
789
+ api.removeSelection();
790
+ return;
791
+ case "undo":
792
+ api.undo();
793
+ return;
794
+ case "redo":
795
+ api.redo();
796
+ return;
797
+ case "format":
798
+ clearSourceInputTimer();
799
+ sourceText = canonicalSource;
800
+ renderSourcePane();
801
+ updateDirtyState();
802
+ return;
803
+ }
804
+ }
805
+ function handleOutlineClick(event) {
806
+ const button = event.target?.closest("[data-path-kind]");
807
+ if (!button) {
808
+ return;
809
+ }
810
+ setSelection({
811
+ kind: button.dataset.pathKind,
812
+ id: button.dataset.pathId || undefined,
813
+ key: button.dataset.pathKey || undefined,
814
+ }, true, true);
815
+ }
816
+ function handleInspectorInput() {
817
+ applyInspectorState(false);
818
+ }
819
+ function handleInspectorChange() {
820
+ applyInspectorState(true);
821
+ }
822
+ function applyInspectorState(commitHistory) {
823
+ if (!selection || !inspector) {
824
+ return;
825
+ }
826
+ switch (selection.kind) {
827
+ case "system":
828
+ replaceAtlasDocument(buildSystemDocumentFromInspector(), commitHistory, selection, false);
829
+ return;
830
+ case "defaults":
831
+ replaceAtlasDocument(buildDefaultsDocumentFromInspector(), commitHistory, selection, false);
832
+ return;
833
+ case "metadata":
834
+ replaceAtlasDocument(buildMetadataDocumentFromInspector(selection.key ?? ""), commitHistory, selection, false);
835
+ return;
836
+ case "viewpoint":
837
+ replaceAtlasDocument(buildViewpointDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
838
+ return;
839
+ case "annotation":
840
+ replaceAtlasDocument(buildAnnotationDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
841
+ return;
842
+ case "object":
843
+ replaceAtlasDocument(buildObjectDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
844
+ return;
845
+ }
846
+ }
847
+ function handleSourceInput() {
848
+ sourceText = sourcePane?.value ?? "";
849
+ updateDirtyState();
850
+ clearSourceInputTimer();
851
+ sourceInputTimer = window.setTimeout(() => {
852
+ sourceInputTimer = null;
853
+ applySourceText(sourceText, false);
854
+ }, SOURCE_INPUT_DEBOUNCE_MS);
855
+ }
856
+ function handleSourceCommit() {
857
+ clearSourceInputTimer();
858
+ const committed = applySourceText(sourcePane?.value ?? "", true);
859
+ if (committed) {
860
+ renderSourcePane();
861
+ }
862
+ }
863
+ function handleEditorKeyDown(event) {
864
+ if (event.key === "Escape" && dragState) {
865
+ event.preventDefault();
866
+ cancelActiveDrag();
867
+ return;
868
+ }
869
+ if (!shortcutsEnabled || event.defaultPrevented || shouldIgnoreShortcutEvent(event)) {
870
+ return;
871
+ }
872
+ const isUndo = (event.ctrlKey || event.metaKey) && !event.shiftKey && event.key.toLowerCase() === "z";
873
+ const isRedo = (event.ctrlKey || event.metaKey) &&
874
+ ((event.shiftKey && event.key.toLowerCase() === "z") || event.key.toLowerCase() === "y");
875
+ if (isUndo) {
876
+ event.preventDefault();
877
+ api.undo();
878
+ return;
879
+ }
880
+ if (isRedo) {
881
+ event.preventDefault();
882
+ api.redo();
883
+ }
884
+ }
885
+ function handleOverlayPointerDown(event) {
886
+ const handle = event.target?.closest("[data-handle-kind]");
887
+ if (!handle) {
888
+ return;
889
+ }
890
+ const objectId = handle.dataset.objectId;
891
+ const kind = handle.dataset.handleKind;
892
+ if (!objectId ||
893
+ !["orbit-phase", "orbit-radius", "at-reference", "surface-target", "free-distance"].includes(kind)) {
894
+ return;
895
+ }
896
+ clearSourceInputTimer();
897
+ if (sourceText !== canonicalSource) {
898
+ const committed = applySourceText(sourceText, true);
899
+ if (!committed) {
900
+ event.preventDefault();
901
+ return;
902
+ }
903
+ }
904
+ dragState = {
905
+ kind,
906
+ objectId,
907
+ pointerId: event.pointerId,
908
+ startedFrom: createHistoryEntry(),
909
+ changed: false,
910
+ };
911
+ handle.setPointerCapture?.(event.pointerId);
912
+ event.preventDefault();
913
+ }
914
+ function handleWindowPointerMove(event) {
915
+ if (!dragState ||
916
+ dragState.pointerId !== event.pointerId ||
917
+ selection?.kind !== "object" ||
918
+ selection.id !== dragState.objectId) {
919
+ return;
920
+ }
921
+ const details = viewer.getObjectDetails(dragState.objectId);
922
+ if (!details) {
923
+ return;
924
+ }
925
+ const pointer = getWorldPointFromClient(event.clientX, event.clientY);
926
+ let nextDocument = atlasDocument;
927
+ switch (dragState.kind) {
928
+ case "orbit-phase":
929
+ if (details.object.placement?.mode === "orbit" && details.orbit) {
930
+ nextDocument = updateOrbitPhase(atlasDocument, dragState.objectId, details, pointer);
931
+ }
932
+ break;
933
+ case "orbit-radius":
934
+ if (details.object.placement?.mode === "orbit" && details.orbit) {
935
+ nextDocument = updateOrbitRadius(atlasDocument, dragState.objectId, details, pointer);
936
+ }
937
+ break;
938
+ case "at-reference":
939
+ if (details.object.placement?.mode === "at") {
940
+ nextDocument = updateAtReference(atlasDocument, dragState.objectId, viewer.getScene(), pointer);
941
+ }
942
+ break;
943
+ case "surface-target":
944
+ if (details.object.placement?.mode === "surface") {
945
+ nextDocument = updateSurfaceTarget(atlasDocument, dragState.objectId, viewer.getScene(), pointer);
946
+ }
947
+ break;
948
+ case "free-distance":
949
+ if (details.object.placement?.mode === "free") {
950
+ nextDocument = updateFreeDistance(atlasDocument, dragState.objectId, viewer.getScene(), details, pointer);
951
+ }
952
+ break;
953
+ }
954
+ if (nextDocument === atlasDocument) {
955
+ return;
956
+ }
957
+ dragState.changed = true;
958
+ atlasDocument = cloneAtlasDocument(nextDocument);
959
+ diagnostics = collectDocumentDiagnostics(atlasDocument);
960
+ syncViewer({ preserveCamera: true, applyViewpointSelection: false });
961
+ renderDiagnostics();
962
+ renderSourceDiagnostics();
963
+ renderOutline();
964
+ renderInspector();
965
+ renderStageOverlay();
966
+ renderStatusBar();
967
+ updateLiveRegion();
968
+ schedulePreviewRender();
969
+ updateDirtyState();
970
+ }
971
+ function handleWindowPointerUp(event) {
972
+ if (!dragState || dragState.pointerId !== event.pointerId) {
973
+ return;
974
+ }
975
+ if (!dragState.changed) {
976
+ dragState = null;
977
+ return;
978
+ }
979
+ history.push(dragState.startedFrom);
980
+ future.length = 0;
981
+ canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
982
+ sourceText = canonicalSource;
983
+ dragState = null;
984
+ renderAll();
985
+ updateDirtyState();
986
+ emitSnapshot();
987
+ }
988
+ function cancelActiveDrag() {
989
+ if (!dragState) {
990
+ return;
991
+ }
992
+ const startedFrom = dragState.startedFrom;
993
+ dragState = null;
994
+ atlasDocument = cloneAtlasDocument(startedFrom.atlasDocument);
995
+ canonicalSource = startedFrom.source;
996
+ sourceText = canonicalSource;
997
+ diagnostics = collectDocumentDiagnostics(atlasDocument);
998
+ setSelection(startedFrom.selection, false, false);
999
+ syncViewer({ preserveCamera: true, applyViewpointSelection: selection?.kind === "viewpoint" });
1000
+ renderAll();
1001
+ updateDirtyState();
1002
+ emitSnapshot();
1003
+ }
1004
+ function getWorldPointFromClient(clientX, clientY) {
1005
+ const svg = stage.querySelector("svg");
1006
+ const scene = viewer.getScene();
1007
+ if (!svg) {
1008
+ return { x: scene.width / 2, y: scene.height / 2 };
1009
+ }
1010
+ const rect = svg.getBoundingClientRect();
1011
+ const viewportPoint = {
1012
+ x: ((clientX - rect.left) / Math.max(rect.width, 1)) * scene.width,
1013
+ y: ((clientY - rect.top) / Math.max(rect.height, 1)) * scene.height,
1014
+ };
1015
+ return invertViewerPoint(scene, viewer.getState(), viewportPoint);
1016
+ }
1017
+ function buildSystemDocumentFromInspector() {
1018
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1019
+ const form = inspector?.querySelector("form[data-editor-form='system']");
1020
+ if (!form || !nextDocument.system) {
1021
+ return nextDocument;
1022
+ }
1023
+ nextDocument.system.id = readTextInput(form, "system-id") || nextDocument.system.id;
1024
+ nextDocument.system.title = readOptionalTextInput(form, "system-title");
1025
+ return nextDocument;
1026
+ }
1027
+ function buildDefaultsDocumentFromInspector() {
1028
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1029
+ const form = inspector?.querySelector("form[data-editor-form='defaults']");
1030
+ if (!form || !nextDocument.system) {
1031
+ return nextDocument;
1032
+ }
1033
+ nextDocument.system.defaults.view =
1034
+ readTextInput(form, "defaults-view") || "topdown";
1035
+ nextDocument.system.defaults.scale = readOptionalTextInput(form, "defaults-scale");
1036
+ nextDocument.system.defaults.units = readOptionalTextInput(form, "defaults-units");
1037
+ nextDocument.system.defaults.preset =
1038
+ readOptionalTextInput(form, "defaults-preset") ?? null;
1039
+ nextDocument.system.defaults.theme = readOptionalTextInput(form, "defaults-theme");
1040
+ return nextDocument;
1041
+ }
1042
+ function buildMetadataDocumentFromInspector(currentKey) {
1043
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1044
+ const form = inspector?.querySelector("form[data-editor-form='metadata']");
1045
+ if (!form || !nextDocument.system) {
1046
+ return nextDocument;
1047
+ }
1048
+ const nextKey = readTextInput(form, "metadata-key") || currentKey;
1049
+ const nextValue = readOptionalTextInput(form, "metadata-value") ?? "";
1050
+ if (nextKey !== currentKey) {
1051
+ delete nextDocument.system.atlasMetadata[currentKey];
1052
+ nextDocument.system.atlasMetadata[nextKey] = nextValue;
1053
+ selection = { kind: "metadata", key: nextKey };
1054
+ return nextDocument;
1055
+ }
1056
+ nextDocument.system.atlasMetadata[currentKey] = nextValue;
1057
+ return nextDocument;
1058
+ }
1059
+ function buildViewpointDocumentFromInspector(currentId) {
1060
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1061
+ const form = inspector?.querySelector("form[data-editor-form='viewpoint']");
1062
+ const current = nextDocument.system?.viewpoints.find((viewpoint) => viewpoint.id === currentId);
1063
+ if (!form || !current || !nextDocument.system) {
1064
+ return nextDocument;
1065
+ }
1066
+ const nextId = readTextInput(form, "viewpoint-id") || current.id;
1067
+ const replacement = {
1068
+ ...current,
1069
+ id: nextId,
1070
+ label: readTextInput(form, "viewpoint-label") || current.label,
1071
+ summary: readOptionalTextInput(form, "viewpoint-summary") ?? "",
1072
+ focusObjectId: readOptionalTextInput(form, "viewpoint-focus"),
1073
+ selectedObjectId: readOptionalTextInput(form, "viewpoint-select"),
1074
+ projection: readTextInput(form, "viewpoint-projection") ||
1075
+ current.projection,
1076
+ preset: readOptionalTextInput(form, "viewpoint-preset") ??
1077
+ null,
1078
+ zoom: parseNullableNumber(readOptionalTextInput(form, "viewpoint-zoom")),
1079
+ rotationDeg: parseNullableNumber(readOptionalTextInput(form, "viewpoint-rotation")) ?? 0,
1080
+ layers: {
1081
+ background: readCheckbox(form, "layer-background"),
1082
+ guides: readCheckbox(form, "layer-guides"),
1083
+ "orbits-back": readCheckbox(form, "layer-orbits-back"),
1084
+ "orbits-front": readCheckbox(form, "layer-orbits-front"),
1085
+ objects: readCheckbox(form, "layer-objects"),
1086
+ labels: readCheckbox(form, "layer-labels"),
1087
+ metadata: readCheckbox(form, "layer-metadata"),
1088
+ },
1089
+ filter: {
1090
+ query: readOptionalTextInput(form, "filter-query"),
1091
+ objectTypes: parseObjectTypes(readOptionalTextInput(form, "filter-object-types")),
1092
+ tags: splitTokens(readOptionalTextInput(form, "filter-tags")),
1093
+ groupIds: splitTokens(readOptionalTextInput(form, "filter-groups")),
1094
+ },
1095
+ };
1096
+ nextDocument.system.viewpoints = nextDocument.system.viewpoints
1097
+ .filter((viewpoint) => viewpoint.id !== current.id)
1098
+ .concat(replacement)
1099
+ .sort((left, right) => left.id.localeCompare(right.id));
1100
+ if (current.id !== replacement.id) {
1101
+ selection = { kind: "viewpoint", id: replacement.id };
1102
+ }
1103
+ return nextDocument;
1104
+ }
1105
+ function buildAnnotationDocumentFromInspector(currentId) {
1106
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1107
+ const form = inspector?.querySelector("form[data-editor-form='annotation']");
1108
+ const current = nextDocument.system?.annotations.find((annotation) => annotation.id === currentId);
1109
+ if (!form || !current || !nextDocument.system) {
1110
+ return nextDocument;
1111
+ }
1112
+ const nextId = readTextInput(form, "annotation-id") || current.id;
1113
+ const replacement = {
1114
+ ...current,
1115
+ id: nextId,
1116
+ label: readTextInput(form, "annotation-label") || current.label,
1117
+ targetObjectId: readOptionalTextInput(form, "annotation-target"),
1118
+ body: readOptionalTextInput(form, "annotation-body") ?? "",
1119
+ tags: splitTokens(readOptionalTextInput(form, "annotation-tags")),
1120
+ sourceObjectId: readOptionalTextInput(form, "annotation-source"),
1121
+ };
1122
+ nextDocument.system.annotations = nextDocument.system.annotations
1123
+ .filter((annotation) => annotation.id !== current.id)
1124
+ .concat(replacement)
1125
+ .sort((left, right) => left.id.localeCompare(right.id));
1126
+ if (current.id !== replacement.id) {
1127
+ selection = { kind: "annotation", id: replacement.id };
1128
+ }
1129
+ return nextDocument;
1130
+ }
1131
+ function buildObjectDocumentFromInspector(currentId) {
1132
+ const nextDocument = cloneAtlasDocument(atlasDocument);
1133
+ const form = inspector?.querySelector("form[data-editor-form='object']");
1134
+ const current = nextDocument.objects.find((object) => object.id === currentId);
1135
+ if (!form || !current) {
1136
+ return nextDocument;
1137
+ }
1138
+ const nextId = readTextInput(form, "object-id") || current.id;
1139
+ const replacement = {
1140
+ ...current,
1141
+ id: nextId,
1142
+ type: readTextInput(form, "object-type") || current.type,
1143
+ properties: { ...current.properties },
1144
+ info: { ...current.info },
1145
+ placement: buildPlacementFromForm(form, current),
1146
+ };
1147
+ for (const field of OBJECT_STRING_FIELDS) {
1148
+ setOptionalProperty(replacement.properties, field, readOptionalTextInput(form, `prop-${field}`));
1149
+ }
1150
+ for (const field of OBJECT_UNIT_FIELDS) {
1151
+ setOptionalProperty(replacement.properties, field, parseOptionalNormalizedValue(readOptionalTextInput(form, `prop-${field}`)));
1152
+ }
1153
+ for (const field of OBJECT_NUMBER_FIELDS) {
1154
+ setOptionalProperty(replacement.properties, field, parseNullableNumber(readOptionalTextInput(form, `prop-${field}`)));
1155
+ }
1156
+ setOptionalProperty(replacement.properties, "tags", splitTokens(readOptionalTextInput(form, "prop-tags")));
1157
+ replacement.properties.hidden = readCheckbox(form, "prop-hidden");
1158
+ const description = readOptionalTextInput(form, "info-description");
1159
+ if (description) {
1160
+ replacement.info.description = description;
1161
+ }
1162
+ else {
1163
+ delete replacement.info.description;
1164
+ }
1165
+ const updatedDocument = replaceObject(nextDocument, current.id, replacement);
1166
+ if (current.id !== replacement.id) {
1167
+ selection = { kind: "object", id: replacement.id };
1168
+ }
1169
+ return updatedDocument;
1170
+ }
1171
+ function renderStatusBar() {
1172
+ const errorCount = diagnostics.filter((entry) => entry.diagnostic.severity === "error").length;
1173
+ const warningCount = diagnostics.filter((entry) => entry.diagnostic.severity === "warning").length;
1174
+ const selectionLabel = selection ? describePath(selection) : "Nothing selected";
1175
+ statusBar.innerHTML = `
1176
+ <span class="wo-editor-status-pill${dirty ? " is-dirty" : " is-clean"}">${dirty ? "Unsaved changes" : "Saved"}</span>
1177
+ <span class="wo-editor-status-pill">Schema ${escapeHtml(atlasDocument.version)}</span>
1178
+ <span class="wo-editor-status-pill${errorCount > 0 ? " is-error" : warningCount > 0 ? " is-warning" : ""}">${errorCount} errors · ${warningCount} warnings</span>
1179
+ <span class="wo-editor-status-pill">${escapeHtml(selectionLabel)}</span>
1180
+ `;
1181
+ }
1182
+ function updateLiveRegion() {
1183
+ const errorCount = diagnostics.filter((entry) => entry.diagnostic.severity === "error").length;
1184
+ const warningCount = diagnostics.filter((entry) => entry.diagnostic.severity === "warning").length;
1185
+ liveRegion.textContent = `${dirty ? "Unsaved changes." : "Saved."} ${errorCount} errors, ${warningCount} warnings. ${selection ? describePath(selection) : "Nothing selected"}.`;
1186
+ }
1187
+ function schedulePreviewRender() {
1188
+ if (previewTimer !== null) {
1189
+ return;
1190
+ }
1191
+ previewTimer = window.setTimeout(() => {
1192
+ previewTimer = null;
1193
+ renderPreviewNow();
1194
+ }, PREVIEW_BATCH_DELAY_MS);
1195
+ }
1196
+ function renderPreviewNow() {
1197
+ if (!viewer) {
1198
+ return;
1199
+ }
1200
+ const nextSvg = viewer.exportSvg();
1201
+ if (previewVisual && nextSvg !== lastPreviewSvg) {
1202
+ previewVisual.innerHTML = nextSvg;
1203
+ lastPreviewSvg = nextSvg;
1204
+ }
1205
+ const nextMarkup = buildEmbedMarkup(getCurrentSourceForExport(), atlasDocument);
1206
+ if (previewMarkup && nextMarkup !== lastPreviewMarkup) {
1207
+ previewMarkup.textContent = nextMarkup;
1208
+ lastPreviewMarkup = nextMarkup;
1209
+ }
1210
+ }
1211
+ function shouldIgnoreShortcutEvent(event) {
1212
+ const target = event.target;
1213
+ if (!target) {
1214
+ return false;
1215
+ }
1216
+ const tagName = target.tagName.toLowerCase();
1217
+ return target.isContentEditable || tagName === "input" || tagName === "textarea" || tagName === "select";
1218
+ }
1219
+ function getSelectionDiagnostics(currentSelection, entries) {
1220
+ if (!currentSelection) {
1221
+ return [];
1222
+ }
1223
+ const currentKey = selectionKey(currentSelection);
1224
+ return entries.filter((entry) => {
1225
+ const entryKey = selectionKey(entry.path);
1226
+ if (entryKey && entryKey === currentKey) {
1227
+ return true;
1228
+ }
1229
+ return currentSelection.kind === "object" && entry.diagnostic.objectId === currentSelection.id;
1230
+ });
1231
+ }
1232
+ function getPlacementDiagnosticsForSelection(currentSelection, entries) {
1233
+ return getSelectionDiagnostics(currentSelection, entries).filter((entry) => {
1234
+ const field = entry.diagnostic.field ?? "";
1235
+ return (PLACEMENT_DIAGNOSTIC_FIELDS.has(field) ||
1236
+ entry.diagnostic.message.toLowerCase().includes("placement") ||
1237
+ entry.diagnostic.message.toLowerCase().includes("orbit") ||
1238
+ entry.diagnostic.message.toLowerCase().includes("surface") ||
1239
+ entry.diagnostic.message.toLowerCase().includes("lagrange") ||
1240
+ entry.diagnostic.message.toLowerCase().includes("anchor"));
1241
+ });
1242
+ }
1243
+ function renderInspectorDiagnosticSummary(currentSelection, entries) {
1244
+ const selectionDiagnostics = getSelectionDiagnostics(currentSelection, entries);
1245
+ if (selectionDiagnostics.length === 0) {
1246
+ return "";
1247
+ }
1248
+ return `<div class="wo-editor-inspector-summary">
1249
+ ${selectionDiagnostics
1250
+ .slice(0, 4)
1251
+ .map((entry) => `<article class="wo-editor-diagnostic wo-editor-diagnostic-${escapeHtml(entry.diagnostic.severity)}">
1252
+ <strong>${escapeHtml(entry.diagnostic.severity.toUpperCase())}</strong>
1253
+ <span>${escapeHtml(entry.diagnostic.field ?? describePath(currentSelection))}</span>
1254
+ <p>${escapeHtml(entry.diagnostic.message)}</p>
1255
+ </article>`)
1256
+ .join("")}
1257
+ </div>`;
1258
+ }
1259
+ function decorateInspectorDiagnostics(currentSelection, entries) {
1260
+ if (!inspector || !currentSelection) {
1261
+ return;
1262
+ }
1263
+ const selectionDiagnostics = getSelectionDiagnostics(currentSelection, entries);
1264
+ const fieldMap = new Map();
1265
+ for (const entry of selectionDiagnostics) {
1266
+ for (const inputName of mapDiagnosticFieldToInputNames(currentSelection, entry.diagnostic.field)) {
1267
+ const currentEntries = fieldMap.get(inputName) ?? [];
1268
+ currentEntries.push(entry);
1269
+ fieldMap.set(inputName, currentEntries);
1270
+ }
1271
+ }
1272
+ for (const [inputName, fieldDiagnostics] of fieldMap) {
1273
+ const input = inspector.querySelector(`[name="${CSS.escape(inputName)}"]`);
1274
+ const field = input?.closest(".wo-editor-field, .wo-editor-checkbox");
1275
+ if (!input || !field) {
1276
+ continue;
1277
+ }
1278
+ const hasError = fieldDiagnostics.some((entry) => entry.diagnostic.severity === "error");
1279
+ const hasWarning = fieldDiagnostics.some((entry) => entry.diagnostic.severity === "warning");
1280
+ field.classList.add(hasError ? "has-error" : "has-warning");
1281
+ input.setAttribute("aria-invalid", hasError ? "true" : "false");
1282
+ const note = document.createElement("div");
1283
+ note.className = `wo-editor-field-note${hasError ? " is-error" : hasWarning ? " is-warning" : ""}`;
1284
+ note.textContent = fieldDiagnostics[0]?.diagnostic.message ?? "";
1285
+ field.append(note);
1286
+ }
1287
+ }
1288
+ }
1289
+ function resolveInitialEditorState(options) {
1290
+ if (options.atlasDocument) {
1291
+ const atlasDocument = cloneAtlasDocument(options.atlasDocument);
1292
+ return {
1293
+ atlasDocument,
1294
+ source: formatDocument(atlasDocument, { schema: "2.0" }),
1295
+ diagnostics: collectDocumentDiagnostics(atlasDocument),
1296
+ };
1297
+ }
1298
+ if (options.source) {
1299
+ const loaded = loadWorldOrbitSourceWithDiagnostics(options.source);
1300
+ if (loaded.ok && loaded.value) {
1301
+ const atlasDocument = loaded.value.atlasDocument ?? upgradeDocumentToV2(loaded.value.document);
1302
+ return {
1303
+ atlasDocument,
1304
+ source: formatDocument(atlasDocument, { schema: "2.0" }),
1305
+ diagnostics: mergeDiagnostics(resolveAtlasDiagnostics(atlasDocument, loaded.diagnostics), collectDocumentDiagnostics(atlasDocument)),
1306
+ };
1307
+ }
1308
+ }
1309
+ const atlasDocument = createEmptyAtlasDocument("WorldOrbit");
1310
+ return {
1311
+ atlasDocument,
1312
+ source: formatDocument(atlasDocument, { schema: "2.0" }),
1313
+ diagnostics: collectDocumentDiagnostics(atlasDocument),
1314
+ };
1315
+ }
1316
+ function buildEditorMarkup() {
1317
+ return `<section class="wo-editor-shell">
1318
+ <div class="wo-editor-toolbar" data-editor-toolbar></div>
1319
+ <div class="wo-editor-status" data-editor-status role="status" aria-live="polite"></div>
1320
+ <div class="wo-editor-main">
1321
+ <aside class="wo-editor-sidebar" data-editor-pane="sidebar">
1322
+ <div class="wo-editor-panel">
1323
+ <h2>Atlas</h2>
1324
+ <div class="wo-editor-outline" data-editor-outline></div>
1325
+ </div>
1326
+ <div class="wo-editor-panel">
1327
+ <h2>Diagnostics</h2>
1328
+ <div class="wo-editor-diagnostics" data-editor-diagnostics></div>
1329
+ </div>
1330
+ </aside>
1331
+ <div class="wo-editor-stage-panel">
1332
+ <div class="wo-editor-stage-shell" data-editor-stage-shell>
1333
+ <div class="wo-editor-stage" data-editor-stage></div>
1334
+ <div class="wo-editor-overlay" data-editor-overlay></div>
1335
+ </div>
1336
+ <div class="wo-editor-preview" data-editor-pane="preview">
1337
+ <div class="wo-editor-panel">
1338
+ <h2>Static SVG</h2>
1339
+ <div class="wo-editor-preview-visual" data-editor-preview-visual></div>
1340
+ </div>
1341
+ <div class="wo-editor-panel">
1342
+ <h2>Embed Markup</h2>
1343
+ <pre class="wo-editor-preview-markup" data-editor-preview-markup></pre>
1344
+ </div>
1345
+ </div>
1346
+ </div>
1347
+ <aside class="wo-editor-panel wo-editor-inspector" data-editor-pane="inspector" data-editor-inspector></aside>
1348
+ </div>
1349
+ <div class="wo-editor-panel wo-editor-text-panel" data-editor-pane="text">
1350
+ <h2>Source</h2>
1351
+ <textarea
1352
+ class="wo-editor-source"
1353
+ data-editor-source
1354
+ spellcheck="false"
1355
+ aria-describedby="worldorbit-editor-source-diagnostics"
1356
+ ></textarea>
1357
+ <div
1358
+ class="wo-editor-source-diagnostics"
1359
+ id="worldorbit-editor-source-diagnostics"
1360
+ data-editor-source-diagnostics
1361
+ aria-live="polite"
1362
+ ></div>
1363
+ </div>
1364
+ <div class="wo-editor-live-region" data-editor-live aria-live="polite" aria-atomic="true"></div>
1365
+ </section>`;
1366
+ }
1367
+ function renderOutlineButton(path, label, activeKey, diagnosticBuckets) {
1368
+ const key = selectionKey(path);
1369
+ const bucket = key ? diagnosticBuckets.get(key) : null;
1370
+ const badge = bucket && (bucket.errors > 0 || bucket.warnings > 0)
1371
+ ? `<span class="wo-editor-outline-badge${bucket.errors > 0 ? " is-error" : " is-warning"}">${bucket.errors > 0 ? bucket.errors : bucket.warnings}</span>`
1372
+ : "";
1373
+ 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>`;
1374
+ }
1375
+ function renderSystemInspector(formState) {
1376
+ return `<form class="wo-editor-form" data-editor-form="system">
1377
+ <h2>System</h2>
1378
+ ${renderTextField("System ID", "system-id", formState.system?.id ?? "")}
1379
+ ${renderTextField("Title", "system-title", formState.system?.title ?? "")}
1380
+ </form>`;
1381
+ }
1382
+ function renderDefaultsInspector(formState) {
1383
+ const defaults = formState.system?.defaults;
1384
+ return `<form class="wo-editor-form" data-editor-form="defaults">
1385
+ <h2>Defaults</h2>
1386
+ ${renderSelectField("Projection", "defaults-view", [
1387
+ ["topdown", "Topdown"],
1388
+ ["isometric", "Isometric"],
1389
+ ], defaults?.view ?? "topdown")}
1390
+ ${renderTextField("Scale preset", "defaults-scale", defaults?.scale ?? "")}
1391
+ ${renderTextField("Units", "defaults-units", defaults?.units ?? "")}
1392
+ ${renderSelectField("Render preset", "defaults-preset", [
1393
+ ["", "Document default"],
1394
+ ["diagram", "Diagram"],
1395
+ ["presentation", "Presentation"],
1396
+ ["atlas-card", "Atlas Card"],
1397
+ ["markdown", "Markdown"],
1398
+ ], defaults?.preset ?? "")}
1399
+ ${renderTextField("Theme", "defaults-theme", defaults?.theme ?? "")}
1400
+ </form>`;
1401
+ }
1402
+ function renderMetadataInspector(formState, key) {
1403
+ const value = formState.system?.atlasMetadata[key] ?? "";
1404
+ return `<form class="wo-editor-form" data-editor-form="metadata">
1405
+ <h2>Metadata</h2>
1406
+ ${renderTextField("Key", "metadata-key", key)}
1407
+ ${renderTextAreaField("Value", "metadata-value", value)}
1408
+ </form>`;
1409
+ }
1410
+ function renderViewpointInspector(formState, id) {
1411
+ const viewpoint = formState.viewpoints.find((entry) => entry.id === id);
1412
+ if (!viewpoint) {
1413
+ return `<p class="wo-editor-empty">Viewpoint not found.</p>`;
1414
+ }
1415
+ return `<form class="wo-editor-form" data-editor-form="viewpoint">
1416
+ <h2>Viewpoint</h2>
1417
+ ${renderTextField("ID", "viewpoint-id", viewpoint.id)}
1418
+ ${renderTextField("Label", "viewpoint-label", viewpoint.label)}
1419
+ ${renderTextAreaField("Summary", "viewpoint-summary", viewpoint.summary)}
1420
+ ${renderTextField("Focus object", "viewpoint-focus", viewpoint.focusObjectId ?? "")}
1421
+ ${renderTextField("Selected object", "viewpoint-select", viewpoint.selectedObjectId ?? "")}
1422
+ ${renderSelectField("Projection", "viewpoint-projection", [
1423
+ ["topdown", "Topdown"],
1424
+ ["isometric", "Isometric"],
1425
+ ], viewpoint.projection)}
1426
+ ${renderSelectField("Preset", "viewpoint-preset", [
1427
+ ["", "Document default"],
1428
+ ["diagram", "Diagram"],
1429
+ ["presentation", "Presentation"],
1430
+ ["atlas-card", "Atlas Card"],
1431
+ ["markdown", "Markdown"],
1432
+ ], viewpoint.preset ?? "")}
1433
+ ${renderTextField("Zoom", "viewpoint-zoom", viewpoint.zoom === null ? "" : String(viewpoint.zoom))}
1434
+ ${renderTextField("Rotation", "viewpoint-rotation", String(viewpoint.rotationDeg))}
1435
+ <fieldset class="wo-editor-fieldset">
1436
+ <legend>Layers</legend>
1437
+ ${renderCheckboxField("Background", "layer-background", viewpoint.layers.background !== false)}
1438
+ ${renderCheckboxField("Guides", "layer-guides", viewpoint.layers.guides !== false)}
1439
+ ${renderCheckboxField("Orbits back", "layer-orbits-back", viewpoint.layers["orbits-back"] !== false)}
1440
+ ${renderCheckboxField("Orbits front", "layer-orbits-front", viewpoint.layers["orbits-front"] !== false)}
1441
+ ${renderCheckboxField("Objects", "layer-objects", viewpoint.layers.objects !== false)}
1442
+ ${renderCheckboxField("Labels", "layer-labels", viewpoint.layers.labels !== false)}
1443
+ ${renderCheckboxField("Metadata", "layer-metadata", viewpoint.layers.metadata !== false)}
1444
+ </fieldset>
1445
+ ${renderTextField("Filter query", "filter-query", viewpoint.filter?.query ?? "")}
1446
+ ${renderTextField("Filter object types", "filter-object-types", viewpoint.filter?.objectTypes.join(" ") ?? "")}
1447
+ ${renderTextField("Filter tags", "filter-tags", viewpoint.filter?.tags.join(" ") ?? "")}
1448
+ ${renderTextField("Filter groups", "filter-groups", viewpoint.filter?.groupIds.join(" ") ?? "")}
1449
+ </form>`;
1450
+ }
1451
+ function renderAnnotationInspector(formState, id) {
1452
+ const annotation = formState.system?.annotations.find((entry) => entry.id === id);
1453
+ if (!annotation) {
1454
+ return `<p class="wo-editor-empty">Annotation not found.</p>`;
1455
+ }
1456
+ return `<form class="wo-editor-form" data-editor-form="annotation">
1457
+ <h2>Annotation</h2>
1458
+ ${renderTextField("ID", "annotation-id", annotation.id)}
1459
+ ${renderTextField("Label", "annotation-label", annotation.label)}
1460
+ ${renderTextField("Target object", "annotation-target", annotation.targetObjectId ?? "")}
1461
+ ${renderTextField("Source object", "annotation-source", annotation.sourceObjectId ?? "")}
1462
+ ${renderTextAreaField("Body", "annotation-body", annotation.body)}
1463
+ ${renderTextField("Tags", "annotation-tags", annotation.tags.join(" "))}
1464
+ </form>`;
1465
+ }
1466
+ function renderObjectInspector(formState, id) {
1467
+ const object = formState.objects.find((entry) => entry.id === id);
1468
+ if (!object) {
1469
+ return `<p class="wo-editor-empty">Object not found.</p>`;
1470
+ }
1471
+ const placementMode = object.placement?.mode ?? "";
1472
+ const placementTarget = object.placement?.mode === "orbit" || object.placement?.mode === "surface" || object.placement?.mode === "at"
1473
+ ? object.placement.target
1474
+ : "";
1475
+ const freeValue = object.placement?.mode === "free"
1476
+ ? object.placement.distance
1477
+ ? formatUnitValue(object.placement.distance)
1478
+ : object.placement.descriptor ?? ""
1479
+ : "";
1480
+ return `<form class="wo-editor-form" data-editor-form="object">
1481
+ <h2>Object</h2>
1482
+ ${renderTextField("ID", "object-id", object.id)}
1483
+ ${renderSelectField("Type", "object-type", OBJECT_TYPES.map((type) => [type, humanizeIdentifier(type)]), object.type)}
1484
+ ${renderSelectField("Placement mode", "placement-mode", [
1485
+ ["", "None"],
1486
+ ["orbit", "Orbit"],
1487
+ ["at", "At"],
1488
+ ["surface", "Surface"],
1489
+ ["free", "Free"],
1490
+ ], placementMode)}
1491
+ ${renderTextField("Placement target", "placement-target", placementTarget)}
1492
+ ${renderTextField("Free value", "placement-free", freeValue)}
1493
+ ${renderTextField("Distance", "placement-distance", object.placement?.mode === "orbit" && object.placement.distance ? formatUnitValue(object.placement.distance) : "")}
1494
+ ${renderTextField("Semi-major", "placement-semiMajor", object.placement?.mode === "orbit" && object.placement.semiMajor ? formatUnitValue(object.placement.semiMajor) : "")}
1495
+ ${renderTextField("Eccentricity", "placement-eccentricity", object.placement?.mode === "orbit" && object.placement.eccentricity !== undefined ? String(object.placement.eccentricity) : "")}
1496
+ ${renderTextField("Period", "placement-period", object.placement?.mode === "orbit" && object.placement.period ? formatUnitValue(object.placement.period) : "")}
1497
+ ${renderTextField("Angle", "placement-angle", object.placement?.mode === "orbit" && object.placement.angle ? formatUnitValue(object.placement.angle) : "")}
1498
+ ${renderTextField("Inclination", "placement-inclination", object.placement?.mode === "orbit" && object.placement.inclination ? formatUnitValue(object.placement.inclination) : "")}
1499
+ ${renderTextField("Phase", "placement-phase", object.placement?.mode === "orbit" && object.placement.phase ? formatUnitValue(object.placement.phase) : "")}
1500
+ <fieldset class="wo-editor-fieldset">
1501
+ <legend>Properties</legend>
1502
+ ${renderTextField("Kind", "prop-kind", readStringProperty(object.properties.kind))}
1503
+ ${renderTextField("Class", "prop-class", readStringProperty(object.properties.class))}
1504
+ ${renderTextField("Culture", "prop-culture", readStringProperty(object.properties.culture))}
1505
+ ${renderTextField("Tags", "prop-tags", readTagsProperty(object.properties.tags))}
1506
+ ${renderTextField("Color", "prop-color", readStringProperty(object.properties.color))}
1507
+ ${renderTextField("Image", "prop-image", readStringProperty(object.properties.image))}
1508
+ ${renderCheckboxField("Hidden", "prop-hidden", object.properties.hidden === true)}
1509
+ ${renderTextField("Radius", "prop-radius", readUnitProperty(object.properties.radius))}
1510
+ ${renderTextField("Mass", "prop-mass", readUnitProperty(object.properties.mass))}
1511
+ ${renderTextField("Density", "prop-density", readUnitProperty(object.properties.density))}
1512
+ ${renderTextField("Gravity", "prop-gravity", readUnitProperty(object.properties.gravity))}
1513
+ ${renderTextField("Temperature", "prop-temperature", readUnitProperty(object.properties.temperature))}
1514
+ ${renderTextField("Albedo", "prop-albedo", readNumberProperty(object.properties.albedo))}
1515
+ ${renderTextField("Atmosphere", "prop-atmosphere", readStringProperty(object.properties.atmosphere))}
1516
+ ${renderTextField("Inner", "prop-inner", readUnitProperty(object.properties.inner))}
1517
+ ${renderTextField("Outer", "prop-outer", readUnitProperty(object.properties.outer))}
1518
+ ${renderTextField("On", "prop-on", readStringProperty(object.properties.on))}
1519
+ ${renderTextField("Source", "prop-source", readStringProperty(object.properties.source))}
1520
+ ${renderTextField("Cycle", "prop-cycle", readUnitProperty(object.properties.cycle))}
1521
+ </fieldset>
1522
+ ${renderTextAreaField("Description", "info-description", object.info.description ?? "")}
1523
+ </form>`;
1524
+ }
1525
+ function renderTextField(label, name, value) {
1526
+ return `<label class="wo-editor-field"><span>${escapeHtml(label)}</span><input name="${escapeHtml(name)}" value="${escapeAttribute(value)}" /></label>`;
1527
+ }
1528
+ function renderTextAreaField(label, name, value) {
1529
+ return `<label class="wo-editor-field"><span>${escapeHtml(label)}</span><textarea name="${escapeHtml(name)}">${escapeHtml(value)}</textarea></label>`;
1530
+ }
1531
+ function renderSelectField(label, name, options, selectedValue) {
1532
+ return `<label class="wo-editor-field"><span>${escapeHtml(label)}</span><select name="${escapeHtml(name)}">${options
1533
+ .map(([value, optionLabel]) => `<option value="${escapeHtml(value)}"${value === selectedValue ? " selected" : ""}>${escapeHtml(optionLabel)}</option>`)
1534
+ .join("")}</select></label>`;
1535
+ }
1536
+ function renderCheckboxField(label, name, checked) {
1537
+ return `<label class="wo-editor-checkbox"><input type="checkbox" name="${escapeHtml(name)}"${checked ? " checked" : ""} /><span>${escapeHtml(label)}</span></label>`;
1538
+ }
1539
+ function createHandleElement(kind, objectId, point, label) {
1540
+ const element = document.createElement("button");
1541
+ element.type = "button";
1542
+ element.className = "wo-editor-handle";
1543
+ element.dataset.handleKind = kind;
1544
+ element.dataset.objectId = objectId;
1545
+ element.style.left = `${point.x}px`;
1546
+ element.style.top = `${point.y}px`;
1547
+ element.textContent = label;
1548
+ return element;
1549
+ }
1550
+ function readTextInput(form, name) {
1551
+ return form.elements.namedItem(name)?.value.trim() ?? "";
1552
+ }
1553
+ function readOptionalTextInput(form, name) {
1554
+ const value = readTextInput(form, name);
1555
+ return value ? value : null;
1556
+ }
1557
+ function readCheckbox(form, name) {
1558
+ return form.elements.namedItem(name)?.checked ?? false;
1559
+ }
1560
+ function buildPlacementFromForm(form, current) {
1561
+ const mode = readTextInput(form, "placement-mode");
1562
+ const target = readOptionalTextInput(form, "placement-target");
1563
+ switch (mode) {
1564
+ case "orbit":
1565
+ return {
1566
+ mode,
1567
+ target: target ??
1568
+ (current.placement?.mode === "orbit"
1569
+ ? current.placement.target
1570
+ : current.id),
1571
+ distance: parseOptionalUnit(readOptionalTextInput(form, "placement-distance")),
1572
+ semiMajor: parseOptionalUnit(readOptionalTextInput(form, "placement-semiMajor")),
1573
+ eccentricity: parseNullableNumber(readOptionalTextInput(form, "placement-eccentricity")) ?? undefined,
1574
+ period: parseOptionalUnit(readOptionalTextInput(form, "placement-period")),
1575
+ angle: parseOptionalUnit(readOptionalTextInput(form, "placement-angle")),
1576
+ inclination: parseOptionalUnit(readOptionalTextInput(form, "placement-inclination")),
1577
+ phase: parseOptionalUnit(readOptionalTextInput(form, "placement-phase")),
1578
+ };
1579
+ case "at":
1580
+ return {
1581
+ mode,
1582
+ target: target ?? current.id,
1583
+ reference: parseAtReferenceString(target ?? current.id),
1584
+ };
1585
+ case "surface":
1586
+ return {
1587
+ mode,
1588
+ target: target ?? current.id,
1589
+ };
1590
+ case "free": {
1591
+ const freeValue = readOptionalTextInput(form, "placement-free");
1592
+ const distance = parseOptionalUnit(freeValue);
1593
+ return {
1594
+ mode,
1595
+ distance: distance ?? undefined,
1596
+ descriptor: distance ? undefined : freeValue ?? undefined,
1597
+ };
1598
+ }
1599
+ default:
1600
+ return null;
1601
+ }
1602
+ }
1603
+ function setOptionalProperty(properties, key, value) {
1604
+ const emptyArray = Array.isArray(value) && value.length === 0;
1605
+ if (value === null || value === undefined || emptyArray || value === "") {
1606
+ delete properties[key];
1607
+ return;
1608
+ }
1609
+ properties[key] = value;
1610
+ }
1611
+ function parseOptionalNormalizedValue(value) {
1612
+ if (!value) {
1613
+ return null;
1614
+ }
1615
+ const unit = parseOptionalUnit(value);
1616
+ return unit ?? value;
1617
+ }
1618
+ function parseOptionalUnit(value) {
1619
+ if (!value) {
1620
+ return undefined;
1621
+ }
1622
+ const match = value.match(/^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/);
1623
+ if (!match) {
1624
+ return undefined;
1625
+ }
1626
+ return {
1627
+ value: Number(match[1]),
1628
+ unit: match[2] ?? null,
1629
+ };
1630
+ }
1631
+ function parseNullableNumber(value) {
1632
+ if (!value) {
1633
+ return null;
1634
+ }
1635
+ const parsed = Number(value);
1636
+ return Number.isFinite(parsed) ? parsed : null;
1637
+ }
1638
+ function parseObjectTypes(value) {
1639
+ const tokens = splitTokens(value);
1640
+ return tokens.filter((token) => OBJECT_TYPES.includes(token));
1641
+ }
1642
+ function splitTokens(value) {
1643
+ return value
1644
+ ?.split(/[\s,]+/)
1645
+ .map((entry) => entry.trim())
1646
+ .filter(Boolean) ?? [];
1647
+ }
1648
+ function createNewObject(type, id, document) {
1649
+ const orbitTarget = document.objects.find((object) => object.type === "star")?.id ??
1650
+ document.objects[0]?.id ??
1651
+ id;
1652
+ return {
1653
+ type,
1654
+ id,
1655
+ properties: {},
1656
+ placement: type === "structure" || type === "phenomenon"
1657
+ ? {
1658
+ mode: "at",
1659
+ target: `${orbitTarget}:L4`,
1660
+ reference: parseAtReferenceString(`${orbitTarget}:L4`),
1661
+ }
1662
+ : {
1663
+ mode: "orbit",
1664
+ target: orbitTarget,
1665
+ distance: { value: 1, unit: "au" },
1666
+ },
1667
+ info: {},
1668
+ };
1669
+ }
1670
+ function insertObject(document, object) {
1671
+ const next = cloneAtlasDocument(document);
1672
+ next.objects = next.objects
1673
+ .filter((entry) => entry.id !== object.id)
1674
+ .concat(object)
1675
+ .sort(compareObjects);
1676
+ return next;
1677
+ }
1678
+ function replaceObject(document, currentId, object) {
1679
+ const next = cloneAtlasDocument(document);
1680
+ next.objects = next.objects
1681
+ .filter((entry) => entry.id !== currentId)
1682
+ .concat(object)
1683
+ .sort(compareObjects);
1684
+ if (currentId !== object.id) {
1685
+ renameObjectReferences(next, currentId, object.id);
1686
+ }
1687
+ return next;
1688
+ }
1689
+ function renameObjectReferences(document, fromId, toId) {
1690
+ for (const object of document.objects) {
1691
+ if (object.id === toId) {
1692
+ continue;
1693
+ }
1694
+ if (object.placement?.mode === "orbit" && object.placement.target === fromId) {
1695
+ object.placement.target = toId;
1696
+ }
1697
+ if (object.placement?.mode === "surface" && object.placement.target === fromId) {
1698
+ object.placement.target = toId;
1699
+ }
1700
+ if (object.placement?.mode === "at") {
1701
+ const reference = object.placement.reference;
1702
+ if (reference.kind === "anchor" && reference.objectId === fromId) {
1703
+ reference.objectId = toId;
1704
+ }
1705
+ if (reference.kind === "lagrange") {
1706
+ if (reference.primary === fromId) {
1707
+ reference.primary = toId;
1708
+ }
1709
+ if (reference.secondary === fromId) {
1710
+ reference.secondary = toId;
1711
+ }
1712
+ }
1713
+ object.placement.target = formatAtReference(reference);
1714
+ }
1715
+ }
1716
+ for (const viewpoint of document.system?.viewpoints ?? []) {
1717
+ if (viewpoint.focusObjectId === fromId) {
1718
+ viewpoint.focusObjectId = toId;
1719
+ }
1720
+ if (viewpoint.selectedObjectId === fromId) {
1721
+ viewpoint.selectedObjectId = toId;
1722
+ }
1723
+ }
1724
+ for (const annotation of document.system?.annotations ?? []) {
1725
+ if (annotation.targetObjectId === fromId) {
1726
+ annotation.targetObjectId = toId;
1727
+ }
1728
+ if (annotation.sourceObjectId === fromId) {
1729
+ annotation.sourceObjectId = toId;
1730
+ }
1731
+ }
1732
+ }
1733
+ function removeSelectedNode(document, selection) {
1734
+ const next = removeAtlasDocumentNode(document, selection);
1735
+ if (selection.kind !== "object" || !selection.id) {
1736
+ return next;
1737
+ }
1738
+ for (const object of next.objects) {
1739
+ if (object.placement?.mode === "orbit" && object.placement.target === selection.id) {
1740
+ object.placement = null;
1741
+ }
1742
+ if (object.placement?.mode === "surface" && object.placement.target === selection.id) {
1743
+ object.placement = null;
1744
+ }
1745
+ if (object.placement?.mode === "at") {
1746
+ const reference = object.placement.reference;
1747
+ const touchesSelection = (reference.kind === "anchor" && reference.objectId === selection.id) ||
1748
+ (reference.kind === "lagrange" &&
1749
+ (reference.primary === selection.id || reference.secondary === selection.id));
1750
+ if (touchesSelection) {
1751
+ object.placement = null;
1752
+ }
1753
+ }
1754
+ }
1755
+ for (const viewpoint of next.system?.viewpoints ?? []) {
1756
+ if (viewpoint.focusObjectId === selection.id) {
1757
+ viewpoint.focusObjectId = null;
1758
+ }
1759
+ if (viewpoint.selectedObjectId === selection.id) {
1760
+ viewpoint.selectedObjectId = null;
1761
+ }
1762
+ }
1763
+ for (const annotation of next.system?.annotations ?? []) {
1764
+ if (annotation.targetObjectId === selection.id) {
1765
+ annotation.targetObjectId = null;
1766
+ }
1767
+ if (annotation.sourceObjectId === selection.id) {
1768
+ annotation.sourceObjectId = null;
1769
+ }
1770
+ }
1771
+ return next;
1772
+ }
1773
+ function updateOrbitPhase(document, objectId, details, pointer) {
1774
+ const orbit = details.orbit;
1775
+ if (!orbit || details.object.placement?.mode !== "orbit") {
1776
+ return document;
1777
+ }
1778
+ const unrotated = rotatePoint(pointer, { x: orbit.cx, y: orbit.cy }, -orbit.rotationDeg);
1779
+ const rx = orbit.kind === "circle" ? orbit.radius ?? 1 : orbit.rx ?? 1;
1780
+ const ry = orbit.kind === "circle" ? orbit.radius ?? 1 : orbit.ry ?? 1;
1781
+ const radians = Math.atan2((unrotated.y - orbit.cy) / Math.max(ry, 1), (unrotated.x - orbit.cx) / Math.max(rx, 1));
1782
+ const phaseDeg = normalizeDegrees((radians * 180) / Math.PI);
1783
+ const next = cloneAtlasDocument(document);
1784
+ const object = next.objects.find((entry) => entry.id === objectId);
1785
+ if (!object || object.placement?.mode !== "orbit") {
1786
+ return document;
1787
+ }
1788
+ object.placement.phase = {
1789
+ value: roundNumber(phaseDeg, 2),
1790
+ unit: "deg",
1791
+ };
1792
+ return next;
1793
+ }
1794
+ function updateOrbitRadius(document, objectId, details, pointer) {
1795
+ const orbit = details.orbit;
1796
+ if (!orbit || details.object.placement?.mode !== "orbit") {
1797
+ return document;
1798
+ }
1799
+ const unrotated = rotatePoint(pointer, { x: orbit.cx, y: orbit.cy }, -orbit.rotationDeg);
1800
+ const currentRadius = orbit.kind === "circle" ? orbit.radius ?? 1 : orbit.rx ?? 1;
1801
+ const nextRadius = Math.max(Math.abs(unrotated.x - orbit.cx), 24);
1802
+ const ratio = nextRadius / Math.max(currentRadius, 1);
1803
+ const next = cloneAtlasDocument(document);
1804
+ const object = next.objects.find((entry) => entry.id === objectId);
1805
+ if (!object || object.placement?.mode !== "orbit") {
1806
+ return document;
1807
+ }
1808
+ const currentValue = object.placement.semiMajor ??
1809
+ object.placement.distance ?? {
1810
+ value: 1,
1811
+ unit: "au",
1812
+ };
1813
+ const scaled = {
1814
+ value: roundNumber(currentValue.value * ratio, 3),
1815
+ unit: currentValue.unit,
1816
+ };
1817
+ if (object.placement.semiMajor) {
1818
+ object.placement.semiMajor = scaled;
1819
+ }
1820
+ else {
1821
+ object.placement.distance = scaled;
1822
+ }
1823
+ return next;
1824
+ }
1825
+ function updateAtReference(document, objectId, scene, pointer) {
1826
+ const candidate = findNearestAtCandidate(scene, objectId, pointer);
1827
+ if (!candidate) {
1828
+ return document;
1829
+ }
1830
+ const next = cloneAtlasDocument(document);
1831
+ const object = next.objects.find((entry) => entry.id === objectId);
1832
+ if (!object || object.placement?.mode !== "at") {
1833
+ return document;
1834
+ }
1835
+ object.placement.reference = candidate.reference;
1836
+ object.placement.target = formatAtReference(candidate.reference);
1837
+ return next;
1838
+ }
1839
+ function updateSurfaceTarget(document, objectId, scene, pointer) {
1840
+ const target = findNearestSceneObject(scene, objectId, pointer, (entry) => SURFACE_TARGET_TYPES.has(entry.object.type));
1841
+ if (!target) {
1842
+ return document;
1843
+ }
1844
+ const next = cloneAtlasDocument(document);
1845
+ const object = next.objects.find((entry) => entry.id === objectId);
1846
+ if (!object || object.placement?.mode !== "surface") {
1847
+ return document;
1848
+ }
1849
+ object.placement.target = target.objectId;
1850
+ return next;
1851
+ }
1852
+ function updateFreeDistance(document, objectId, scene, details, pointer) {
1853
+ if (details.object.placement?.mode !== "free") {
1854
+ return document;
1855
+ }
1856
+ const railX = scene.width - scene.padding - 140;
1857
+ const offsetPx = Math.max(0, railX - pointer.x);
1858
+ const next = cloneAtlasDocument(document);
1859
+ const object = next.objects.find((entry) => entry.id === objectId);
1860
+ if (!object || object.placement?.mode !== "free") {
1861
+ return document;
1862
+ }
1863
+ const preferredUnit = normalizeFreeDistanceUnit(object.placement.distance?.unit ?? null);
1864
+ const metric = offsetPx / Math.max(FREE_DISTANCE_PIXEL_FACTOR * scene.scaleModel.freePlacementMultiplier, 1);
1865
+ if (metric < 0.01) {
1866
+ object.placement.distance = undefined;
1867
+ if (!object.placement.descriptor) {
1868
+ delete object.placement.descriptor;
1869
+ }
1870
+ return next;
1871
+ }
1872
+ object.placement.distance = distanceMetricToUnitValue(metric, preferredUnit);
1873
+ delete object.placement.descriptor;
1874
+ return next;
1875
+ }
1876
+ function findNearestSceneObject(scene, selectedObjectId, pointer, predicate = () => true) {
1877
+ let nearest = null;
1878
+ let nearestDistance = Number.POSITIVE_INFINITY;
1879
+ for (const entry of scene.objects) {
1880
+ if (entry.hidden || entry.objectId === selectedObjectId || !predicate(entry)) {
1881
+ continue;
1882
+ }
1883
+ const distance = Math.hypot(pointer.x - entry.x, pointer.y - entry.y);
1884
+ if (distance < nearestDistance) {
1885
+ nearest = entry;
1886
+ nearestDistance = distance;
1887
+ }
1888
+ }
1889
+ return nearestDistance <= 140 ? nearest : null;
1890
+ }
1891
+ function findNearestAtCandidate(scene, selectedObjectId, pointer) {
1892
+ let directObjectCandidate = null;
1893
+ for (const entry of scene.objects) {
1894
+ if (entry.hidden || entry.objectId === selectedObjectId) {
1895
+ continue;
1896
+ }
1897
+ const distance = Math.hypot(pointer.x - entry.x, pointer.y - entry.y);
1898
+ const snapRadius = Math.max(entry.visualRadius + 16, 28);
1899
+ if (distance <= snapRadius) {
1900
+ if (!directObjectCandidate || distance < directObjectCandidate.distance) {
1901
+ directObjectCandidate = {
1902
+ reference: parseAtReferenceString(entry.objectId),
1903
+ x: entry.x,
1904
+ y: entry.y,
1905
+ distance,
1906
+ };
1907
+ }
1908
+ }
1909
+ }
1910
+ if (directObjectCandidate) {
1911
+ return {
1912
+ reference: directObjectCandidate.reference,
1913
+ x: directObjectCandidate.x,
1914
+ y: directObjectCandidate.y,
1915
+ };
1916
+ }
1917
+ const candidates = [];
1918
+ for (const entry of scene.objects) {
1919
+ if (entry.hidden || entry.objectId === selectedObjectId) {
1920
+ continue;
1921
+ }
1922
+ candidates.push({
1923
+ reference: parseAtReferenceString(entry.objectId),
1924
+ x: entry.x,
1925
+ y: entry.y,
1926
+ });
1927
+ }
1928
+ for (const orbit of scene.orbitVisuals) {
1929
+ if (orbit.hidden || orbit.objectId === selectedObjectId) {
1930
+ continue;
1931
+ }
1932
+ const parent = scene.objects.find((entry) => entry.objectId === orbit.parentId && !entry.hidden);
1933
+ const secondary = scene.objects.find((entry) => entry.objectId === orbit.objectId && !entry.hidden);
1934
+ if (!parent || !secondary) {
1935
+ continue;
1936
+ }
1937
+ for (const point of ["L1", "L2", "L3", "L4", "L5"]) {
1938
+ const position = computeLagrangeCandidatePosition(parent, secondary, point);
1939
+ candidates.push({
1940
+ reference: parseAtReferenceString(`${orbit.objectId}:${point}`),
1941
+ x: position.x,
1942
+ y: position.y,
1943
+ });
1944
+ }
1945
+ }
1946
+ let nearest = null;
1947
+ let nearestDistance = Number.POSITIVE_INFINITY;
1948
+ for (const candidate of candidates) {
1949
+ const distance = Math.hypot(pointer.x - candidate.x, pointer.y - candidate.y);
1950
+ if (distance < nearestDistance) {
1951
+ nearest = candidate;
1952
+ nearestDistance = distance;
1953
+ }
1954
+ }
1955
+ return nearestDistance <= 140 ? nearest : null;
1956
+ }
1957
+ function computeLagrangeCandidatePosition(primary, secondary, point) {
1958
+ const dx = secondary.x - primary.x;
1959
+ const dy = secondary.y - primary.y;
1960
+ const distance = Math.hypot(dx, dy) || 1;
1961
+ const ux = dx / distance;
1962
+ const uy = dy / distance;
1963
+ const nx = -uy;
1964
+ const ny = ux;
1965
+ const offset = clampNumber(distance * 0.25, 24, 68);
1966
+ switch (point) {
1967
+ case "L1":
1968
+ return {
1969
+ x: secondary.x - ux * offset,
1970
+ y: secondary.y - uy * offset,
1971
+ };
1972
+ case "L2":
1973
+ return {
1974
+ x: secondary.x + ux * offset,
1975
+ y: secondary.y + uy * offset,
1976
+ };
1977
+ case "L3":
1978
+ return {
1979
+ x: primary.x - ux * offset,
1980
+ y: primary.y - uy * offset,
1981
+ };
1982
+ case "L4":
1983
+ return {
1984
+ x: secondary.x + (ux * 0.5 - nx * 0.8660254) * offset,
1985
+ y: secondary.y + (uy * 0.5 - ny * 0.8660254) * offset,
1986
+ };
1987
+ case "L5":
1988
+ return {
1989
+ x: secondary.x + (ux * 0.5 + nx * 0.8660254) * offset,
1990
+ y: secondary.y + (uy * 0.5 + ny * 0.8660254) * offset,
1991
+ };
1992
+ }
1993
+ }
1994
+ function parseAtReferenceString(target) {
1995
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
1996
+ if (pairedMatch) {
1997
+ return {
1998
+ kind: "lagrange",
1999
+ primary: pairedMatch[1],
2000
+ secondary: pairedMatch[2],
2001
+ point: pairedMatch[3],
2002
+ };
2003
+ }
2004
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
2005
+ if (simpleMatch) {
2006
+ return {
2007
+ kind: "lagrange",
2008
+ primary: simpleMatch[1],
2009
+ secondary: null,
2010
+ point: simpleMatch[2],
2011
+ };
2012
+ }
2013
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
2014
+ if (anchorMatch) {
2015
+ return {
2016
+ kind: "anchor",
2017
+ objectId: anchorMatch[1],
2018
+ anchor: anchorMatch[2],
2019
+ };
2020
+ }
2021
+ return {
2022
+ kind: "named",
2023
+ name: target,
2024
+ };
2025
+ }
2026
+ function formatAtReference(reference) {
2027
+ switch (reference.kind) {
2028
+ case "lagrange":
2029
+ return reference.secondary
2030
+ ? `${reference.primary}-${reference.secondary}:${reference.point}`
2031
+ : `${reference.primary}:${reference.point}`;
2032
+ case "anchor":
2033
+ return `${reference.objectId}:${reference.anchor}`;
2034
+ case "named":
2035
+ return reference.name;
2036
+ }
2037
+ }
2038
+ function collectDocumentDiagnostics(document) {
2039
+ return validateAtlasDocumentWithDiagnostics(document);
2040
+ }
2041
+ function mergeDiagnostics(primary, secondary) {
2042
+ const seen = new Set();
2043
+ const merged = [];
2044
+ for (const entry of [...primary, ...secondary]) {
2045
+ const key = `${entry.diagnostic.code}:${entry.diagnostic.message}:${selectionKey(entry.path)}`;
2046
+ if (seen.has(key)) {
2047
+ continue;
2048
+ }
2049
+ seen.add(key);
2050
+ merged.push(cloneResolvedDiagnostic(entry));
2051
+ }
2052
+ return merged;
2053
+ }
2054
+ function buildDiagnosticBuckets(entries) {
2055
+ const buckets = new Map();
2056
+ for (const entry of entries) {
2057
+ const key = selectionKey(entry.path) ??
2058
+ (entry.diagnostic.objectId ? selectionKey({ kind: "object", id: entry.diagnostic.objectId }) : null);
2059
+ if (!key) {
2060
+ continue;
2061
+ }
2062
+ const bucket = buckets.get(key) ?? { errors: 0, warnings: 0 };
2063
+ if (entry.diagnostic.severity === "error") {
2064
+ bucket.errors += 1;
2065
+ }
2066
+ else if (entry.diagnostic.severity === "warning") {
2067
+ bucket.warnings += 1;
2068
+ }
2069
+ buckets.set(key, bucket);
2070
+ }
2071
+ return buckets;
2072
+ }
2073
+ function formatDiagnosticLocation(entry) {
2074
+ if (entry.diagnostic.line !== undefined && entry.diagnostic.column !== undefined) {
2075
+ return `Line ${entry.diagnostic.line}:${entry.diagnostic.column}`;
2076
+ }
2077
+ if (entry.diagnostic.line !== undefined) {
2078
+ return `Line ${entry.diagnostic.line}`;
2079
+ }
2080
+ return null;
2081
+ }
2082
+ function mapDiagnosticFieldToInputNames(selection, field) {
2083
+ if (!field) {
2084
+ return [];
2085
+ }
2086
+ switch (selection.kind) {
2087
+ case "system":
2088
+ if (field === "id") {
2089
+ return ["system-id"];
2090
+ }
2091
+ if (field === "title") {
2092
+ return ["system-title"];
2093
+ }
2094
+ return [];
2095
+ case "defaults":
2096
+ switch (field) {
2097
+ case "view":
2098
+ return ["defaults-view"];
2099
+ case "scale":
2100
+ return ["defaults-scale"];
2101
+ case "units":
2102
+ return ["defaults-units"];
2103
+ case "preset":
2104
+ return ["defaults-preset"];
2105
+ case "theme":
2106
+ return ["defaults-theme"];
2107
+ default:
2108
+ return [];
2109
+ }
2110
+ case "metadata":
2111
+ return field === "key" ? ["metadata-key"] : ["metadata-value"];
2112
+ case "viewpoint":
2113
+ switch (field) {
2114
+ case "id":
2115
+ return ["viewpoint-id"];
2116
+ case "label":
2117
+ return ["viewpoint-label"];
2118
+ case "summary":
2119
+ return ["viewpoint-summary"];
2120
+ case "focusObjectId":
2121
+ return ["viewpoint-focus"];
2122
+ case "selectedObjectId":
2123
+ return ["viewpoint-select"];
2124
+ case "projection":
2125
+ return ["viewpoint-projection"];
2126
+ case "preset":
2127
+ return ["viewpoint-preset"];
2128
+ case "zoom":
2129
+ return ["viewpoint-zoom"];
2130
+ case "rotationDeg":
2131
+ return ["viewpoint-rotation"];
2132
+ default:
2133
+ return [];
2134
+ }
2135
+ case "annotation":
2136
+ switch (field) {
2137
+ case "id":
2138
+ return ["annotation-id"];
2139
+ case "label":
2140
+ return ["annotation-label"];
2141
+ case "targetObjectId":
2142
+ return ["annotation-target"];
2143
+ case "sourceObjectId":
2144
+ return ["annotation-source"];
2145
+ case "body":
2146
+ return ["annotation-body"];
2147
+ case "tags":
2148
+ return ["annotation-tags"];
2149
+ default:
2150
+ return [];
2151
+ }
2152
+ case "object":
2153
+ if (field === "id") {
2154
+ return ["object-id"];
2155
+ }
2156
+ if (field === "type") {
2157
+ return ["object-type"];
2158
+ }
2159
+ if (field === "placement") {
2160
+ return ["placement-mode"];
2161
+ }
2162
+ if (field === "description") {
2163
+ return ["info-description"];
2164
+ }
2165
+ if (field === "reference") {
2166
+ return ["placement-target"];
2167
+ }
2168
+ if (field === "descriptor") {
2169
+ return ["placement-free"];
2170
+ }
2171
+ if (field === "target") {
2172
+ return ["placement-target"];
2173
+ }
2174
+ if (PLACEMENT_DIAGNOSTIC_FIELDS.has(field)) {
2175
+ return [`placement-${field}`];
2176
+ }
2177
+ return [`prop-${field}`];
2178
+ }
2179
+ }
2180
+ function buildEmbedMarkup(source, document) {
2181
+ return renderWorldOrbitBlock(source, {
2182
+ mode: "interactive",
2183
+ preset: document.system?.defaults.preset ?? "atlas-card",
2184
+ projection: document.system?.defaults.view ?? "topdown",
2185
+ });
2186
+ }
2187
+ function describePath(path) {
2188
+ switch (path.kind) {
2189
+ case "system":
2190
+ return "System";
2191
+ case "defaults":
2192
+ return "Defaults";
2193
+ case "metadata":
2194
+ return `Metadata: ${path.key ?? ""}`;
2195
+ case "object":
2196
+ return `Object: ${path.id ?? ""}`;
2197
+ case "viewpoint":
2198
+ return `Viewpoint: ${path.id ?? ""}`;
2199
+ case "annotation":
2200
+ return `Annotation: ${path.id ?? ""}`;
2201
+ }
2202
+ }
2203
+ function selectionKey(path) {
2204
+ return path ? `${path.kind}:${path.id ?? path.key ?? ""}` : null;
2205
+ }
2206
+ function compareObjects(left, right) {
2207
+ return left.id.localeCompare(right.id);
2208
+ }
2209
+ function createUniqueId(prefix, existing) {
2210
+ const safePrefix = prefix.trim() || "item";
2211
+ let counter = 1;
2212
+ let candidate = safePrefix;
2213
+ while (existing.includes(candidate)) {
2214
+ counter += 1;
2215
+ candidate = `${safePrefix}-${counter}`;
2216
+ }
2217
+ return candidate;
2218
+ }
2219
+ function humanizeIdentifier(value) {
2220
+ return value
2221
+ .split(/[-_]+/)
2222
+ .filter(Boolean)
2223
+ .map((segment) => segment[0].toUpperCase() + segment.slice(1))
2224
+ .join(" ");
2225
+ }
2226
+ function cloneResolvedDiagnostic(diagnostic) {
2227
+ return {
2228
+ diagnostic: { ...diagnostic.diagnostic },
2229
+ path: diagnostic.path ? { ...diagnostic.path } : null,
2230
+ };
2231
+ }
2232
+ function readStringProperty(value) {
2233
+ return typeof value === "string" ? value : "";
2234
+ }
2235
+ function readTagsProperty(value) {
2236
+ return Array.isArray(value) ? value.join(" ") : "";
2237
+ }
2238
+ function readUnitProperty(value) {
2239
+ return value && typeof value === "object" && "value" in value
2240
+ ? formatUnitValue(value)
2241
+ : "";
2242
+ }
2243
+ function readNumberProperty(value) {
2244
+ return typeof value === "number" ? String(value) : "";
2245
+ }
2246
+ function formatUnitValue(value) {
2247
+ return `${value.value}${value.unit ?? ""}`;
2248
+ }
2249
+ function roundNumber(value, decimals) {
2250
+ const factor = 10 ** decimals;
2251
+ return Math.round(value * factor) / factor;
2252
+ }
2253
+ function clampNumber(value, min, max) {
2254
+ return Math.min(Math.max(value, min), max);
2255
+ }
2256
+ function normalizeDegrees(value) {
2257
+ let normalized = value % 360;
2258
+ if (normalized < 0) {
2259
+ normalized += 360;
2260
+ }
2261
+ return normalized;
2262
+ }
2263
+ function normalizeFreeDistanceUnit(unit) {
2264
+ switch (unit) {
2265
+ case "au":
2266
+ case "km":
2267
+ case "re":
2268
+ case "sol":
2269
+ case null:
2270
+ return unit;
2271
+ default:
2272
+ return null;
2273
+ }
2274
+ }
2275
+ function distanceMetricToUnitValue(metric, unit) {
2276
+ switch (unit) {
2277
+ case "km":
2278
+ return { value: roundNumber(metric * AU_IN_KM, 0), unit };
2279
+ case "re":
2280
+ return { value: roundNumber((metric * AU_IN_KM) / EARTH_RADIUS_IN_KM, 3), unit };
2281
+ case "sol":
2282
+ return { value: roundNumber((metric * AU_IN_KM) / SOLAR_RADIUS_IN_KM, 4), unit };
2283
+ case "au":
2284
+ return { value: roundNumber(metric, 3), unit };
2285
+ default:
2286
+ return { value: roundNumber(metric, 2), unit: null };
2287
+ }
2288
+ }
2289
+ function escapeHtml(value) {
2290
+ return value
2291
+ .replaceAll("&", "&amp;")
2292
+ .replaceAll("<", "&lt;")
2293
+ .replaceAll(">", "&gt;")
2294
+ .replaceAll('"', "&quot;");
2295
+ }
2296
+ function escapeAttribute(value) {
2297
+ return escapeHtml(value);
2298
+ }
2299
+ function ensureBrowserEnvironment(container) {
2300
+ if (typeof window === "undefined" || typeof document === "undefined") {
2301
+ throw new Error("createWorldOrbitEditor can only run in a browser environment.");
2302
+ }
2303
+ if (!(container instanceof HTMLElement)) {
2304
+ throw new Error("WorldOrbit editor requires an HTMLElement container.");
2305
+ }
2306
+ }
2307
+ function queryRequired(container, selector) {
2308
+ const found = container.querySelector(selector);
2309
+ if (!found) {
2310
+ throw new Error(`WorldOrbit editor failed to initialize selector "${selector}".`);
2311
+ }
2312
+ return found;
2313
+ }
2314
+ function installEditorStyles() {
2315
+ if (document.getElementById(STYLE_ID)) {
2316
+ return;
2317
+ }
2318
+ const style = document.createElement("style");
2319
+ style.id = STYLE_ID;
2320
+ style.textContent = `
2321
+ .wo-editor {
2322
+ --wo-editor-sidebar-width: 280px;
2323
+ --wo-editor-inspector-width: 360px;
2324
+ --wo-editor-source-height: 280px;
2325
+ }
2326
+ .wo-editor-shell { display: grid; gap: 16px; min-width: 0; }
2327
+ .wo-editor-toolbar { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; min-width: 0; }
2328
+ .wo-editor-toolbar-group { display: flex; gap: 10px; flex-wrap: wrap; min-width: 0; }
2329
+ .wo-editor-toolbar button, .wo-editor-toolbar select {
2330
+ border: 1px solid rgba(240, 180, 100, 0.2);
2331
+ border-radius: 999px;
2332
+ background: rgba(240, 180, 100, 0.08);
2333
+ color: #edf6ff;
2334
+ font: 600 13px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
2335
+ padding: 10px 14px;
2336
+ }
2337
+ .wo-editor button:focus-visible,
2338
+ .wo-editor select:focus-visible,
2339
+ .wo-editor input:focus-visible,
2340
+ .wo-editor textarea:focus-visible {
2341
+ outline: 2px solid rgba(255, 214, 138, 0.95);
2342
+ outline-offset: 2px;
2343
+ }
2344
+ .wo-editor-toolbar button:disabled { opacity: 0.45; cursor: not-allowed; }
2345
+ .wo-editor-status {
2346
+ display: flex;
2347
+ flex-wrap: wrap;
2348
+ gap: 10px;
2349
+ min-width: 0;
2350
+ }
2351
+ .wo-editor-status-pill {
2352
+ border: 1px solid rgba(255,255,255,0.08);
2353
+ border-radius: 999px;
2354
+ background: rgba(255,255,255,0.04);
2355
+ color: #edf6ff;
2356
+ font: 600 12px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
2357
+ padding: 8px 12px;
2358
+ }
2359
+ .wo-editor-status-pill.is-clean { border-color: rgba(120, 255, 195, 0.22); color: #c7ffe5; }
2360
+ .wo-editor-status-pill.is-dirty { border-color: rgba(240, 180, 100, 0.28); color: #ffd58a; }
2361
+ .wo-editor-status-pill.is-warning { border-color: rgba(240, 180, 100, 0.28); color: #ffd58a; }
2362
+ .wo-editor-status-pill.is-error { border-color: rgba(255, 120, 120, 0.3); color: #ffb2b2; }
2363
+ .wo-editor-main {
2364
+ display: grid;
2365
+ grid-template-columns:
2366
+ minmax(220px, var(--wo-editor-sidebar-width))
2367
+ minmax(0, 1fr)
2368
+ minmax(280px, var(--wo-editor-inspector-width));
2369
+ gap: 16px;
2370
+ min-width: 0;
2371
+ align-items: start;
2372
+ }
2373
+ .wo-editor-sidebar, .wo-editor-stage-panel, .wo-editor-preview { display: grid; gap: 16px; min-width: 0; }
2374
+ .wo-editor-panel {
2375
+ border-radius: 24px;
2376
+ border: 1px solid rgba(255,255,255,0.08);
2377
+ background: rgba(7, 16, 25, 0.72);
2378
+ padding: 18px;
2379
+ min-width: 0;
2380
+ }
2381
+ .wo-editor-panel h2 {
2382
+ margin: 0 0 14px;
2383
+ color: #edf6ff;
2384
+ font: 700 14px/1.2 "Segoe UI Variable Display", "Segoe UI", sans-serif;
2385
+ text-transform: uppercase;
2386
+ letter-spacing: 0.08em;
2387
+ }
2388
+ .wo-editor[data-wo-show-inspector="false"] [data-editor-pane="inspector"] { display: none; }
2389
+ .wo-editor[data-wo-show-text-pane="false"] [data-editor-pane="text"] { display: none; }
2390
+ .wo-editor[data-wo-show-preview="false"] [data-editor-pane="preview"] { display: none; }
2391
+ .wo-editor-stage-shell {
2392
+ position: relative;
2393
+ min-width: 0;
2394
+ border-radius: 26px;
2395
+ overflow: hidden;
2396
+ border: 1px solid rgba(255,255,255,0.08);
2397
+ background: rgba(8, 17, 28, 0.9);
2398
+ }
2399
+ .wo-editor-stage { min-height: 620px; min-width: 0; }
2400
+ .wo-editor-overlay { position: absolute; inset: 0; pointer-events: none; }
2401
+ .wo-editor-overlay > * { pointer-events: auto; }
2402
+ .wo-editor-handle {
2403
+ position: absolute;
2404
+ transform: translate(-50%, -50%);
2405
+ border: 0;
2406
+ border-radius: 999px;
2407
+ background: rgba(255, 214, 138, 0.92);
2408
+ color: #071019;
2409
+ cursor: grab;
2410
+ font: 700 11px/1 "Segoe UI Variable", "Segoe UI", sans-serif;
2411
+ padding: 8px 10px;
2412
+ box-shadow: 0 10px 24px rgba(0,0,0,0.24);
2413
+ }
2414
+ .wo-editor-hint {
2415
+ position: absolute;
2416
+ transform: translate(-50%, -50%);
2417
+ padding: 4px 8px;
2418
+ border-radius: 999px;
2419
+ background: rgba(18, 39, 58, 0.84);
2420
+ color: #edf6ff;
2421
+ font: 600 11px/1.3 "Segoe UI Variable", "Segoe UI", sans-serif;
2422
+ white-space: nowrap;
2423
+ box-shadow: 0 6px 18px rgba(0,0,0,0.22);
2424
+ }
2425
+ .wo-editor-hint.is-subtle { background: rgba(18, 39, 58, 0.64); color: rgba(237, 246, 255, 0.78); }
2426
+ .wo-editor-hint-note { left: 16px; top: 16px; transform: none; }
2427
+ .wo-editor-overlay-diagnostics {
2428
+ position: absolute;
2429
+ right: 16px;
2430
+ top: 16px;
2431
+ display: grid;
2432
+ gap: 8px;
2433
+ max-width: min(320px, calc(100% - 32px));
2434
+ }
2435
+ .wo-editor-overlay-diagnostic {
2436
+ border-radius: 14px;
2437
+ padding: 10px 12px;
2438
+ background: rgba(10, 20, 32, 0.92);
2439
+ box-shadow: 0 12px 30px rgba(0,0,0,0.28);
2440
+ }
2441
+ .wo-editor-overlay-diagnostic strong {
2442
+ display: block;
2443
+ margin-bottom: 4px;
2444
+ font: 700 11px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
2445
+ letter-spacing: 0.08em;
2446
+ text-transform: uppercase;
2447
+ }
2448
+ .wo-editor-overlay-diagnostic p {
2449
+ margin: 0;
2450
+ color: #edf6ff;
2451
+ font: 500 12px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
2452
+ }
2453
+ .wo-editor-overlay-diagnostic-error { border: 1px solid rgba(255, 120, 120, 0.28); }
2454
+ .wo-editor-overlay-diagnostic-warning { border: 1px solid rgba(240, 180, 100, 0.24); }
2455
+ .wo-editor-outline { display: grid; gap: 14px; }
2456
+ .wo-editor-outline-section { display: grid; gap: 8px; }
2457
+ .wo-editor-outline-section h3 {
2458
+ margin: 0;
2459
+ color: rgba(237,246,255,0.68);
2460
+ font: 600 11px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
2461
+ text-transform: uppercase;
2462
+ letter-spacing: 0.08em;
2463
+ }
2464
+ .wo-editor-outline-item {
2465
+ border: 1px solid transparent;
2466
+ border-radius: 16px;
2467
+ background: rgba(255,255,255,0.04);
2468
+ color: #edf6ff;
2469
+ cursor: pointer;
2470
+ font: 500 13px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
2471
+ padding: 10px 12px;
2472
+ text-align: left;
2473
+ display: flex;
2474
+ align-items: center;
2475
+ justify-content: space-between;
2476
+ gap: 10px;
2477
+ }
2478
+ .wo-editor-outline-item.is-active {
2479
+ border-color: rgba(255, 214, 138, 0.34);
2480
+ background: rgba(255, 214, 138, 0.12);
2481
+ color: #ffdda9;
2482
+ }
2483
+ .wo-editor-outline-badge {
2484
+ min-width: 22px;
2485
+ border-radius: 999px;
2486
+ background: rgba(240, 180, 100, 0.16);
2487
+ color: #ffd58a;
2488
+ font: 700 11px/1 "Segoe UI Variable", "Segoe UI", sans-serif;
2489
+ padding: 5px 7px;
2490
+ text-align: center;
2491
+ }
2492
+ .wo-editor-outline-badge.is-error {
2493
+ background: rgba(255, 120, 120, 0.18);
2494
+ color: #ffb2b2;
2495
+ }
2496
+ .wo-editor-diagnostics { display: grid; gap: 10px; }
2497
+ .wo-editor-diagnostic {
2498
+ display: grid;
2499
+ gap: 4px;
2500
+ border-radius: 16px;
2501
+ padding: 12px;
2502
+ background: rgba(255,255,255,0.04);
2503
+ color: #edf6ff;
2504
+ font: 500 12px/1.45 "Segoe UI Variable", "Segoe UI", sans-serif;
2505
+ }
2506
+ .wo-editor-diagnostic strong { font-size: 11px; letter-spacing: 0.08em; }
2507
+ .wo-editor-diagnostic p { margin: 0; color: rgba(237,246,255,0.82); }
2508
+ .wo-editor-diagnostic-warning { border: 1px solid rgba(240, 180, 100, 0.22); }
2509
+ .wo-editor-diagnostic-error { border: 1px solid rgba(255, 120, 120, 0.24); }
2510
+ .wo-editor-inspector-summary {
2511
+ display: grid;
2512
+ gap: 10px;
2513
+ margin-bottom: 14px;
2514
+ }
2515
+ .wo-editor-form { display: grid; gap: 12px; min-width: 0; }
2516
+ .wo-editor-field { display: grid; gap: 6px; }
2517
+ .wo-editor-field span, .wo-editor-fieldset legend {
2518
+ color: rgba(237,246,255,0.72);
2519
+ font: 600 11px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
2520
+ text-transform: uppercase;
2521
+ letter-spacing: 0.08em;
2522
+ }
2523
+ .wo-editor-field input, .wo-editor-field select, .wo-editor-field textarea, .wo-editor-source {
2524
+ width: 100%;
2525
+ min-width: 0;
2526
+ border: 1px solid rgba(255,255,255,0.08);
2527
+ border-radius: 14px;
2528
+ background: rgba(12, 26, 39, 0.84);
2529
+ color: #edf6ff;
2530
+ font: 500 13px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
2531
+ padding: 10px 12px;
2532
+ box-sizing: border-box;
2533
+ }
2534
+ .wo-editor-field.has-error input,
2535
+ .wo-editor-field.has-error select,
2536
+ .wo-editor-field.has-error textarea,
2537
+ .wo-editor-checkbox.has-error {
2538
+ border-color: rgba(255, 120, 120, 0.44);
2539
+ }
2540
+ .wo-editor-field.has-warning input,
2541
+ .wo-editor-field.has-warning select,
2542
+ .wo-editor-field.has-warning textarea,
2543
+ .wo-editor-checkbox.has-warning {
2544
+ border-color: rgba(240, 180, 100, 0.42);
2545
+ }
2546
+ .wo-editor-field textarea, .wo-editor-source { min-height: 110px; resize: vertical; }
2547
+ .wo-editor-source {
2548
+ min-height: var(--wo-editor-source-height);
2549
+ font-family: "Cascadia Code", "Consolas", monospace;
2550
+ white-space: pre;
2551
+ overflow: auto;
2552
+ }
2553
+ .wo-editor-field-note {
2554
+ color: rgba(237,246,255,0.72);
2555
+ font: 500 12px/1.45 "Segoe UI Variable", "Segoe UI", sans-serif;
2556
+ }
2557
+ .wo-editor-field-note.is-error { color: #ffb2b2; }
2558
+ .wo-editor-field-note.is-warning { color: #ffd58a; }
2559
+ .wo-editor-fieldset {
2560
+ display: grid;
2561
+ gap: 10px;
2562
+ border: 1px solid rgba(255,255,255,0.08);
2563
+ border-radius: 18px;
2564
+ padding: 14px;
2565
+ min-width: 0;
2566
+ }
2567
+ .wo-editor-checkbox {
2568
+ display: flex;
2569
+ gap: 10px;
2570
+ align-items: center;
2571
+ color: #edf6ff;
2572
+ font: 500 13px/1.4 "Segoe UI Variable", "Segoe UI", sans-serif;
2573
+ }
2574
+ .wo-editor-preview { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2575
+ .wo-editor-preview-visual {
2576
+ min-height: 240px;
2577
+ overflow: auto;
2578
+ }
2579
+ .wo-editor-preview-markup {
2580
+ margin: 0;
2581
+ min-height: 240px;
2582
+ white-space: pre-wrap;
2583
+ overflow-wrap: anywhere;
2584
+ color: #edf6ff;
2585
+ font: 12px/1.5 "Cascadia Code", "Consolas", monospace;
2586
+ }
2587
+ .wo-editor-source-diagnostics {
2588
+ display: grid;
2589
+ gap: 10px;
2590
+ margin-top: 14px;
2591
+ }
2592
+ .wo-editor-empty {
2593
+ margin: 0;
2594
+ color: rgba(237,246,255,0.68);
2595
+ font: 500 12px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
2596
+ }
2597
+ .wo-editor-live-region {
2598
+ position: absolute;
2599
+ width: 1px;
2600
+ height: 1px;
2601
+ padding: 0;
2602
+ margin: -1px;
2603
+ overflow: hidden;
2604
+ clip: rect(0, 0, 0, 0);
2605
+ white-space: nowrap;
2606
+ border: 0;
2607
+ }
2608
+ @media (max-width: 1280px) {
2609
+ .wo-editor-main { grid-template-columns: minmax(220px, 260px) minmax(0, 1fr); }
2610
+ .wo-editor-inspector { grid-column: 1 / -1; }
2611
+ .wo-editor-preview { grid-template-columns: 1fr; }
2612
+ }
2613
+ @media (max-width: 960px) {
2614
+ .wo-editor-main { grid-template-columns: 1fr; }
2615
+ .wo-editor-stage { min-height: 440px; }
2616
+ .wo-editor-status { flex-direction: column; }
2617
+ }
2618
+ `;
2619
+ document.head.append(style);
2620
+ }