@world-engines/project-host 0.1.0-alpha.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 (218) hide show
  1. package/LICENSE +46 -0
  2. package/assets/gui/WELogo-title.svg +11 -0
  3. package/assets/gui/app.js +1326 -0
  4. package/assets/gui/index.html +118 -0
  5. package/assets/gui/local-preview-asset-urls.js +61 -0
  6. package/assets/gui/local-preview-bridge.js +393 -0
  7. package/assets/gui/spatial-template-panel.js +233 -0
  8. package/assets/gui/styles.css +115 -0
  9. package/assets/gui/terrain-render.js +120 -0
  10. package/assets/gui/workspace-status.css +17 -0
  11. package/assets/gui/workspace-status.js +255 -0
  12. package/assets/gui/world-map-globe.js +187 -0
  13. package/assets/gui/world-map-worker.js +62 -0
  14. package/assets/gui/world-map.css +30 -0
  15. package/assets/gui/world-map.js +2466 -0
  16. package/dist/account-model-proxy.d.ts +21 -0
  17. package/dist/account-model-proxy.js +470 -0
  18. package/dist/authoring-browser-runtime.d.ts +7 -0
  19. package/dist/authoring-browser-runtime.js +110 -0
  20. package/dist/authoring-ingestion.d.ts +38 -0
  21. package/dist/authoring-ingestion.js +410 -0
  22. package/dist/authoring-model-connection.d.ts +166 -0
  23. package/dist/authoring-model-connection.js +757 -0
  24. package/dist/chinese-lexical.d.ts +152 -0
  25. package/dist/chinese-lexical.js +555 -0
  26. package/dist/chromium-preview-browser-driver.d.ts +43 -0
  27. package/dist/chromium-preview-browser-driver.js +575 -0
  28. package/dist/cli/download.d.ts +12 -0
  29. package/dist/cli/download.js +50 -0
  30. package/dist/cli/dump.d.ts +32 -0
  31. package/dist/cli/dump.js +91 -0
  32. package/dist/cli/ingest.d.ts +49 -0
  33. package/dist/cli/ingest.js +225 -0
  34. package/dist/cli/review.d.ts +10 -0
  35. package/dist/cli/review.js +44 -0
  36. package/dist/cli/upload.d.ts +57 -0
  37. package/dist/cli/upload.js +112 -0
  38. package/dist/cli-command.d.ts +60 -0
  39. package/dist/cli-command.js +613 -0
  40. package/dist/cli.d.ts +2 -0
  41. package/dist/cli.js +182 -0
  42. package/dist/desktop-authoring-runtime.d.ts +88 -0
  43. package/dist/desktop-authoring-runtime.js +1602 -0
  44. package/dist/desktop-sidecar.d.ts +11 -0
  45. package/dist/desktop-sidecar.js +143 -0
  46. package/dist/document-dump.d.ts +113 -0
  47. package/dist/document-dump.js +431 -0
  48. package/dist/document-graph-extractor.d.ts +125 -0
  49. package/dist/document-graph-extractor.js +588 -0
  50. package/dist/document-ingestion-journal.d.ts +68 -0
  51. package/dist/document-ingestion-journal.js +471 -0
  52. package/dist/document-ingestion.d.ts +183 -0
  53. package/dist/document-ingestion.js +976 -0
  54. package/dist/document-query-path-proof.d.ts +30 -0
  55. package/dist/document-query-path-proof.js +34 -0
  56. package/dist/document-query-path-renderer.d.ts +50 -0
  57. package/dist/document-query-path-renderer.js +75 -0
  58. package/dist/document-query-proof-paths.d.ts +22 -0
  59. package/dist/document-query-proof-paths.js +71 -0
  60. package/dist/document-query.d.ts +97 -0
  61. package/dist/document-query.js +1106 -0
  62. package/dist/document-relation-role.d.ts +14 -0
  63. package/dist/document-relation-role.js +86 -0
  64. package/dist/document-source.d.ts +107 -0
  65. package/dist/document-source.js +255 -0
  66. package/dist/embedding-cache.d.ts +35 -0
  67. package/dist/embedding-cache.js +317 -0
  68. package/dist/frozen-review-local-pipeline.d.ts +88 -0
  69. package/dist/frozen-review-local-pipeline.js +609 -0
  70. package/dist/frozen-review-source-capture.d.ts +15 -0
  71. package/dist/frozen-review-source-capture.js +551 -0
  72. package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
  73. package/dist/frozen-review-wasm-encryptor.js +72 -0
  74. package/dist/frozen-review.d.ts +131 -0
  75. package/dist/frozen-review.js +802 -0
  76. package/dist/graph-traversal.d.ts +23 -0
  77. package/dist/graph-traversal.js +100 -0
  78. package/dist/gui/graph-layout-worker.js +1 -0
  79. package/dist/gui/graph-layout.wasm +0 -0
  80. package/dist/gui/graph.css +1 -0
  81. package/dist/gui/graph.js +76 -0
  82. package/dist/gui/plane-generation.js +112 -0
  83. package/dist/gui/plane-generator.js +310 -0
  84. package/dist/gui/plane-generator_bg.wasm +0 -0
  85. package/dist/gui/procedural-city.js +595 -0
  86. package/dist/gui/road-generation.js +129 -0
  87. package/dist/gui/road-parcels.js +1584 -0
  88. package/dist/gui/score.css +1 -0
  89. package/dist/gui/score.js +54 -0
  90. package/dist/gui/spatial-runtime.js +1231 -0
  91. package/dist/gui/spatial-templates.js +200 -0
  92. package/dist/gui/trigger.css +1 -0
  93. package/dist/gui/trigger.js +142 -0
  94. package/dist/harness-callback-transport.d.ts +17 -0
  95. package/dist/harness-callback-transport.js +128 -0
  96. package/dist/harness-document-ingestion.d.ts +77 -0
  97. package/dist/harness-document-ingestion.js +343 -0
  98. package/dist/index.d.ts +43 -0
  99. package/dist/index.js +43 -0
  100. package/dist/ingestion-source-reconciliation.d.ts +88 -0
  101. package/dist/ingestion-source-reconciliation.js +253 -0
  102. package/dist/ladybug-runtime.d.ts +117 -0
  103. package/dist/ladybug-runtime.js +886 -0
  104. package/dist/local-account-embedding.d.ts +34 -0
  105. package/dist/local-account-embedding.js +294 -0
  106. package/dist/local-gallery.d.ts +35 -0
  107. package/dist/local-gallery.js +227 -0
  108. package/dist/local-gui-auth.d.ts +96 -0
  109. package/dist/local-gui-auth.js +703 -0
  110. package/dist/local-gui-contract.d.ts +53 -0
  111. package/dist/local-gui-contract.js +5 -0
  112. package/dist/local-gui-image-assets.d.ts +17 -0
  113. package/dist/local-gui-image-assets.js +96 -0
  114. package/dist/local-gui-preview-client.d.ts +3 -0
  115. package/dist/local-gui-preview-client.js +111 -0
  116. package/dist/local-gui-score.d.ts +72 -0
  117. package/dist/local-gui-score.js +184 -0
  118. package/dist/local-gui-server.d.ts +10 -0
  119. package/dist/local-gui-server.js +987 -0
  120. package/dist/local-gui-version.d.ts +17 -0
  121. package/dist/local-gui-version.js +147 -0
  122. package/dist/local-gui-workspace.d.ts +14 -0
  123. package/dist/local-gui-workspace.js +171 -0
  124. package/dist/local-model-settings.d.ts +28 -0
  125. package/dist/local-model-settings.js +124 -0
  126. package/dist/local-preview-assets.d.ts +28 -0
  127. package/dist/local-preview-assets.js +124 -0
  128. package/dist/local-preview-bound-resources.d.ts +12 -0
  129. package/dist/local-preview-bound-resources.js +80 -0
  130. package/dist/local-preview-message-store.d.ts +63 -0
  131. package/dist/local-preview-message-store.js +214 -0
  132. package/dist/local-preview-player-read-models.d.ts +47 -0
  133. package/dist/local-preview-player-read-models.js +90 -0
  134. package/dist/local-preview-projections.d.ts +61 -0
  135. package/dist/local-preview-projections.js +117 -0
  136. package/dist/local-preview-runtime.d.ts +105 -0
  137. package/dist/local-preview-runtime.js +553 -0
  138. package/dist/local-preview-session.d.ts +73 -0
  139. package/dist/local-preview-session.js +395 -0
  140. package/dist/local-preview-source.d.ts +40 -0
  141. package/dist/local-preview-source.js +192 -0
  142. package/dist/local-preview-view-events.d.ts +101 -0
  143. package/dist/local-preview-view-events.js +179 -0
  144. package/dist/local-preview-view-state-sqlite.d.ts +24 -0
  145. package/dist/local-preview-view-state-sqlite.js +263 -0
  146. package/dist/local-preview-view-state.d.ts +93 -0
  147. package/dist/local-preview-view-state.js +108 -0
  148. package/dist/local-preview-vite-config.d.ts +6 -0
  149. package/dist/local-preview-vite-config.js +122 -0
  150. package/dist/local-preview-world-effects.d.ts +9 -0
  151. package/dist/local-preview-world-effects.js +55 -0
  152. package/dist/local-preview-worldline-store.d.ts +47 -0
  153. package/dist/local-preview-worldline-store.js +282 -0
  154. package/dist/local-preview-worldlines.d.ts +95 -0
  155. package/dist/local-preview-worldlines.js +136 -0
  156. package/dist/mcp-async-dispatcher.d.ts +3 -0
  157. package/dist/mcp-async-dispatcher.js +129 -0
  158. package/dist/mcp-line-queue.d.ts +14 -0
  159. package/dist/mcp-line-queue.js +184 -0
  160. package/dist/mcp-pipe-internal.d.ts +6 -0
  161. package/dist/mcp-pipe-internal.js +151 -0
  162. package/dist/mcp-pipe-response.d.ts +14 -0
  163. package/dist/mcp-pipe-response.js +50 -0
  164. package/dist/mcp-stdio-response.d.ts +12 -0
  165. package/dist/mcp-stdio-response.js +27 -0
  166. package/dist/mcp.d.ts +27 -0
  167. package/dist/mcp.js +194 -0
  168. package/dist/prepared-embedding-cache.d.ts +38 -0
  169. package/dist/prepared-embedding-cache.js +238 -0
  170. package/dist/preview-browser-adapter.d.ts +53 -0
  171. package/dist/preview-browser-adapter.js +171 -0
  172. package/dist/preview-runtime.d.ts +67 -0
  173. package/dist/preview-runtime.js +460 -0
  174. package/dist/project-auth-transport.d.ts +27 -0
  175. package/dist/project-auth-transport.js +121 -0
  176. package/dist/project-auth.d.ts +51 -0
  177. package/dist/project-auth.js +108 -0
  178. package/dist/project-git-capture-recovery.d.ts +16 -0
  179. package/dist/project-git-capture-recovery.js +347 -0
  180. package/dist/project-git-cli.d.ts +5 -0
  181. package/dist/project-git-cli.js +40 -0
  182. package/dist/project-git-scenario-source.d.ts +10 -0
  183. package/dist/project-git-scenario-source.js +240 -0
  184. package/dist/project-git-view-source.d.ts +13 -0
  185. package/dist/project-git-view-source.js +134 -0
  186. package/dist/project-git.d.ts +97 -0
  187. package/dist/project-git.js +656 -0
  188. package/dist/project-host.d.ts +59 -0
  189. package/dist/project-host.js +471 -0
  190. package/dist/project-writer-lease.d.ts +18 -0
  191. package/dist/project-writer-lease.js +430 -0
  192. package/dist/public-error.d.ts +7 -0
  193. package/dist/public-error.js +61 -0
  194. package/dist/retrieval-benchmark.d.ts +46 -0
  195. package/dist/retrieval-benchmark.js +58 -0
  196. package/dist/safe-project-path.d.ts +8 -0
  197. package/dist/safe-project-path.js +93 -0
  198. package/dist/scenario-writer.d.ts +32 -0
  199. package/dist/scenario-writer.js +191 -0
  200. package/dist/transfer/download.d.ts +159 -0
  201. package/dist/transfer/download.js +598 -0
  202. package/dist/transfer/transport.d.ts +26 -0
  203. package/dist/transfer/transport.js +1 -0
  204. package/dist/transfer/upload.d.ts +162 -0
  205. package/dist/transfer/upload.js +354 -0
  206. package/dist/transfer-types.d.ts +95 -0
  207. package/dist/transfer-types.js +1 -0
  208. package/dist/transfer.d.ts +18 -0
  209. package/dist/transfer.js +273 -0
  210. package/dist/trigger-runtime.d.ts +121 -0
  211. package/dist/trigger-runtime.js +153 -0
  212. package/dist/vendor/wasm-core/asset-hashes.json +5 -0
  213. package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
  214. package/dist/vendor/wasm-core/wasm_core.js +1 -0
  215. package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
  216. package/dist/view-watcher.d.ts +9 -0
  217. package/dist/view-watcher.js +34 -0
  218. package/package.json +110 -0
@@ -0,0 +1,255 @@
1
+ const SVG_NS = "http://www.w3.org/2000/svg";
2
+ const STYLE_MARKER = "data-workspace-status-styles";
3
+ const MAX_DETAIL_LENGTH = 240;
4
+
5
+ const SURFACES = {
6
+ graph: {
7
+ name: "世界图",
8
+ loadingTitle: "正在载入世界图",
9
+ loadingDescription: "正在整理人物、地点与关系,请稍候。",
10
+ },
11
+ map: {
12
+ name: "世界地图",
13
+ loadingTitle: "正在载入世界地图",
14
+ loadingDescription: "正在读取地形、水系与地图标记,请稍候。",
15
+ },
16
+ trigger: {
17
+ name: "触发器",
18
+ loadingTitle: "正在载入触发器",
19
+ loadingDescription: "正在准备触发条件与动作流程,请稍候。",
20
+ },
21
+ };
22
+
23
+ const ERROR_COPY = {
24
+ session: {
25
+ title: "连接已过期",
26
+ description: "当前编辑连接已失效。刷新连接后再继续,未提交的操作不会自动重放。",
27
+ retryLabel: "刷新连接",
28
+ },
29
+ network: {
30
+ title: "暂时无法连接",
31
+ description: "服务暂时不可达,请检查网络后重试。",
32
+ retryLabel: "重新连接",
33
+ },
34
+ conflict: {
35
+ title: "内容版本已更新",
36
+ description: "其他修改已先一步保存。重新读取最新内容后再继续。",
37
+ retryLabel: "重新读取",
38
+ },
39
+ validation: {
40
+ title: "内容需要检查",
41
+ description: "部分数据不符合当前规则。请返回检查后再次提交。",
42
+ retryLabel: "返回检查",
43
+ },
44
+ worker: {
45
+ title: "生成服务暂不可用",
46
+ description: "后台生成任务没有完成。当前内容仍会保留,可以稍后重试。",
47
+ retryLabel: "重新生成",
48
+ },
49
+ unknown: {
50
+ title: "暂时无法显示",
51
+ description: "遇到了未预期的问题。当前内容仍会保留,请重试。",
52
+ retryLabel: "重试",
53
+ },
54
+ };
55
+
56
+ const ILLUSTRATIONS = {
57
+ "graph-loading": [
58
+ ["path", { d: "M20 32 38 20l20 12 18-13 20 13M38 20v37m20-25v31m18-44v39" }],
59
+ ["circle", { cx: "20", cy: "32", r: "6" }],
60
+ ["circle", { cx: "38", cy: "20", r: "6" }],
61
+ ["circle", { cx: "58", cy: "32", r: "6" }],
62
+ ["circle", { cx: "76", cy: "19", r: "6" }],
63
+ ["circle", { cx: "96", cy: "32", r: "6" }],
64
+ ["circle", { cx: "38", cy: "57", r: "6" }],
65
+ ["circle", { cx: "58", cy: "63", r: "6" }],
66
+ ["circle", { cx: "76", cy: "58", r: "6" }],
67
+ ],
68
+ "map-loading": [
69
+ ["path", { d: "M13 65c13-10 19-5 29-16s17-27 30-25 15 19 35 36M14 72c18-8 31-5 45-17 15-13 28-15 47-5" }],
70
+ ["path", { d: "M31 51c9 0 13 5 19 13s10 11 18 12 15-4 20-13" }],
71
+ ["circle", { cx: "72", cy: "24", r: "4" }],
72
+ ],
73
+ "trigger-loading": [
74
+ ["rect", { x: "12", y: "25", width: "25", height: "18", rx: "4" }],
75
+ ["path", { d: "M37 34h15l8-10 16 20 9-10h21M60 24v-9m0 38v-9" }],
76
+ ["rect", { x: "84", y: "25", width: "24", height: "18", rx: "4" }],
77
+ ["circle", { cx: "60", cy: "15", r: "4" }],
78
+ ["circle", { cx: "60", cy: "53", r: "4" }],
79
+ ],
80
+ "session-lock": [
81
+ ["rect", { x: "34", y: "35", width: "52", height: "37", rx: "7" }],
82
+ ["path", { d: "M45 35v-9a15 15 0 0 1 30 0v9M60 49v10" }],
83
+ ["circle", { cx: "60", cy: "48", r: "3" }],
84
+ ["path", { d: "M21 55h9m60 0h9" }],
85
+ ],
86
+ "network-disconnected": [
87
+ ["path", { d: "M18 35h22l10 10m20 20 10 10h22M40 29v12m40 28v12M47 55H26m47 0h21" }],
88
+ ["circle", { cx: "20", cy: "55", r: "6" }],
89
+ ["circle", { cx: "100", cy: "55", r: "6" }],
90
+ ["path", { d: "m46 27 28 56" }],
91
+ ],
92
+ "revision-branch": [
93
+ ["path", { d: "M34 20v26a16 16 0 0 0 16 16h36M34 46a16 16 0 0 1 16-16h22" }],
94
+ ["circle", { cx: "34", cy: "20", r: "6" }],
95
+ ["circle", { cx: "72", cy: "30", r: "6" }],
96
+ ["circle", { cx: "86", cy: "62", r: "6" }],
97
+ ],
98
+ "document-warning": [
99
+ ["path", { d: "M33 15h37l17 17v52H33zM70 15v18h17" }],
100
+ ["path", { d: "M60 42v19" }],
101
+ ["circle", { cx: "60", cy: "72", r: "3" }],
102
+ ],
103
+ "terrain-worker": [
104
+ ["path", { d: "M12 76c15-18 24-24 35-13l12-20 21 25 12-11 16 19M76 23a10 10 0 1 1-20 0 10 10 0 0 1 20 0M66 8v5m0 20v5M51 23h5m20 0h5m-5-10-4 4m-12 12-4 4m20 0-4-4M60 17l-4-4" }],
105
+ ],
106
+ warning: [
107
+ ["path", { d: "M60 14 106 82H14z" }],
108
+ ["path", { d: "M60 38v21" }],
109
+ ["circle", { cx: "60", cy: "70", r: "3" }],
110
+ ],
111
+ };
112
+
113
+ function ensureStylesheet() {
114
+ if (typeof document === "undefined" || !document.head) return;
115
+ if (document.head.querySelector(`link[${STYLE_MARKER}="true"]`)) return;
116
+ const link = document.createElement("link");
117
+ link.rel = "stylesheet";
118
+ link.href = new URL("./workspace-status.css", import.meta.url).href;
119
+ link.setAttribute(STYLE_MARKER, "true");
120
+ document.head.append(link);
121
+ }
122
+
123
+ function element(tag, className, text) {
124
+ const node = document.createElement(tag);
125
+ if (className) node.className = className;
126
+ if (text !== undefined) node.textContent = text;
127
+ return node;
128
+ }
129
+
130
+ function illustration(name) {
131
+ const svg = document.createElementNS(SVG_NS, "svg");
132
+ svg.setAttribute("viewBox", "0 0 120 96");
133
+ svg.setAttribute("aria-hidden", "true");
134
+ svg.setAttribute("focusable", "false");
135
+ svg.setAttribute("data-illustration", name);
136
+ for (const [tag, attributes] of ILLUSTRATIONS[name] ?? ILLUSTRATIONS.warning) {
137
+ const child = document.createElementNS(SVG_NS, tag);
138
+ for (const [key, value] of Object.entries(attributes)) child.setAttribute(key, value);
139
+ svg.append(child);
140
+ }
141
+ return svg;
142
+ }
143
+
144
+ function errorFields(error) {
145
+ if (typeof error === "string") return { code: "", message: error };
146
+ if (!error || typeof error !== "object") return { code: "", message: "" };
147
+ return {
148
+ code: typeof error.code === "string" ? error.code : "",
149
+ message: typeof error.message === "string" ? error.message : "",
150
+ };
151
+ }
152
+
153
+ function classifyError(error) {
154
+ const { code, message } = errorFields(error);
155
+ const signal = `${code} ${message}`.toUpperCase();
156
+ if (/GUI_SESSION_REQUIRED|SESSION_REQUIRED|UNAUTHORIZED|AUTH(?:ENTICATION)?|会话.{0,8}(过期|失效)|登录.{0,8}(过期|失效)/u.test(signal)) return "session";
157
+ if (/NETWORK|OFFLINE|ECONN|FETCH|SOCKET|网络|连接失败|无法连接/u.test(signal)) return "network";
158
+ if (/STALE|REVISION|CONFLICT|\bCAS\b|版本.{0,8}(变化|冲突|更新)|冲突/u.test(signal)) return "conflict";
159
+ if (/VALIDATION|SCHEMA|INVALID|校验|验证|格式|字段/u.test(signal)) return "validation";
160
+ if (/GENERATOR|WORKER|WASM|生成器|后台生成|生成失败/u.test(signal)) return "worker";
161
+ return "unknown";
162
+ }
163
+
164
+ function safeCode(value) {
165
+ return /^[A-Z][A-Z0-9_.:-]{0,79}$/u.test(value) ? value : "";
166
+ }
167
+
168
+ function safeMessage(value) {
169
+ if (!value) return "";
170
+ let message = String(value).split(/\r?\n/u, 1)[0];
171
+ message = message
172
+ .replace(/(["'](?:authorization|token|access[_-]?token|refresh[_-]?token|api[_-]?key|secret|password|credential)s?["']\s*:\s*["'])(?:bearer\s+)?[^"']*(["'])/giu, "$1[已隐藏]$2")
173
+ .replace(/(\b(?:authorization|token|access[_-]?token|refresh[_-]?token|api[_-]?key|secret|password|credential)s?\b\s*[:=]\s*)(?:bearer\s+)?[^\s,;]+/giu, "$1[已隐藏]")
174
+ .replace(/(\bbearer\s+)[^\s,;]+/giu, "$1[已隐藏]")
175
+ .replace(/([?&](?:token|access_token|refresh_token|api_key|key|secret|password)=)[^&#\s]+/giu, "$1[已隐藏]");
176
+ return message.length > MAX_DETAIL_LENGTH ? `${message.slice(0, MAX_DETAIL_LENGTH - 1)}…` : message;
177
+ }
178
+
179
+ function technicalDetails(error) {
180
+ const fields = errorFields(error);
181
+ const code = safeCode(fields.code);
182
+ const message = safeMessage(fields.message);
183
+ if (!code && !message) return null;
184
+ const details = element("details", "workspace-status__details");
185
+ details.append(element("summary", "", "技术详情"));
186
+ const content = element("div", "workspace-status__details-content");
187
+ if (code) content.append(element("code", "workspace-status__code", code));
188
+ if (message) content.append(element("p", "workspace-status__message", message));
189
+ details.append(content);
190
+ return details;
191
+ }
192
+
193
+ function loadingView(surface) {
194
+ const copy = SURFACES[surface];
195
+ const root = element("section", "workspace-status workspace-status--loading");
196
+ root.dataset.surface = surface;
197
+ root.setAttribute("role", "status");
198
+ root.setAttribute("aria-live", "polite");
199
+ root.append(illustration(`${surface}-loading`));
200
+ root.append(element("h2", "workspace-status__title", copy.loadingTitle));
201
+ root.append(element("p", "workspace-status__description", copy.loadingDescription));
202
+ return root;
203
+ }
204
+
205
+ function errorView(surface, error, onRetry, retryLabel) {
206
+ const kind = classifyError(error);
207
+ const copy = ERROR_COPY[kind];
208
+ const illustrationName = {
209
+ session: "session-lock",
210
+ network: "network-disconnected",
211
+ conflict: "revision-branch",
212
+ validation: "document-warning",
213
+ worker: "terrain-worker",
214
+ unknown: "warning",
215
+ }[kind];
216
+ const root = element("section", "workspace-status workspace-status--error");
217
+ root.dataset.surface = surface;
218
+ root.dataset.errorKind = kind;
219
+ root.setAttribute("role", "alert");
220
+ root.append(illustration(illustrationName));
221
+ root.append(element("h2", "workspace-status__title", copy.title));
222
+ const description = kind === "unknown"
223
+ ? `${SURFACES[surface].name}暂时无法显示。请重试,未提交的操作不会自动执行。`
224
+ : copy.description;
225
+ root.append(element("p", "workspace-status__description", description));
226
+ const details = technicalDetails(error);
227
+ if (details) root.append(details);
228
+ const button = element("button", "button primary workspace-status__action", kind === "session" ? copy.retryLabel : (retryLabel || copy.retryLabel));
229
+ button.type = "button";
230
+ if (kind === "session") {
231
+ button.addEventListener("click", () => globalThis.location?.reload());
232
+ } else if (typeof onRetry === "function") {
233
+ button.addEventListener("click", () => onRetry());
234
+ } else {
235
+ button.disabled = true;
236
+ }
237
+ root.append(button);
238
+ return root;
239
+ }
240
+
241
+ /**
242
+ * 在世界图、世界地图或触发器工作区渲染统一的载入/错误状态。
243
+ * 会话错误只允许用户主动刷新页面;其他恢复动作由调用方提供 onRetry。
244
+ */
245
+ export function renderWorkspaceStatus(target, options = {}) {
246
+ if (!target || typeof target.replaceChildren !== "function") throw new TypeError("target 必须是可替换子节点的 DOM 元素");
247
+ const state = options.state === "error" ? "error" : "loading";
248
+ const surface = Object.hasOwn(SURFACES, options.surface) ? options.surface : "graph";
249
+ ensureStylesheet();
250
+ const view = state === "error"
251
+ ? errorView(surface, options.error, options.onRetry, options.retryLabel)
252
+ : loadingView(surface);
253
+ target.replaceChildren(view);
254
+ return view;
255
+ }
@@ -0,0 +1,187 @@
1
+ const TAU = Math.PI * 2;
2
+ const BASE_RADIUS_RATIO = 0.46;
3
+ const MAX_RENDER_EDGE = 600;
4
+ const renderSurfaces = new WeakMap();
5
+
6
+ function finite(value, name) {
7
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} 必须是有限数值`);
8
+ return value;
9
+ }
10
+
11
+ function geometry(viewport, view) {
12
+ const width = finite(viewport?.width, "viewport.width");
13
+ const height = finite(viewport?.height, "viewport.height");
14
+ const yaw = finite(view?.yaw, "view.yaw");
15
+ const pitch = finite(view?.pitch, "view.pitch");
16
+ const zoom = finite(view?.zoom, "view.zoom");
17
+ if (width <= 0 || height <= 0) throw new RangeError("viewport 尺寸必须大于零");
18
+ if (zoom <= 0) throw new RangeError("view.zoom 必须大于零");
19
+ return {
20
+ width,
21
+ height,
22
+ centerX: width / 2,
23
+ centerY: height / 2,
24
+ radius: Math.min(width, height) * BASE_RADIUS_RATIO * zoom,
25
+ sinYaw: Math.sin(yaw),
26
+ cosYaw: Math.cos(yaw),
27
+ sinPitch: Math.sin(pitch),
28
+ cosPitch: Math.cos(pitch),
29
+ };
30
+ }
31
+
32
+ function normalizedU(value) {
33
+ const result = value - Math.floor(value);
34
+ return result === 1 ? 0 : result;
35
+ }
36
+
37
+ /**
38
+ * 把等距圆柱纹理坐标投影为 CSS 像素坐标。
39
+ * yaw 与 pitch 的单位为弧度;u=0/1 是国际日期变更线,v=0 是北极。
40
+ */
41
+ export function projectGlobe(u, v, viewport, view) {
42
+ finite(u, "u");
43
+ finite(v, "v");
44
+ const g = geometry(viewport, view);
45
+ const longitude = (normalizedU(u) - 0.5) * TAU;
46
+ const latitude = (0.5 - v) * Math.PI;
47
+ const cosLatitude = Math.cos(latitude);
48
+ const worldX = Math.sin(longitude) * cosLatitude;
49
+ const worldY = Math.sin(latitude);
50
+ const worldZ = Math.cos(longitude) * cosLatitude;
51
+
52
+ const cameraX = g.cosYaw * worldX - g.sinYaw * worldZ;
53
+ const yawZ = g.sinYaw * worldX + g.cosYaw * worldZ;
54
+ const cameraY = g.cosPitch * worldY - g.sinPitch * yawZ;
55
+ const cameraZ = g.sinPitch * worldY + g.cosPitch * yawZ;
56
+ return {
57
+ x: g.centerX + cameraX * g.radius,
58
+ y: g.centerY - cameraY * g.radius,
59
+ visible: cameraZ >= 0,
60
+ };
61
+ }
62
+
63
+ /** 把 CSS 像素坐标反投影为球面经纬度;球外与背半球没有命中。 */
64
+ export function pickGlobe(x, y, viewport, view) {
65
+ finite(x, "x");
66
+ finite(y, "y");
67
+ const g = geometry(viewport, view);
68
+ const cameraX = (x - g.centerX) / g.radius;
69
+ const cameraY = (g.centerY - y) / g.radius;
70
+ const radialSquared = cameraX * cameraX + cameraY * cameraY;
71
+ if (radialSquared > 1 + 1e-12) return null;
72
+
73
+ // 正交投影的屏幕点只取朝向相机的半球,因此不会点中背面。
74
+ const cameraZ = Math.sqrt(Math.max(0, 1 - radialSquared));
75
+ const worldY = g.cosPitch * cameraY + g.sinPitch * cameraZ;
76
+ const yawZ = -g.sinPitch * cameraY + g.cosPitch * cameraZ;
77
+ const worldX = g.cosYaw * cameraX + g.sinYaw * yawZ;
78
+ const worldZ = -g.sinYaw * cameraX + g.cosYaw * yawZ;
79
+ let longitudeRadians = Math.atan2(worldX, worldZ);
80
+ if (longitudeRadians >= Math.PI) longitudeRadians -= TAU;
81
+ const latitudeRadians = Math.asin(Math.max(-1, Math.min(1, worldY)));
82
+ const u = normalizedU(longitudeRadians / TAU + 0.5);
83
+ const v = Math.max(0, Math.min(1, 0.5 - latitudeRadians / Math.PI));
84
+ return {
85
+ u,
86
+ v,
87
+ longitude: longitudeRadians * 180 / Math.PI,
88
+ latitude: latitudeRadians * 180 / Math.PI,
89
+ };
90
+ }
91
+
92
+ function createSurface(ctx, width, height) {
93
+ let surface = renderSurfaces.get(ctx);
94
+ if (!surface) {
95
+ const ownerDocument = ctx.canvas?.ownerDocument ?? globalThis.document;
96
+ const canvas = typeof OffscreenCanvas === "function"
97
+ ? new OffscreenCanvas(width, height)
98
+ : ownerDocument?.createElement?.("canvas");
99
+ const surfaceContext = canvas?.getContext?.("2d", { alpha: true });
100
+ if (!canvas || !surfaceContext) throw new Error("浏览器不支持球面地图离屏画布");
101
+ surface = { canvas, context: surfaceContext, image: null };
102
+ renderSurfaces.set(ctx, surface);
103
+ }
104
+ if (surface.canvas.width !== width) surface.canvas.width = width;
105
+ if (surface.canvas.height !== height) surface.canvas.height = height;
106
+ if (!surface.image || surface.image.width !== width || surface.image.height !== height) {
107
+ surface.image = surface.context.createImageData(width, height);
108
+ }
109
+ return surface;
110
+ }
111
+
112
+ function validateTexture(texture) {
113
+ const width = finite(texture?.width, "texture.width");
114
+ const height = finite(texture?.height, "texture.height");
115
+ if (!Number.isInteger(width) || !Number.isInteger(height) || width <= 0 || height <= 0) throw new RangeError("texture 尺寸无效");
116
+ if (!(texture.data instanceof Uint8ClampedArray) || texture.data.length !== width * height * 4) {
117
+ throw new TypeError("texture.data 必须是匹配尺寸的 Uint8ClampedArray RGBA 数据");
118
+ }
119
+ return { data: texture.data, width, height };
120
+ }
121
+
122
+ /**
123
+ * 用正交球面投影绘制 RGBA 等距圆柱纹理。球外保持透明,调用方的 CSS 背景会自然透出。
124
+ */
125
+ export function drawGlobe(ctx, texture, viewport, view) {
126
+ if (!ctx || typeof ctx.drawImage !== "function") throw new TypeError("ctx 必须是 CanvasRenderingContext2D");
127
+ const source = validateTexture(texture);
128
+ const target = geometry(viewport, view);
129
+ const renderScale = Math.min(1, MAX_RENDER_EDGE / Math.max(target.width, target.height));
130
+ const renderWidth = Math.max(1, Math.round(target.width * renderScale));
131
+ const renderHeight = Math.max(1, Math.round(target.height * renderScale));
132
+ const surface = createSurface(ctx, renderWidth, renderHeight);
133
+ const image = surface.image;
134
+ const pixels = image.data;
135
+ pixels.fill(0);
136
+
137
+ const centerX = renderWidth / 2;
138
+ const centerY = renderHeight / 2;
139
+ const radius = target.radius * renderScale;
140
+ const inverseRadius = 1 / radius;
141
+ const minX = Math.max(0, Math.floor(centerX - radius));
142
+ const maxX = Math.min(renderWidth - 1, Math.ceil(centerX + radius));
143
+ const minY = Math.max(0, Math.floor(centerY - radius));
144
+ const maxY = Math.min(renderHeight - 1, Math.ceil(centerY + radius));
145
+ const lightX = -0.38;
146
+ const lightY = 0.5;
147
+ const lightZ = 0.78;
148
+
149
+ for (let outputY = minY; outputY <= maxY; outputY += 1) {
150
+ const cameraY = (centerY - outputY - 0.5) * inverseRadius;
151
+ for (let outputX = minX; outputX <= maxX; outputX += 1) {
152
+ const cameraX = (outputX + 0.5 - centerX) * inverseRadius;
153
+ const radialSquared = cameraX * cameraX + cameraY * cameraY;
154
+ if (radialSquared > 1) continue;
155
+ const cameraZ = Math.sqrt(1 - radialSquared);
156
+
157
+ const worldY = target.cosPitch * cameraY + target.sinPitch * cameraZ;
158
+ const yawZ = -target.sinPitch * cameraY + target.cosPitch * cameraZ;
159
+ const worldX = target.cosYaw * cameraX + target.sinYaw * yawZ;
160
+ const worldZ = -target.sinYaw * cameraX + target.cosYaw * yawZ;
161
+ const longitude = Math.atan2(worldX, worldZ);
162
+ const latitude = Math.asin(Math.max(-1, Math.min(1, worldY)));
163
+ const u = normalizedU(longitude / TAU + 0.5);
164
+ const v = Math.max(0, Math.min(1, 0.5 - latitude / Math.PI));
165
+ const sourceX = Math.min(source.width - 1, Math.floor(u * source.width));
166
+ const sourceY = Math.min(source.height - 1, Math.floor(v * source.height));
167
+ const sourceOffset = (sourceY * source.width + sourceX) * 4;
168
+ const outputOffset = (outputY * renderWidth + outputX) * 4;
169
+
170
+ // 固定在相机空间的日光产生真实球面明暗,同时保留纹理原本的生物群系色彩。
171
+ const diffuse = Math.max(0, cameraX * lightX + cameraY * lightY + cameraZ * lightZ);
172
+ const shade = 0.44 + diffuse * 0.56;
173
+ pixels[outputOffset] = Math.round(source.data[sourceOffset] * shade);
174
+ pixels[outputOffset + 1] = Math.round(source.data[sourceOffset + 1] * shade);
175
+ pixels[outputOffset + 2] = Math.round(source.data[sourceOffset + 2] * shade);
176
+ pixels[outputOffset + 3] = source.data[sourceOffset + 3];
177
+ }
178
+ }
179
+
180
+ surface.context.putImageData(image, 0, 0);
181
+ ctx.clearRect?.(0, 0, target.width, target.height);
182
+ ctx.save?.();
183
+ ctx.imageSmoothingEnabled = true;
184
+ ctx.drawImage(surface.canvas, 0, 0, renderWidth, renderHeight, 0, 0, target.width, target.height);
185
+ ctx.restore?.();
186
+ }
187
+
@@ -0,0 +1,62 @@
1
+ import init, { generate_plane_json, generate_world_roads_json, sample_plane_tile_json } from "./plane-generator.js";
2
+ import { validatePlaneGeneration } from "./plane-generation.js";
3
+ import { validateRoadGeneration } from "./road-generation.js";
4
+ import {
5
+ PROCEDURAL_GENERATOR_IDENTITY,
6
+ PROCEDURAL_WORLD_PROFILE,
7
+ validateProceduralWorld,
8
+ } from "./procedural-city.js";
9
+
10
+ let wasmReady;
11
+
12
+ async function loadWasm() {
13
+ if (!wasmReady) {
14
+ wasmReady = init({ module_or_path: new URL("./plane-generator_bg.wasm", import.meta.url) });
15
+ }
16
+ await wasmReady;
17
+ }
18
+
19
+ self.addEventListener("message", async (event) => {
20
+ const id = event.data?.id;
21
+ if (!Number.isSafeInteger(id) || id < 1) return;
22
+ const kind = event.data?.kind;
23
+ const epoch = event.data?.epoch;
24
+ try {
25
+ const settings = validatePlaneGeneration(event.data?.settings);
26
+ if (kind === "roads") {
27
+ if (!Number.isSafeInteger(epoch) || epoch < 0) throw new TypeError("道路请求 epoch 无效");
28
+ const roadGeneration = validateRoadGeneration(event.data?.roadGeneration);
29
+ const world = validateProceduralWorld({
30
+ profile: PROCEDURAL_WORLD_PROFILE,
31
+ identity: PROCEDURAL_GENERATOR_IDENTITY,
32
+ worldSeed: settings.seed,
33
+ cities: event.data?.cities,
34
+ }, { widthMeters: settings.widthMeters, heightMeters: settings.heightMeters });
35
+ await loadWasm();
36
+ const result = JSON.parse(generate_world_roads_json(JSON.stringify({
37
+ settings,
38
+ roadGeneration,
39
+ cities: world.cities,
40
+ })));
41
+ self.postMessage({ id, epoch, kind: "roads", result });
42
+ return;
43
+ }
44
+ await loadWasm();
45
+ if (kind === "tile") {
46
+ const tile = event.data?.tile;
47
+ if (!tile || typeof tile !== "object" || !Number.isInteger(tile.level) || tile.level < 0 || tile.level > 12
48
+ || !Number.isInteger(tile.x) || tile.x < 0 || tile.x >= 2 ** tile.level
49
+ || !Number.isInteger(tile.y) || tile.y < 0 || tile.y >= 2 ** tile.level || tile.size !== 128) {
50
+ throw new TypeError("地图细节区块参数无效");
51
+ }
52
+ const result = JSON.parse(sample_plane_tile_json(JSON.stringify({ settings, tile })));
53
+ self.postMessage({ id, kind: "tile", result });
54
+ return;
55
+ }
56
+ const result = JSON.parse(generate_plane_json(JSON.stringify(settings)));
57
+ self.postMessage({ id, kind: "base", result });
58
+ } catch (error) {
59
+ const responseKind = kind === "roads" || kind === "tile" ? kind : "base";
60
+ self.postMessage({ id, ...(responseKind === "roads" ? { epoch } : {}), kind: responseKind, error: error instanceof Error ? error.message : "地图生成失败" });
61
+ }
62
+ });
@@ -0,0 +1,30 @@
1
+ body.world-map-active main{height:calc(100vh - 65px);max-width:none;margin:0;padding:0;overflow:hidden}body.world-map-active .world-map-view.active{display:block;height:100%;min-height:0;text-align:initial}.world-map-view>#world-map-root{display:block;height:100%;justify-items:stretch;gap:0;color:inherit}#world-map-root>.world-map-shell{display:grid;height:100%}
2
+ .world-map-shell{height:100%;min-height:520px;display:grid;grid-template-rows:58px minmax(0,1fr);overflow:hidden;background:var(--bg);color:var(--text)}
3
+ .world-map-header{display:flex;align-items:center;gap:18px;padding:0 18px;border-bottom:1px solid var(--line-soft);background:var(--surface)}
4
+ .world-map-heading{min-width:0}.world-map-heading h1{margin:0;font-size:1rem}.world-map-status{margin:2px 0 0;color:var(--muted);font-size:.72rem}.world-map-shell.is-dirty .world-map-status{color:var(--amber)}
5
+ .world-map-header-actions{display:flex;align-items:center;gap:8px;margin-left:auto}.world-map-icon-button{flex:0 0 auto}.world-map-stage{position:relative;min-height:0;overflow:hidden;background:var(--bg)}
6
+ #world-map-root .world-map-icon-button svg{width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
7
+ .world-map-canvas{display:block;width:100%;height:100%;min-height:420px;outline:none;cursor:grab;touch-action:none}.world-map-canvas.is-panning{cursor:grabbing}.world-map-canvas:focus-visible{box-shadow:inset 0 0 0 2px var(--accent)}
8
+ .world-map-phase{position:absolute;z-index:3;top:14px;left:50%;transform:translateX(-50%);padding:6px 10px;border:1px solid var(--line-soft);border-radius:999px;background:rgba(32,31,32,.9);color:var(--muted);font-size:.65rem;letter-spacing:.02em;pointer-events:none}
9
+ .world-map-overlay{position:absolute;z-index:5;inset:50% auto auto 50%;transform:translate(-50%,-50%);display:grid;justify-items:center;gap:10px;max-width:min(460px,calc(100% - 40px));padding:14px 18px;border:1px solid var(--line);border-radius:var(--radius);background:rgba(32,31,32,.94);color:var(--muted);font-size:.8rem;text-align:center;box-shadow:var(--shadow);pointer-events:none}.world-map-overlay p{margin:0;white-space:pre-line}.world-map-overlay.has-action{pointer-events:auto}.world-map-overlay.error{border-color:#693b40;color:#ff959a}.world-map-overlay.error .button{color:var(--text)}
10
+ .world-map-preview-watermark{position:absolute;z-index:3;right:14px;top:14px;padding:6px 10px;border:1px solid var(--line-soft);border-radius:999px;background:rgba(32,31,32,.78);color:var(--muted);font-size:.62rem;pointer-events:none}.world-map-shell.is-dirty .world-map-preview-watermark{border-color:var(--amber);color:var(--amber)}
11
+ .world-map-controls{position:absolute;z-index:4;top:14px;left:14px;display:flex;gap:7px;padding:6px;border:1px solid var(--line-soft);border-radius:999px;background:rgba(32,31,32,.92);box-shadow:var(--shadow)}
12
+ .world-map-settings{position:absolute;z-index:4;top:14px;right:14px;width:min(310px,calc(100% - 28px));max-height:calc(100% - 94px);overflow:auto;padding:16px;border:1px solid var(--line);border-radius:var(--radius);background:rgba(32,31,32,.97);box-shadow:var(--shadow)}
13
+ .world-map-settings-heading{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:10px}.world-map-settings-heading h2,.world-map-settings h3{margin:0;font-size:.88rem}.world-map-settings-heading small{color:var(--muted);font-size:.65rem}.world-map-wizard-progress{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin-bottom:13px}.world-map-wizard-progress span{padding:6px 8px;border:1px solid var(--line-soft);border-radius:6px;color:var(--muted);font-size:.64rem;text-align:center}.world-map-wizard-progress span.is-active{border-color:var(--accent);color:var(--text);background:rgba(50,213,81,.08)}.world-map-wizard-progress span.is-complete{color:var(--accent)}.world-map-form{display:grid;grid-template-columns:1fr 1fr;gap:10px}.world-map-field{display:grid;gap:4px;min-width:0}.world-map-field-label{display:flex;justify-content:space-between;gap:7px;color:var(--muted);font-size:.68rem}.world-map-field-label small{font-size:inherit}.world-map-field input,.world-map-field select{min-width:0;width:100%;padding:7px 8px;border:1px solid var(--line);border-radius:6px;background:var(--bg);color:var(--text);font-size:.75rem;outline:none}.world-map-field input:focus,.world-map-field select:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(50,213,81,.14)}
14
+ .world-map-settings h3{margin:17px 0 8px;color:var(--muted);font-size:.72rem}.world-map-layers{display:grid;grid-template-columns:1fr 1fr;gap:8px}.world-map-layer{display:flex;align-items:center;gap:7px;color:var(--text);font-size:.72rem;cursor:pointer}.world-map-layer input{width:14px;height:14px;margin:0;accent-color:var(--accent)}
15
+ .world-map-road-hint,.world-map-road-status{margin:0;color:var(--muted);font-size:.68rem;line-height:1.45}.world-map-road-form{display:grid;grid-template-columns:1fr 1fr;gap:10px}.world-map-road-form>.world-map-layer{min-height:28px;padding:0 2px}.world-map-road-actions{display:flex;gap:8px;margin-top:10px}.world-map-road-actions .button{flex:1}.world-map-road-status{min-height:2.9em;margin-top:8px}.world-map-road-status[data-state="loading"]{color:#ffd879}.world-map-road-status[data-state="error"]{color:#ff959a}.world-map-road-status[data-state="ready"]{color:#9ad6a6}
16
+ .world-map-road-parcel-error{margin-top:8px;padding:7px 8px;border:1px solid #693b40;border-radius:6px;background:rgba(105,59,64,.14);color:#ffb0b4;font-size:.67rem}.world-map-road-parcel-error summary{cursor:pointer}.world-map-road-parcel-error pre{max-height:130px;margin:7px 0 0;overflow:auto;white-space:pre-wrap;word-break:break-word;color:#ffd8da;font:inherit}
17
+ .world-map-stats{position:absolute;z-index:3;left:14px;bottom:14px;display:flex;flex-wrap:wrap;gap:0;margin:0;max-width:calc(100% - 28px);border:1px solid var(--line-soft);border-radius:var(--radius);background:rgba(32,31,32,.92);box-shadow:var(--shadow)}.world-map-stat{min-width:92px;padding:8px 11px;border-right:1px solid var(--line-soft)}.world-map-stat:last-child{border-right:0}.world-map-stat dt{color:var(--muted);font-size:.62rem}.world-map-stat dd{margin:1px 0 0;font:650 .7rem/1.35 ui-monospace,SFMono-Regular,Consolas,monospace;white-space:nowrap}
18
+ .world-map-legend{position:absolute;z-index:3;right:14px;bottom:14px;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px 10px;max-width:360px;padding:8px 10px;border:1px solid var(--line-soft);border-radius:var(--radius);background:rgba(32,31,32,.92);color:var(--muted);font-size:.62rem}.world-map-legend span{display:flex;align-items:center;gap:4px}.world-map-legend i{width:9px;height:9px;border-radius:2px;background:#316b3b}.world-map-legend i.sea{background:#0e405b}.world-map-legend i.high{background:#987d62}.world-map-legend i.river{background:#1c79a7}.world-map-legend i.lake{background:#166783}.world-map-legend i.object{border:2px solid #f2f2f2;border-radius:50%;background:var(--accent)}
19
+ .world-map-city-legend{position:absolute;z-index:3;right:14px;bottom:58px;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px 10px;max-width:360px;padding:8px 10px;border:1px solid var(--line-soft);border-radius:var(--radius);background:rgba(32,31,32,.92);color:var(--muted);font-size:.62rem}.world-map-city-legend[hidden]{display:none}.world-map-city-legend span{display:flex;align-items:center;gap:4px}.world-map-city-legend i{width:9px;height:9px;border-radius:2px}
20
+ .world-map-road-legend{position:absolute;z-index:3;right:14px;bottom:102px;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px 10px;max-width:360px;padding:8px 10px;border:1px solid var(--line-soft);border-radius:var(--radius);background:rgba(32,31,32,.92);color:var(--muted);font-size:.62rem}.world-map-road-legend[hidden]{display:none}.world-map-road-legend span{display:flex;align-items:center;gap:4px}.world-map-road-legend i{width:14px;height:0;border-top:2px solid #f2f2f2}.world-map-road-legend i.arterial{border-color:#e0a85d}.world-map-road-legend i.local{border-color:rgba(242,242,242,.75)}.world-map-road-legend i.bridge{border-color:#e9c46a;border-top-style:dashed}.world-map-road-legend i.tunnel{border-color:#b592f5;border-top-style:dashed}
21
+ .world-map-shell:not(.has-map-preview) .world-map-stats,.world-map-shell:not(.has-map-preview) .world-map-legend,.world-map-shell:not(.has-map-preview) .world-map-controls{display:none}
22
+ @media(min-width:851px){.world-map-shell[data-phase="parameters"] .world-map-overlay{inset:50% 338px auto 14px;transform:translateY(-50%);max-width:none}}
23
+ @media(max-width:850px){body.world-map-active main{height:calc(100vh - 121px)}.world-map-header{padding:0 10px}.world-map-heading{display:none}.world-map-header-actions{width:100%;margin:0}.world-map-header-actions .button{flex:1}.world-map-settings{top:64px;max-height:calc(100% - 142px)}.world-map-phase{display:none}.world-map-preview-watermark{top:10px;right:10px}.world-map-stats{right:14px;overflow:auto;flex-wrap:nowrap}.world-map-stat{min-width:88px}.world-map-legend,.world-map-city-legend{display:none}}
24
+ @media(prefers-reduced-motion:reduce){.world-map-canvas{scroll-behavior:auto}}
25
+ .world-map-overlay.is-workspace-status{padding:0;border:0;background:transparent;box-shadow:none;width:min(500px,calc(100% - 36px));pointer-events:auto}.world-map-overlay.is-workspace-status .workspace-status{min-height:0;padding:28px 24px;border:1px solid var(--line-soft);border-radius:var(--radius);background:var(--surface)}
26
+ .world-map-detail-status{position:absolute;z-index:3;top:68px;left:14px;padding:5px 9px;border:1px solid var(--line-soft);border-radius:999px;background:rgba(32,31,32,.88);color:var(--muted);font:600 .62rem/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;pointer-events:none}.world-map-detail-status[data-state="loading"]{border-color:#746229;color:#ffd879}.world-map-detail-status[data-state="error"]{border-color:#693b40;color:#ff959a}.world-map-detail-status[data-state="ready"]{color:#9ad6a6}.world-map-shell:not(.has-map-preview) .world-map-detail-status{display:none}
27
+ .world-map-detail-controls{display:grid;grid-template-columns:1fr 1fr;gap:10px}.world-map-camera-field{display:grid;gap:4px;padding:7px 8px;border:1px solid var(--line);border-radius:6px;background:var(--bg)}.world-map-camera-field>span{color:var(--muted);font-size:.68rem}.world-map-camera-field output{font:650 .72rem/1.35 ui-monospace,SFMono-Regular,Consolas,monospace}
28
+ .world-map-create-hint{position:absolute;z-index:3;left:14px;bottom:76px;padding:5px 9px;border:1px solid var(--line-soft);border-radius:999px;background:rgba(32,31,32,.82);color:var(--muted);font-size:.62rem;pointer-events:none}.world-map-shell:not(.has-map-preview) .world-map-create-hint{display:none}
29
+ .world-map-city-dialog{position:absolute;z-index:4;top:14px;right:338px;width:min(560px,calc(100% - 366px));max-height:calc(100% - 94px);margin:0;overflow:auto;padding:0;border:1px solid var(--line);border-radius:var(--radius);background:rgba(32,31,32,.97);color:var(--text);box-shadow:var(--shadow)}.world-map-city-dialog::backdrop{background:rgba(0,0,0,.58)}.world-map-city-form{display:grid;gap:14px;padding:18px}.world-map-city-form h2,.world-map-city-form h3{margin:0}.world-map-city-form h2{font-size:1rem}.world-map-city-form h3{font-size:.76rem;color:var(--muted)}.world-map-city-grid{display:grid;grid-template-columns:2fr 1fr 1fr;gap:10px}.world-map-city-grid.land-use{grid-template-columns:repeat(5,1fr)}.world-map-city-form .world-map-field input,.world-map-city-form .world-map-field select{min-width:0;width:100%;padding:7px 8px;border:1px solid var(--line);border-radius:6px;background:var(--bg);color:var(--text);font-size:.75rem;outline:none}.world-map-city-dialog-status{min-height:1.2em;margin:0;color:var(--muted);font-size:.72rem}.world-map-city-dialog-status[data-state="loading"]{color:#ffd879}.world-map-city-dialog-status[data-state="error"]{color:#ff959a}.world-map-city-actions{display:flex;justify-content:flex-end;gap:8px}
30
+ @media(max-width:850px){.world-map-detail-status{top:58px;left:10px}.world-map-create-hint{display:none}.world-map-road-legend{display:none}.world-map-city-dialog{top:64px;right:14px;width:calc(100% - 28px);max-height:calc(100% - 142px)}.world-map-detail-controls,.world-map-city-grid,.world-map-city-grid.land-use{grid-template-columns:1fr 1fr}}