worldorbit 2.6.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +12 -5
  2. package/dist/browser/core/dist/atlas-edit.d.ts +11 -0
  3. package/dist/browser/core/dist/atlas-edit.js +347 -0
  4. package/dist/browser/core/dist/atlas-utils.d.ts +22 -0
  5. package/dist/browser/core/dist/atlas-utils.js +189 -0
  6. package/dist/browser/core/dist/atlas-validate.d.ts +2 -0
  7. package/dist/browser/core/dist/atlas-validate.js +488 -0
  8. package/dist/browser/core/dist/diagnostics.d.ts +10 -0
  9. package/dist/browser/core/dist/diagnostics.js +109 -0
  10. package/dist/browser/core/dist/draft-parse.d.ts +3 -0
  11. package/dist/browser/core/dist/draft-parse.js +1654 -0
  12. package/dist/browser/core/dist/draft.d.ts +21 -0
  13. package/dist/browser/core/dist/draft.js +482 -0
  14. package/dist/browser/core/dist/errors.d.ts +7 -0
  15. package/dist/browser/core/dist/errors.js +16 -0
  16. package/dist/browser/core/dist/format.d.ts +4 -0
  17. package/dist/browser/core/dist/format.js +613 -0
  18. package/dist/browser/core/dist/index.d.ts +29 -0
  19. package/dist/browser/core/dist/index.js +35 -6542
  20. package/dist/browser/core/dist/load.d.ts +4 -0
  21. package/dist/browser/core/dist/load.js +182 -0
  22. package/dist/browser/core/dist/markdown.d.ts +2 -0
  23. package/dist/browser/core/dist/markdown.js +37 -0
  24. package/dist/browser/core/dist/normalize.d.ts +2 -0
  25. package/dist/browser/core/dist/normalize.js +312 -0
  26. package/dist/browser/core/dist/parse.d.ts +2 -0
  27. package/dist/browser/core/dist/parse.js +133 -0
  28. package/dist/browser/core/dist/scene.d.ts +3 -0
  29. package/dist/browser/core/dist/scene.js +1901 -0
  30. package/dist/browser/core/dist/schema.d.ts +8 -0
  31. package/dist/browser/core/dist/schema.js +298 -0
  32. package/dist/browser/core/dist/spatial-scene.d.ts +3 -0
  33. package/dist/browser/core/dist/spatial-scene.js +420 -0
  34. package/dist/browser/core/dist/tokenize.d.ts +4 -0
  35. package/dist/browser/core/dist/tokenize.js +68 -0
  36. package/dist/browser/core/dist/types.d.ts +637 -0
  37. package/dist/browser/core/dist/types.js +1 -0
  38. package/dist/browser/core/dist/validate.d.ts +2 -0
  39. package/dist/browser/core/dist/validate.js +56 -0
  40. package/dist/browser/editor/dist/editor.d.ts +2 -0
  41. package/dist/browser/editor/dist/editor.js +3700 -0
  42. package/dist/browser/editor/dist/index.d.ts +2 -0
  43. package/dist/browser/editor/dist/index.js +1 -12250
  44. package/dist/browser/editor/dist/types.d.ts +59 -0
  45. package/dist/browser/editor/dist/types.js +1 -0
  46. package/dist/browser/markdown/dist/html.d.ts +3 -0
  47. package/dist/browser/markdown/dist/html.js +64 -0
  48. package/dist/browser/markdown/dist/index.d.ts +4 -0
  49. package/dist/browser/markdown/dist/index.js +3 -6179
  50. package/dist/browser/markdown/dist/rehype.d.ts +10 -0
  51. package/dist/browser/markdown/dist/rehype.js +49 -0
  52. package/dist/browser/markdown/dist/remark.d.ts +9 -0
  53. package/dist/browser/markdown/dist/remark.js +28 -0
  54. package/dist/browser/markdown/dist/types.d.ts +11 -0
  55. package/dist/browser/markdown/dist/types.js +1 -0
  56. package/dist/browser/viewer/dist/atlas-state.d.ts +12 -0
  57. package/dist/browser/viewer/dist/atlas-state.js +269 -0
  58. package/dist/browser/viewer/dist/atlas-viewer.d.ts +2 -0
  59. package/dist/browser/viewer/dist/atlas-viewer.js +495 -0
  60. package/dist/browser/viewer/dist/custom-element.d.ts +1 -0
  61. package/dist/browser/viewer/dist/custom-element.js +78 -0
  62. package/dist/browser/viewer/dist/embed.d.ts +24 -0
  63. package/dist/browser/viewer/dist/embed.js +172 -0
  64. package/dist/browser/viewer/dist/errors.d.ts +6 -0
  65. package/dist/browser/viewer/dist/errors.js +12 -0
  66. package/dist/browser/viewer/dist/index.d.ts +10 -0
  67. package/dist/browser/viewer/dist/index.js +9 -8334
  68. package/dist/browser/viewer/dist/minimap.d.ts +3 -0
  69. package/dist/browser/viewer/dist/minimap.js +63 -0
  70. package/dist/browser/viewer/dist/render.d.ts +6 -0
  71. package/dist/browser/viewer/dist/render.js +670 -0
  72. package/dist/browser/viewer/dist/runtime-3d.d.ts +19 -0
  73. package/dist/browser/viewer/dist/runtime-3d.js +494 -0
  74. package/dist/browser/viewer/dist/theme.d.ts +4 -0
  75. package/dist/browser/viewer/dist/theme.js +103 -0
  76. package/dist/browser/viewer/dist/tooltip.d.ts +3 -0
  77. package/dist/browser/viewer/dist/tooltip.js +198 -0
  78. package/dist/browser/viewer/dist/types.d.ts +292 -0
  79. package/dist/browser/viewer/dist/types.js +1 -0
  80. package/dist/browser/viewer/dist/vendor/three.module.js +53032 -0
  81. package/dist/browser/viewer/dist/viewer-state.d.ts +19 -0
  82. package/dist/browser/viewer/dist/viewer-state.js +162 -0
  83. package/dist/browser/viewer/dist/viewer.d.ts +2 -0
  84. package/dist/browser/viewer/dist/viewer.js +1662 -0
  85. package/dist/unpkg/core/dist/atlas-edit.d.ts +11 -0
  86. package/dist/unpkg/core/dist/atlas-edit.js +347 -0
  87. package/dist/unpkg/core/dist/atlas-utils.d.ts +22 -0
  88. package/dist/unpkg/core/dist/atlas-utils.js +189 -0
  89. package/dist/unpkg/core/dist/atlas-validate.d.ts +2 -0
  90. package/dist/unpkg/core/dist/atlas-validate.js +488 -0
  91. package/dist/unpkg/core/dist/diagnostics.d.ts +10 -0
  92. package/dist/unpkg/core/dist/diagnostics.js +109 -0
  93. package/dist/unpkg/core/dist/draft-parse.d.ts +3 -0
  94. package/dist/unpkg/core/dist/draft-parse.js +1654 -0
  95. package/dist/unpkg/core/dist/draft.d.ts +21 -0
  96. package/dist/unpkg/core/dist/draft.js +482 -0
  97. package/dist/unpkg/core/dist/errors.d.ts +7 -0
  98. package/dist/unpkg/core/dist/errors.js +16 -0
  99. package/dist/unpkg/core/dist/format.d.ts +4 -0
  100. package/dist/unpkg/core/dist/format.js +613 -0
  101. package/dist/unpkg/core/dist/index.d.ts +29 -0
  102. package/dist/unpkg/core/dist/index.js +35 -6614
  103. package/dist/unpkg/core/dist/load.d.ts +4 -0
  104. package/dist/unpkg/core/dist/load.js +182 -0
  105. package/dist/unpkg/core/dist/markdown.d.ts +2 -0
  106. package/dist/unpkg/core/dist/markdown.js +37 -0
  107. package/dist/unpkg/core/dist/normalize.d.ts +2 -0
  108. package/dist/unpkg/core/dist/normalize.js +312 -0
  109. package/dist/unpkg/core/dist/parse.d.ts +2 -0
  110. package/dist/unpkg/core/dist/parse.js +133 -0
  111. package/dist/unpkg/core/dist/scene.d.ts +3 -0
  112. package/dist/unpkg/core/dist/scene.js +1901 -0
  113. package/dist/unpkg/core/dist/schema.d.ts +8 -0
  114. package/dist/unpkg/core/dist/schema.js +298 -0
  115. package/dist/unpkg/core/dist/spatial-scene.d.ts +3 -0
  116. package/dist/unpkg/core/dist/spatial-scene.js +420 -0
  117. package/dist/unpkg/core/dist/tokenize.d.ts +4 -0
  118. package/dist/unpkg/core/dist/tokenize.js +68 -0
  119. package/dist/unpkg/core/dist/types.d.ts +637 -0
  120. package/dist/unpkg/core/dist/types.js +1 -0
  121. package/dist/unpkg/core/dist/validate.d.ts +2 -0
  122. package/dist/unpkg/core/dist/validate.js +56 -0
  123. package/dist/unpkg/editor/dist/editor.d.ts +2 -0
  124. package/dist/unpkg/editor/dist/editor.js +3700 -0
  125. package/dist/unpkg/editor/dist/index.d.ts +2 -0
  126. package/dist/unpkg/editor/dist/index.js +1 -12275
  127. package/dist/unpkg/editor/dist/types.d.ts +59 -0
  128. package/dist/unpkg/editor/dist/types.js +1 -0
  129. package/dist/unpkg/markdown/dist/html.d.ts +3 -0
  130. package/dist/unpkg/markdown/dist/html.js +64 -0
  131. package/dist/unpkg/markdown/dist/index.d.ts +4 -0
  132. package/dist/unpkg/markdown/dist/index.js +3 -6207
  133. package/dist/unpkg/markdown/dist/rehype.d.ts +10 -0
  134. package/dist/unpkg/markdown/dist/rehype.js +49 -0
  135. package/dist/unpkg/markdown/dist/remark.d.ts +9 -0
  136. package/dist/unpkg/markdown/dist/remark.js +28 -0
  137. package/dist/unpkg/markdown/dist/types.d.ts +11 -0
  138. package/dist/unpkg/markdown/dist/types.js +1 -0
  139. package/dist/unpkg/viewer/dist/atlas-state.d.ts +12 -0
  140. package/dist/unpkg/viewer/dist/atlas-state.js +269 -0
  141. package/dist/unpkg/viewer/dist/atlas-viewer.d.ts +2 -0
  142. package/dist/unpkg/viewer/dist/atlas-viewer.js +495 -0
  143. package/dist/unpkg/viewer/dist/custom-element.d.ts +1 -0
  144. package/dist/unpkg/viewer/dist/custom-element.js +78 -0
  145. package/dist/unpkg/viewer/dist/embed.d.ts +24 -0
  146. package/dist/unpkg/viewer/dist/embed.js +172 -0
  147. package/dist/unpkg/viewer/dist/errors.d.ts +6 -0
  148. package/dist/unpkg/viewer/dist/errors.js +12 -0
  149. package/dist/unpkg/viewer/dist/index.d.ts +10 -0
  150. package/dist/unpkg/viewer/dist/index.js +9 -8391
  151. package/dist/unpkg/viewer/dist/minimap.d.ts +3 -0
  152. package/dist/unpkg/viewer/dist/minimap.js +63 -0
  153. package/dist/unpkg/viewer/dist/render.d.ts +6 -0
  154. package/dist/unpkg/viewer/dist/render.js +670 -0
  155. package/dist/unpkg/viewer/dist/runtime-3d.d.ts +19 -0
  156. package/dist/unpkg/viewer/dist/runtime-3d.js +494 -0
  157. package/dist/unpkg/viewer/dist/theme.d.ts +4 -0
  158. package/dist/unpkg/viewer/dist/theme.js +103 -0
  159. package/dist/unpkg/viewer/dist/tooltip.d.ts +3 -0
  160. package/dist/unpkg/viewer/dist/tooltip.js +198 -0
  161. package/dist/unpkg/viewer/dist/types.d.ts +292 -0
  162. package/dist/unpkg/viewer/dist/types.js +1 -0
  163. package/dist/unpkg/viewer/dist/vendor/three.module.js +53032 -0
  164. package/dist/unpkg/viewer/dist/viewer-state.d.ts +19 -0
  165. package/dist/unpkg/viewer/dist/viewer-state.js +162 -0
  166. package/dist/unpkg/viewer/dist/viewer.d.ts +2 -0
  167. package/dist/unpkg/viewer/dist/viewer.js +1662 -0
  168. package/dist/unpkg/worldorbit-core.min.js +1 -12
  169. package/dist/unpkg/worldorbit-editor.min.js +1 -904
  170. package/dist/unpkg/worldorbit-markdown.min.js +1 -103
  171. package/dist/unpkg/worldorbit-viewer.min.js +1 -259
  172. package/dist/unpkg/worldorbit.js +2 -9704
  173. package/dist/unpkg/worldorbit.min.js +2 -263
  174. package/package.json +1 -1
  175. package/packages/core/dist/index.d.ts +1 -0
  176. package/packages/core/dist/index.js +1 -0
  177. package/packages/core/dist/spatial-scene.d.ts +3 -0
  178. package/packages/core/dist/spatial-scene.js +420 -0
  179. package/packages/core/dist/types.d.ts +105 -0
  180. package/packages/editor/dist/editor.js +25 -4
  181. package/packages/editor/dist/types.d.ts +4 -0
  182. package/packages/markdown/dist/html.js +10 -3
  183. package/packages/viewer/dist/atlas-state.js +3 -0
  184. package/packages/viewer/dist/atlas-viewer.js +1 -0
  185. package/packages/viewer/dist/custom-element.js +18 -4
  186. package/packages/viewer/dist/embed.d.ts +5 -1
  187. package/packages/viewer/dist/embed.js +58 -24
  188. package/packages/viewer/dist/errors.d.ts +6 -0
  189. package/packages/viewer/dist/errors.js +12 -0
  190. package/packages/viewer/dist/index.d.ts +1 -0
  191. package/packages/viewer/dist/index.js +1 -0
  192. package/packages/viewer/dist/runtime-3d.d.ts +19 -0
  193. package/packages/viewer/dist/runtime-3d.js +494 -0
  194. package/packages/viewer/dist/types.d.ts +21 -2
  195. package/packages/viewer/dist/vendor/three.module.js +53032 -0
  196. package/packages/viewer/dist/viewer.js +501 -41
@@ -0,0 +1,670 @@
1
+ import { loadWorldOrbitSource, renderDocumentToScene, } from "@worldorbit/core";
2
+ import { computeVisibleObjectIds } from "./atlas-state.js";
3
+ import { resolveLayers, resolveTheme } from "./theme.js";
4
+ export const WORLD_LAYER_ID = "worldorbit-camera-root";
5
+ export function renderSceneToSvg(scene, options = {}) {
6
+ const theme = resolveTheme(options.theme);
7
+ const presetDefaults = resolveRenderPreset(options.preset ?? scene.renderPreset ?? undefined);
8
+ const layers = resolveLayers({
9
+ ...presetDefaults.layers,
10
+ ...options.layers,
11
+ });
12
+ const subtitle = options.subtitle ?? scene.subtitle;
13
+ const visibleObjectIds = computeVisibleObjectIds(scene, options.filter ?? null);
14
+ const visibleObjects = scene.objects
15
+ .filter((object) => !object.hidden)
16
+ .filter((object) => visibleObjectIds.has(object.objectId))
17
+ .filter((object) => layers.structures || !isStructureLike(object.object))
18
+ .sort((left, right) => left.sortKey - right.sortKey);
19
+ const visibleLabels = scene.labels
20
+ .filter((label) => !label.hidden)
21
+ .filter((label) => visibleObjectIds.has(label.objectId))
22
+ .filter((label) => layers.structures || !isStructureLike(label.object));
23
+ const imageDefinitions = buildImageDefinitions(visibleObjects);
24
+ const orbitMarkup = layers.orbits
25
+ ? renderOrbitLayer(scene, visibleObjectIds, layers.structures)
26
+ : { back: "", front: "" };
27
+ const leaderMarkup = layers.guides
28
+ ? scene.leaders
29
+ .filter((leader) => !leader.hidden)
30
+ .filter((leader) => visibleObjectIds.has(leader.objectId))
31
+ .filter((leader) => layers.structures || !isStructureLike(leader.object))
32
+ .map((leader) => `<line class="wo-leader wo-leader-${leader.mode}" x1="${leader.x1}" y1="${leader.y1}" x2="${leader.x2}" y2="${leader.y2}" data-render-id="${escapeXml(leader.renderId)}" data-group-id="${escapeAttribute(leader.groupId ?? "")}" />`)
33
+ .join("")
34
+ : "";
35
+ const relationMarkup = layers.relations
36
+ ? scene.relations
37
+ .filter((relation) => !relation.hidden)
38
+ .filter((relation) => visibleObjectIds.has(relation.fromObjectId) && visibleObjectIds.has(relation.toObjectId))
39
+ .map((relation) => `<line class="wo-relation" x1="${relation.x1}" y1="${relation.y1}" x2="${relation.x2}" y2="${relation.y2}" data-render-id="${escapeXml(relation.renderId)}" data-relation-id="${escapeAttribute(relation.relationId)}" />`)
40
+ .join("")
41
+ : "";
42
+ const eventMarkup = layers.events
43
+ ? scene.events
44
+ .filter((event) => !event.hidden)
45
+ .map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme))
46
+ .join("")
47
+ : "";
48
+ const objectMarkup = layers.objects
49
+ ? visibleObjects
50
+ .map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme))
51
+ .join("")
52
+ : "";
53
+ const labelMarkup = layers.labels
54
+ ? visibleLabels
55
+ .map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null))
56
+ .join("")
57
+ : "";
58
+ const metadataMarkup = layers.metadata
59
+ ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
60
+ <text class="wo-subtitle" x="56" y="88">${escapeXml(subtitle)}</text>
61
+ <text class="wo-meta" x="56" y="${scene.height - 42}">${escapeXml(renderMetadata(scene))}</text>`
62
+ : "";
63
+ const backgroundMarkup = layers.background
64
+ ? `<rect class="wo-bg" x="0" y="0" width="${scene.width}" height="${scene.height}" rx="28" ry="28" />
65
+ <rect class="wo-bg-glow" x="0" y="0" width="${scene.width}" height="${scene.height}" rx="28" ry="28" />
66
+ ${layers.guides ? renderBackdrop(scene.width, scene.height) : ""}`
67
+ : "";
68
+ return `<svg xmlns="http://www.w3.org/2000/svg" data-worldorbit-svg="true" width="${scene.width}" height="${scene.height}" viewBox="0 0 ${scene.width} ${scene.height}" preserveAspectRatio="xMidYMid meet" role="img" aria-labelledby="worldorbit-title worldorbit-desc">
69
+ <title id="worldorbit-title">${escapeXml(scene.title)}</title>
70
+ <desc id="worldorbit-desc">A ${escapeXml(scene.viewMode)} WorldOrbit render with ${visibleObjects.length} visible objects.</desc>
71
+ <defs>
72
+ <linearGradient id="wo-bg" x1="0%" y1="0%" x2="100%" y2="100%">
73
+ <stop offset="0%" stop-color="${theme.backgroundStart}" />
74
+ <stop offset="100%" stop-color="${theme.backgroundEnd}" />
75
+ </linearGradient>
76
+ <radialGradient id="wo-star-glow" cx="50%" cy="50%" r="50%">
77
+ <stop offset="0%" stop-color="${theme.starGlow}" stop-opacity="0.95" />
78
+ <stop offset="100%" stop-color="${theme.starCore}" stop-opacity="0" />
79
+ </radialGradient>
80
+ <radialGradient id="wo-bg-glow" cx="20%" cy="10%" r="90%">
81
+ <stop offset="0%" stop-color="${theme.backgroundGlow}" />
82
+ <stop offset="100%" stop-color="transparent" />
83
+ </radialGradient>
84
+ ${imageDefinitions}
85
+ </defs>
86
+ <style>
87
+ .wo-bg { fill: url(#wo-bg); }
88
+ .wo-bg-glow { fill: url(#wo-bg-glow); }
89
+ .wo-grid { fill: none; stroke: ${theme.guide}; stroke-width: 1; }
90
+ .wo-orbit { fill: none; stroke: ${theme.orbit}; stroke-width: 1.5; }
91
+ .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
92
+ .wo-orbit-front { opacity: 0.9; }
93
+ .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
94
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
95
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
96
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
97
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
98
+ .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
99
+ .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
100
+ .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
101
+ .wo-title { fill: ${theme.ink}; font: 700 24px ${theme.displayFont}; letter-spacing: 0.06em; text-transform: uppercase; }
102
+ .wo-subtitle { fill: ${theme.muted}; font: 500 12px ${theme.fontFamily}; letter-spacing: 0.14em; text-transform: uppercase; }
103
+ .wo-meta { fill: ${theme.muted}; font: 500 11px ${theme.fontFamily}; letter-spacing: 0.06em; }
104
+ .wo-object { cursor: pointer; outline: none; }
105
+ .wo-object:focus-visible .wo-selection-ring,
106
+ .wo-object:hover .wo-selection-ring,
107
+ .wo-object-selected .wo-selection-ring { display: block; }
108
+ .wo-object-selected .wo-selection-ring { stroke: ${theme.selected}; }
109
+ .wo-object-label-selected .wo-label { fill: ${theme.accent}; }
110
+ .wo-object-label-selected .wo-label-secondary { fill: ${theme.selected}; }
111
+ .wo-chain-selected .wo-selection-ring,
112
+ .wo-chain-hover .wo-selection-ring { display: block; }
113
+ .wo-ancestor-selected .wo-selection-ring,
114
+ .wo-ancestor-hover .wo-selection-ring { display: block; opacity: 0.66; }
115
+ .wo-chain-selected .wo-label,
116
+ .wo-chain-hover .wo-label { fill: ${theme.accent}; }
117
+ .wo-ancestor-selected .wo-label,
118
+ .wo-ancestor-hover .wo-label { fill: ${theme.ink}; opacity: 0.82; }
119
+ .wo-orbit-related-selected,
120
+ .wo-orbit-related-hover { stroke: ${theme.accentStrong}; opacity: 1; }
121
+ .wo-selection-ring { display: none; fill: none; stroke-width: 2; stroke-dasharray: 6 5; }
122
+ .wo-object-image { pointer-events: none; }
123
+ </style>
124
+ ${backgroundMarkup}
125
+ ${metadataMarkup}
126
+ <g data-worldorbit-world="true">
127
+ <g data-worldorbit-camera-root="${WORLD_LAYER_ID}" id="${WORLD_LAYER_ID}">
128
+ <g data-worldorbit-world-content="true">
129
+ ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
130
+ ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
131
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
132
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
133
+ ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
134
+ ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
135
+ ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
136
+ </g>
137
+ </g>
138
+ </g>
139
+ </svg>`;
140
+ }
141
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
142
+ const participants = event.objectIds
143
+ .filter((objectId) => visibleObjectIds.has(objectId))
144
+ .map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden))
145
+ .filter(Boolean);
146
+ if (participants.length === 0) {
147
+ return "";
148
+ }
149
+ const stroke = event.event.color || theme.accent;
150
+ const label = event.event.label || event.event.id;
151
+ const lineMarkup = participants
152
+ .map((object) => `<line class="wo-event-line" x1="${event.x}" y1="${event.y}" x2="${object.x}" y2="${object.y}" stroke="${escapeAttribute(stroke)}" data-event-id="${escapeAttribute(event.eventId)}" data-object-id="${escapeAttribute(object.objectId)}" />`)
153
+ .join("");
154
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
155
+ ${lineMarkup}
156
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
157
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
158
+ </g>`;
159
+ }
160
+ export function renderDocumentToSvg(document, options = {}) {
161
+ return renderSceneToSvg(renderDocumentToScene(document, options), options);
162
+ }
163
+ export function renderSourceToSvg(source, options = {}) {
164
+ const loaded = loadWorldOrbitSource(source);
165
+ return renderDocumentToSvg(loaded.document, resolveSourceRenderOptions(loaded, options));
166
+ }
167
+ function resolveSourceRenderOptions(loaded, options) {
168
+ const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
169
+ if (options.preset || !atlasDocument?.system?.defaults.preset) {
170
+ return options;
171
+ }
172
+ return {
173
+ ...options,
174
+ preset: atlasDocument.system.defaults.preset,
175
+ };
176
+ }
177
+ function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
178
+ const backParts = [];
179
+ const frontParts = [];
180
+ for (const visual of scene.orbitVisuals.filter((entry) => !entry.hidden &&
181
+ visibleObjectIds.has(entry.objectId) &&
182
+ (includeStructures || !isStructureLike(entry.object)))) {
183
+ const strokeWidth = visual.bandThickness ?? (visual.band ? 10 : 1.5);
184
+ const orbitClass = visual.band ? "wo-orbit wo-orbit-band wo-orbit-node" : "wo-orbit wo-orbit-node";
185
+ const dataAttributes = `data-render-id="${escapeXml(visual.renderId)}" data-orbit-object-id="${escapeAttribute(visual.objectId)}" data-parent-id="${escapeAttribute(visual.parentId)}" data-group-id="${escapeAttribute(visual.groupId ?? "")}"`;
186
+ if (visual.backArcPath || visual.frontArcPath) {
187
+ if (visual.backArcPath) {
188
+ backParts.push(`<path class="${orbitClass} wo-orbit-back" d="${visual.backArcPath}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
189
+ }
190
+ if (visual.frontArcPath) {
191
+ frontParts.push(`<path class="${orbitClass} wo-orbit-front" d="${visual.frontArcPath}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
192
+ }
193
+ continue;
194
+ }
195
+ if (visual.kind === "ellipse") {
196
+ const rx = visual.rx ?? visual.radius ?? 0;
197
+ const ry = visual.ry ?? visual.radius ?? 0;
198
+ frontParts.push(`<ellipse class="${orbitClass} wo-orbit-front" cx="${visual.cx}" cy="${visual.cy}" rx="${rx}" ry="${ry}" transform="rotate(${visual.rotationDeg} ${visual.cx} ${visual.cy})" stroke-width="${strokeWidth}" ${dataAttributes} />`);
199
+ continue;
200
+ }
201
+ frontParts.push(`<circle class="${orbitClass} wo-orbit-front" cx="${visual.cx}" cy="${visual.cy}" r="${visual.radius ?? 0}" stroke-width="${strokeWidth}" ${dataAttributes} />`);
202
+ }
203
+ return {
204
+ back: backParts.join(""),
205
+ front: frontParts.join(""),
206
+ };
207
+ }
208
+ function buildImageDefinitions(objects) {
209
+ return objects
210
+ .filter((object) => Boolean(object.imageHref))
211
+ .map((object) => renderImageClipPath(object))
212
+ .join("");
213
+ }
214
+ function renderSceneObject(sceneObject, selectedObjectId, theme) {
215
+ const { object, x, y, radius, visualRadius } = sceneObject;
216
+ const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
217
+ const kindClass = object.properties.kind
218
+ ? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}`
219
+ : "";
220
+ const palette = resolveObjectPalette(sceneObject, theme);
221
+ const imageMarkup = renderObjectImage(sceneObject);
222
+ const outlineMarkup = imageMarkup
223
+ ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true })
224
+ : "";
225
+ return `<g class="wo-object wo-object-${object.type}${kindClass}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
226
+ <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
227
+ ${renderAtmosphere(sceneObject, palette)}
228
+ ${renderObjectBody(object, x, y, radius, palette)}
229
+ ${imageMarkup}
230
+ ${outlineMarkup}
231
+ </g>`;
232
+ }
233
+ function renderSceneLabel(scene, label, selectedObjectId) {
234
+ const selectionClass = selectedObjectId === label.objectId ? " wo-object-label-selected" : "";
235
+ const labelScale = scene.scaleModel.labelMultiplier;
236
+ return `<g class="wo-object-label${selectionClass}" data-object-id="${escapeXml(label.objectId)}" data-group-id="${escapeAttribute(label.groupId ?? "")}" data-render-id="${escapeXml(label.renderId)}">
237
+ <text class="wo-label" x="${label.x}" y="${label.y}" text-anchor="${label.textAnchor}" font-size="${14 * labelScale}">${escapeXml(label.label)}</text>
238
+ <text class="wo-label-secondary" x="${label.x}" y="${label.secondaryY}" text-anchor="${label.textAnchor}" font-size="${11 * labelScale}">${escapeXml(label.secondaryLabel)}</text>
239
+ </g>`;
240
+ }
241
+ function renderObjectBody(object, x, y, radius, palette, options = {}) {
242
+ const fill = options.outlineOnly ? "transparent" : palette.fill;
243
+ const tail = palette.tail ?? palette.fill;
244
+ switch (object.type) {
245
+ case "star":
246
+ return options.outlineOnly
247
+ ? `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="2" />`
248
+ : `<circle cx="${x}" cy="${y}" r="${radius * 2.4}" fill="${palette.glow ?? "url(#wo-star-glow)"}" opacity="0.84" />
249
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
250
+ case "planet":
251
+ case "moon":
252
+ case "belt":
253
+ case "asteroid":
254
+ case "ring":
255
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="${options.outlineOnly ? 1.5 : 1.4}" />`;
256
+ case "comet":
257
+ return options.outlineOnly
258
+ ? `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`
259
+ : `<path d="M ${x - radius * 2} ${y + radius * 1.3} Q ${x - radius * 0.7} ${y + radius * 0.3} ${x - radius * 0.45} ${y}" fill="none" stroke="${tail}" stroke-width="${Math.max(2, radius * 0.8)}" stroke-linecap="round" opacity="0.85" />
260
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
261
+ case "structure":
262
+ return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
263
+ case "phenomenon": {
264
+ const kind = String(object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
265
+ if (options.outlineOnly) {
266
+ if (kind === "black-hole" || kind === "nebula" || kind === "galaxy" || kind === "dwarf-galaxy") {
267
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
268
+ }
269
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
270
+ }
271
+ if (kind === "black-hole") {
272
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.4}" ry="${radius * 0.55}" fill="none" stroke="${palette.accentRing ?? palette.stroke}" stroke-width="3.5" />
273
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
274
+ }
275
+ if (kind === "galaxy") {
276
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.6}" ry="${radius}" fill="${palette.halo ?? "none"}" stroke="none" />
277
+ <ellipse cx="${x}" cy="${y}" rx="${radius * 1.5}" ry="${radius * 0.42}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.2" />
278
+ <circle cx="${x}" cy="${y}" r="${radius * 0.28}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
279
+ }
280
+ if (kind === "dwarf-galaxy") {
281
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 1.6}" ry="${radius * 0.55}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />
282
+ <circle cx="${x}" cy="${y}" r="${radius * 0.25}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
283
+ }
284
+ if (kind === "nebula") {
285
+ return `<circle cx="${x}" cy="${y}" r="${radius * 2.2}" fill="${palette.halo ?? "none"}" stroke="none" />
286
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />`;
287
+ }
288
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
289
+ }
290
+ }
291
+ }
292
+ function renderAtmosphere(sceneObject, palette) {
293
+ if (!palette.atmosphere) {
294
+ return "";
295
+ }
296
+ const { object, x, y, radius } = sceneObject;
297
+ if (object.type !== "planet" && object.type !== "moon" && object.type !== "asteroid") {
298
+ return "";
299
+ }
300
+ return `<circle cx="${x}" cy="${y}" r="${radius + 4}" fill="none" stroke="${palette.atmosphere}" stroke-width="4" opacity="0.45" />`;
301
+ }
302
+ function renderObjectImage(sceneObject) {
303
+ if (!sceneObject.imageHref) {
304
+ return "";
305
+ }
306
+ const bounds = imageBoundsForObject(sceneObject);
307
+ return `<image class="wo-object-image" x="${bounds.x}" y="${bounds.y}" width="${bounds.width}" height="${bounds.height}" href="${escapeAttribute(sceneObject.imageHref)}" preserveAspectRatio="xMidYMid slice" clip-path="url(#${escapeAttribute(getObjectClipPathId(sceneObject))})" />`;
308
+ }
309
+ function renderImageClipPath(sceneObject) {
310
+ const { x, y, radius } = sceneObject;
311
+ let content = "";
312
+ switch (sceneObject.object.type) {
313
+ case "star":
314
+ case "planet":
315
+ case "moon":
316
+ case "asteroid":
317
+ case "comet":
318
+ content = `<circle cx="${x}" cy="${y}" r="${radius}" />`;
319
+ break;
320
+ case "structure":
321
+ content = `<polygon points="${diamondPoints(x, y, radius)}" />`;
322
+ break;
323
+ case "phenomenon":
324
+ content = `<polygon points="${phenomenonPoints(x, y, radius)}" />`;
325
+ break;
326
+ default:
327
+ return "";
328
+ }
329
+ return `<clipPath id="${escapeAttribute(getObjectClipPathId(sceneObject))}" clipPathUnits="userSpaceOnUse">${content}</clipPath>`;
330
+ }
331
+ function imageBoundsForObject(sceneObject) {
332
+ const { object, x, y, radius } = sceneObject;
333
+ switch (object.type) {
334
+ case "structure":
335
+ return {
336
+ x: x - radius,
337
+ y: y - radius,
338
+ width: radius * 2,
339
+ height: radius * 2,
340
+ };
341
+ case "phenomenon":
342
+ return {
343
+ x: x - radius * 1.2,
344
+ y: y - radius * 1.2,
345
+ width: radius * 2.4,
346
+ height: radius * 2.4,
347
+ };
348
+ default:
349
+ return {
350
+ x: x - radius,
351
+ y: y - radius,
352
+ width: radius * 2,
353
+ height: radius * 2,
354
+ };
355
+ }
356
+ }
357
+ function computeLabelPlacements(scene, objects) {
358
+ const placements = new Map();
359
+ const occupied = [];
360
+ const labelScale = scene.scaleModel.labelMultiplier;
361
+ for (const object of objects) {
362
+ const direction = object.y > scene.height * 0.62 ? -1 : 1;
363
+ const labelHalfWidth = estimateLabelHalfWidth(object, labelScale);
364
+ let labelY = object.y + direction * (object.radius + 18 * labelScale);
365
+ let secondaryY = labelY + direction * (16 * labelScale);
366
+ let rect = labelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
367
+ let attempts = 0;
368
+ while (occupied.some((entry) => rectsOverlap(entry, rect)) && attempts < 10) {
369
+ labelY += direction * 14 * labelScale;
370
+ secondaryY += direction * 14 * labelScale;
371
+ rect = labelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
372
+ attempts += 1;
373
+ }
374
+ occupied.push(rect);
375
+ placements.set(object.objectId, { labelY, secondaryY });
376
+ }
377
+ return placements;
378
+ }
379
+ function defaultLabelPlacement(scene, object) {
380
+ const direction = object.y > scene.height * 0.62 ? -1 : 1;
381
+ const labelScale = scene.scaleModel.labelMultiplier;
382
+ const labelY = object.y + direction * (object.radius + 18 * labelScale);
383
+ return {
384
+ labelY,
385
+ secondaryY: labelY + direction * (16 * labelScale),
386
+ };
387
+ }
388
+ function labelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
389
+ return {
390
+ left: x - labelHalfWidth,
391
+ right: x + labelHalfWidth,
392
+ top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
393
+ bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12),
394
+ };
395
+ }
396
+ function rectsOverlap(left, right) {
397
+ return !(left.right < right.left ||
398
+ right.right < left.left ||
399
+ left.bottom < right.top ||
400
+ right.bottom < left.top);
401
+ }
402
+ function resolveObjectPalette(sceneObject, theme) {
403
+ const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
404
+ const base = basePaletteForType(sceneObject.object.type, kind, theme);
405
+ const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor)
406
+ ? sceneObject.fillColor
407
+ : base.fill;
408
+ const albedo = numericValue(sceneObject.object.properties.albedo);
409
+ const temperature = numericValue(sceneObject.object.properties.temperature);
410
+ const fill = applyTemperatureAndAlbedo(customFill, temperature, albedo, sceneObject.object.type);
411
+ const stroke = mixColors(fill, "#ffffff", 0.4) ?? base.stroke;
412
+ const atmosphere = atmosphereColor(sceneObject.object.properties.atmosphere);
413
+ const glow = sceneObject.object.type === "star"
414
+ ? rgbaString(mixColors(fill, "#fff2cb", 0.4) ?? fill, 0.82)
415
+ : undefined;
416
+ return {
417
+ fill,
418
+ stroke,
419
+ glow,
420
+ atmosphere,
421
+ tail: sceneObject.object.type === "comet"
422
+ ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72)
423
+ : undefined,
424
+ };
425
+ }
426
+ function basePaletteForType(type, kind, theme) {
427
+ switch (type) {
428
+ case "star":
429
+ return {
430
+ fill: theme.starCore,
431
+ stroke: theme.starStroke,
432
+ };
433
+ case "planet":
434
+ return { fill: "#72b7ff", stroke: "#d8efff" };
435
+ case "moon":
436
+ return { fill: "#c7d7ea", stroke: "#f2f8ff" };
437
+ case "belt":
438
+ return { fill: "#d9aa74", stroke: "#f7d5aa" };
439
+ case "asteroid":
440
+ return { fill: "#a7a5b8", stroke: "#ebe5ff" };
441
+ case "comet":
442
+ return { fill: "#9ce7ff", stroke: "#e7fbff" };
443
+ case "ring":
444
+ return { fill: "#e59f7d", stroke: "#fff0d3" };
445
+ case "structure":
446
+ return { fill: theme.accentStrong, stroke: "#fff2ea" };
447
+ case "phenomenon":
448
+ return kindPhenomenonPalette(kind);
449
+ }
450
+ }
451
+ function kindPhenomenonPalette(kind) {
452
+ if (kind === "galaxy") {
453
+ return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
454
+ }
455
+ if (kind === "dwarf-galaxy") {
456
+ return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
457
+ }
458
+ if (kind === "black-hole") {
459
+ return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
460
+ }
461
+ if (kind === "nebula") {
462
+ return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
463
+ }
464
+ if (kind === "void") {
465
+ return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
466
+ }
467
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
468
+ }
469
+ function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
470
+ let nextColor = baseColor;
471
+ if (temperature !== null) {
472
+ const tint = temperatureTint(temperature, type);
473
+ nextColor = mixColors(nextColor, tint, type === "star" ? 0.42 : 0.2) ?? nextColor;
474
+ }
475
+ if (albedo !== null) {
476
+ const clamped = Math.min(Math.max(albedo, 0), 1);
477
+ nextColor =
478
+ mixColors(nextColor, clamped >= 0.5 ? "#ffffff" : "#0f1520", Math.abs(clamped - 0.5) * 0.7) ??
479
+ nextColor;
480
+ }
481
+ return nextColor;
482
+ }
483
+ function temperatureTint(value, type) {
484
+ if (type === "star") {
485
+ if (value >= 8_000)
486
+ return "#d7ebff";
487
+ if (value >= 6_000)
488
+ return "#fff3d8";
489
+ if (value >= 4_000)
490
+ return "#ffd39a";
491
+ return "#ff9d76";
492
+ }
493
+ if (value <= 180)
494
+ return "#8fd8ff";
495
+ if (value >= 600)
496
+ return "#ffb56e";
497
+ return "#fff1c4";
498
+ }
499
+ function atmosphereColor(value) {
500
+ if (typeof value !== "string" || !value.trim()) {
501
+ return undefined;
502
+ }
503
+ const normalized = value.trim().toLowerCase();
504
+ if (normalized.includes("methane"))
505
+ return "rgba(134, 236, 255, 0.75)";
506
+ if (normalized.includes("nitrogen") || normalized.includes("oxygen") || normalized.includes("air")) {
507
+ return "rgba(122, 194, 255, 0.75)";
508
+ }
509
+ if (normalized.includes("carbon") || normalized.includes("co2")) {
510
+ return "rgba(255, 198, 138, 0.75)";
511
+ }
512
+ if (normalized.includes("sulfur")) {
513
+ return "rgba(255, 233, 138, 0.75)";
514
+ }
515
+ return "rgba(180, 222, 255, 0.68)";
516
+ }
517
+ function resolveRenderPreset(preset) {
518
+ switch (preset) {
519
+ case "presentation":
520
+ return {
521
+ layers: {
522
+ background: true,
523
+ guides: true,
524
+ orbits: true,
525
+ objects: true,
526
+ labels: true,
527
+ structures: true,
528
+ metadata: true,
529
+ },
530
+ };
531
+ case "atlas-card":
532
+ return {
533
+ layers: {
534
+ background: true,
535
+ guides: false,
536
+ orbits: true,
537
+ objects: true,
538
+ labels: true,
539
+ structures: true,
540
+ metadata: true,
541
+ },
542
+ };
543
+ case "markdown":
544
+ return {
545
+ layers: {
546
+ background: true,
547
+ guides: false,
548
+ orbits: true,
549
+ objects: true,
550
+ labels: true,
551
+ structures: true,
552
+ metadata: false,
553
+ },
554
+ };
555
+ case "diagram":
556
+ default:
557
+ return {
558
+ layers: {
559
+ background: true,
560
+ guides: true,
561
+ orbits: true,
562
+ objects: true,
563
+ labels: true,
564
+ structures: true,
565
+ metadata: true,
566
+ },
567
+ };
568
+ }
569
+ }
570
+ function numericValue(value) {
571
+ if (typeof value === "number") {
572
+ return value;
573
+ }
574
+ if (value && typeof value === "object" && "value" in value) {
575
+ return value.value;
576
+ }
577
+ return null;
578
+ }
579
+ function estimateLabelHalfWidth(object, labelScale) {
580
+ const primaryWidth = object.label.length * 4.8 * labelScale + 18;
581
+ const secondaryWidth = object.secondaryLabel.length * 4.1 * labelScale + 18;
582
+ return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
583
+ }
584
+ function getObjectClipPathId(sceneObject) {
585
+ return `${sceneObject.renderId}-clip`;
586
+ }
587
+ function diamondPoints(x, y, radius) {
588
+ return `${x},${y - radius} ${x + radius},${y} ${x},${y + radius} ${x - radius},${y}`;
589
+ }
590
+ function phenomenonPoints(x, y, radius) {
591
+ return `${x},${y - radius * 1.2} ${x + radius * 0.9},${y - radius * 0.3} ${x + radius * 1.2},${y + radius * 0.8} ${x},${y + radius * 1.2} ${x - radius * 1.1},${y + radius * 0.3} ${x - radius * 0.8},${y - radius * 0.8}`;
592
+ }
593
+ function renderBackdrop(width, height) {
594
+ const verticalLines = Array.from({ length: 10 }, (_, index) => {
595
+ const x = 90 + index * ((width - 180) / 9);
596
+ return `<line class="wo-grid" x1="${x}" y1="120" x2="${x}" y2="${height - 70}" />`;
597
+ }).join("");
598
+ const horizontalLines = Array.from({ length: 6 }, (_, index) => {
599
+ const y = 150 + index * ((height - 240) / 5);
600
+ return `<line class="wo-grid" x1="56" y1="${y}" x2="${width - 56}" y2="${y}" />`;
601
+ }).join("");
602
+ return `${verticalLines}${horizontalLines}`;
603
+ }
604
+ function renderMetadata(scene) {
605
+ return [
606
+ `${scene.layoutPreset} layout`,
607
+ `${scene.viewMode} view`,
608
+ `${scene.renderPreset ?? "custom"} preset`,
609
+ `schema ${scene.metadata.version ?? "1.0"}`,
610
+ ].join(" - ");
611
+ }
612
+ function isStructureLike(object) {
613
+ return object.type === "structure" || object.type === "phenomenon";
614
+ }
615
+ function mixColors(left, right, amount) {
616
+ const leftRgb = parseColor(left);
617
+ const rightRgb = parseColor(right);
618
+ if (!leftRgb || !rightRgb) {
619
+ return undefined;
620
+ }
621
+ const clamped = Math.min(Math.max(amount, 0), 1);
622
+ return rgbToHex({
623
+ r: Math.round(leftRgb.r + (rightRgb.r - leftRgb.r) * clamped),
624
+ g: Math.round(leftRgb.g + (rightRgb.g - leftRgb.g) * clamped),
625
+ b: Math.round(leftRgb.b + (rightRgb.b - leftRgb.b) * clamped),
626
+ });
627
+ }
628
+ function rgbaString(color, alpha) {
629
+ const rgb = parseColor(color);
630
+ if (!rgb) {
631
+ return color;
632
+ }
633
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
634
+ }
635
+ function parseColor(value) {
636
+ const hex = value.trim();
637
+ if (/^#[0-9a-f]{6}$/i.test(hex)) {
638
+ return {
639
+ r: Number.parseInt(hex.slice(1, 3), 16),
640
+ g: Number.parseInt(hex.slice(3, 5), 16),
641
+ b: Number.parseInt(hex.slice(5, 7), 16),
642
+ };
643
+ }
644
+ if (/^#[0-9a-f]{3}$/i.test(hex)) {
645
+ return {
646
+ r: Number.parseInt(hex[1] + hex[1], 16),
647
+ g: Number.parseInt(hex[2] + hex[2], 16),
648
+ b: Number.parseInt(hex[3] + hex[3], 16),
649
+ };
650
+ }
651
+ return null;
652
+ }
653
+ function rgbToHex(color) {
654
+ const part = (value) => value.toString(16).padStart(2, "0");
655
+ return `#${part(color.r)}${part(color.g)}${part(color.b)}`;
656
+ }
657
+ function isColorLike(value) {
658
+ return Boolean(value.trim());
659
+ }
660
+ function escapeXml(value) {
661
+ return value
662
+ .replaceAll("&", "&amp;")
663
+ .replaceAll("<", "&lt;")
664
+ .replaceAll(">", "&gt;")
665
+ .replaceAll("\"", "&quot;")
666
+ .replaceAll("'", "&apos;");
667
+ }
668
+ function escapeAttribute(value) {
669
+ return escapeXml(value);
670
+ }