@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,233 @@
1
+ const KIND_META = Object.freeze({
2
+ city: { label: "城市模板", applyLabel: "设为城市预设", description: "在地图上右键新建城市时,可选择此预设。" },
3
+ parcel: { label: "地块模板", applyLabel: "套用到地块生成", description: "套用会更新地块生成配置;不会创建建筑或室内。" },
4
+ building: { label: "建筑模板", applyLabel: "套用到建筑生成", description: "套用会更新建筑生成配置;不会创建室内。" },
5
+ interior: { label: "室内模板", applyLabel: "套用到室内生成", description: "套用会更新室内生成配置。" },
6
+ });
7
+
8
+ const KINDS = Object.freeze(Object.keys(KIND_META));
9
+
10
+ function element(tag, className, text) {
11
+ const node = document.createElement(tag);
12
+ if (className) node.className = className;
13
+ if (text !== undefined) node.textContent = text;
14
+ return node;
15
+ }
16
+
17
+ function asObject(value) {
18
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
19
+ }
20
+
21
+ function presetsFor(presets, kind) {
22
+ return (Array.isArray(presets) ? presets : []).filter((preset) => preset && preset.kind === kind
23
+ && typeof preset.id === "string" && typeof preset.revision === "number" && preset.parameters && typeof preset.parameters === "object");
24
+ }
25
+
26
+ function presetValue(preset) { return `${preset.id}@${preset.revision}`; }
27
+
28
+ function selectionFor(configuration, kind, presets) {
29
+ const selected = asObject(asObject(configuration)[kind]);
30
+ return presets.find((preset) => preset.id === selected.presetId && preset.revision === selected.revision) ?? presets[0] ?? null;
31
+ }
32
+
33
+ function frozenSelection(preset) {
34
+ return Object.freeze({ presetId: preset.id, revision: preset.revision, parameters: preset.parameters });
35
+ }
36
+
37
+ function appendParameterEditor(root, parameters, onDirty) {
38
+ const grid = element("div", "spatial-template-parameters");
39
+ const controls = new Map();
40
+ const visit = (value, path = []) => {
41
+ if (typeof value === "number") {
42
+ const name = path.join(".");
43
+ const label = element("label", "spatial-template-field");
44
+ label.append(element("span", "", name));
45
+ const input = document.createElement("input"); input.name = name; input.type = "number"; input.value = String(value); input.step = "any";
46
+ input.addEventListener("input", onDirty);
47
+ label.append(input); grid.append(label); controls.set(name, input);
48
+ return;
49
+ }
50
+ if (value && typeof value === "object" && !Array.isArray(value)) for (const [name, child] of Object.entries(value)) visit(child, [...path, name]);
51
+ };
52
+ visit(parameters);
53
+ root.append(grid);
54
+ return () => {
55
+ const next = structuredClone(parameters);
56
+ for (const [path, input] of controls) {
57
+ const value = Number(input.value); if (!Number.isFinite(value)) throw new Error(`${path} 必须是数字。`);
58
+ const keys = path.split("."); let target = next;
59
+ for (const key of keys.slice(0, -1)) target = target[key];
60
+ target[keys.at(-1)] = value;
61
+ }
62
+ return next;
63
+ };
64
+ }
65
+
66
+ /** 权威 preset 及参数 schema 均由 Host 注入;此模块不推导默认参数。 */
67
+ export function mountSpatialTemplatePanel(root, options) {
68
+ let activeKind = KINDS.includes(options.initialKind) ? options.initialKind : "city";
69
+ let library = Array.isArray(options.presets) ? options.presets : [];
70
+ let configuration = asObject(options.configuration);
71
+ const chosenIds = {};
72
+ let busy = false;
73
+ let dirty = false;
74
+
75
+ const shell = element("section", "spatial-template-panel");
76
+ shell.setAttribute("aria-label", "空间模板");
77
+ const heading = element("header", "spatial-template-heading");
78
+ const titleBlock = element("div");
79
+ titleBlock.append(element("p", "spatial-template-kicker", "WORLD MAP"), element("h1", "", "空间模板"));
80
+ const status = element("p", "spatial-template-status", "正在读取预设…");
81
+ status.setAttribute("role", "status");
82
+ heading.append(titleBlock, status);
83
+ const tabs = element("div", "spatial-template-tabs");
84
+ tabs.setAttribute("role", "tablist");
85
+ const content = element("div", "spatial-template-content");
86
+ const confirmation = element("section", "spatial-template-confirmation");
87
+ confirmation.hidden = true;
88
+ confirmation.setAttribute("role", "alertdialog");
89
+ confirmation.setAttribute("aria-modal", "true");
90
+ const confirmationCopy = element("p", "");
91
+ const confirmationActions = element("div", "spatial-template-actions");
92
+ const confirmationCancel = element("button", "button ghost", "取消"); confirmationCancel.type = "button";
93
+ const confirmationApply = element("button", "button primary", "确认套用"); confirmationApply.type = "button";
94
+ confirmationActions.append(confirmationCancel, confirmationApply); confirmation.append(confirmationCopy, confirmationActions);
95
+ shell.append(heading, tabs, content, confirmation);
96
+ root.replaceChildren(shell);
97
+
98
+ function setStatus(message, state = "") {
99
+ status.textContent = message;
100
+ if (state) status.dataset.state = state;
101
+ else delete status.dataset.state;
102
+ }
103
+
104
+ function impactCopy(kind) {
105
+ if (kind === "city") return "确认将此模板设为后续新建城市的默认预填。不会批量修改或创建现有城市。";
106
+ const label = KIND_META[kind].label.replace("模板", "");
107
+ return `确认套用将影响后续 ${label} 生成;已有布局的编辑保存可能不再兼容。不会物化其他类别。`;
108
+ }
109
+
110
+ function render() {
111
+ tabs.replaceChildren();
112
+ for (const kind of KINDS) {
113
+ const button = element("button", `spatial-template-tab${kind === activeKind ? " active" : ""}`, KIND_META[kind].label);
114
+ button.type = "button";
115
+ button.dataset.templateKind = kind;
116
+ button.setAttribute("role", "tab");
117
+ button.setAttribute("aria-selected", String(kind === activeKind));
118
+ button.addEventListener("click", () => {
119
+ activeKind = kind;
120
+ options.onKindChange?.(kind);
121
+ render();
122
+ });
123
+ tabs.append(button);
124
+ }
125
+
126
+ content.replaceChildren();
127
+ const meta = KIND_META[activeKind];
128
+ const presets = presetsFor(library, activeKind);
129
+ const selected = presets.find((preset) => presetValue(preset) === chosenIds[activeKind]) ?? selectionFor(configuration, activeKind, presets);
130
+ const section = element("section", "spatial-template-card");
131
+ section.append(element("h2", "", meta.label), element("p", "spatial-template-description", meta.description));
132
+ if (!selected) {
133
+ section.append(element("p", "spatial-template-empty", "当前没有可用的权威预设。"));
134
+ content.append(section);
135
+ setStatus("没有可用的权威预设。", "error");
136
+ return;
137
+ }
138
+ const label = element("label", "spatial-template-field");
139
+ label.append(element("span", "", "预设"));
140
+ const select = document.createElement("select");
141
+ select.name = `${activeKind}Template`;
142
+ for (const preset of presets) {
143
+ const option = element("option", "", preset.label || preset.id);
144
+ option.value = presetValue(preset);
145
+ option.selected = preset.id === selected.id && preset.revision === selected.revision;
146
+ select.append(option);
147
+ }
148
+ label.append(select);
149
+ const templateName = element("label", "spatial-template-field");
150
+ templateName.append(element("span", "", "模板名称"));
151
+ const nameInput = document.createElement("input"); nameInput.name = "templateLabel"; nameInput.value = selected.label || selected.id;
152
+ templateName.append(nameInput);
153
+ const markDirty = () => {
154
+ dirty = true;
155
+ confirmation.hidden = true;
156
+ apply.disabled = true;
157
+ setStatus("模板定义已修改,请先保存新修订后再套用。", "");
158
+ };
159
+ nameInput.addEventListener("input", markDirty);
160
+ select.addEventListener("change", () => {
161
+ const next = presets.find((preset) => presetValue(preset) === select.value);
162
+ if (next) { dirty = false; chosenIds[activeKind] = presetValue(next); if (activeKind === "city") options.onCityTemplateSelected?.(next); render(); }
163
+ });
164
+ const revision = element("p", "spatial-template-revision", `预设修订 ${selected.revision}`);
165
+ const fixed = element("p", "spatial-template-fixed", "编辑会创建自定义模板修订;套用前仍由 Host 严格校验。对象参数保持原样,不在浏览器内猜测字段。");
166
+ const readParameters = appendParameterEditor(section, selected.parameters, markDirty);
167
+ const actions = element("div", "spatial-template-actions");
168
+ const save = element("button", "button ghost", String(selected.id).startsWith("custom:") ? "保存新修订" : "另存为自定义模板");
169
+ save.type = "button";
170
+ const apply = element("button", "button primary", meta.applyLabel);
171
+ apply.type = "button";
172
+ const setBusy = (next) => { busy = next; save.disabled = next; apply.disabled = next || dirty; select.disabled = next; };
173
+ const currentPreset = () => presets.find((preset) => presetValue(preset) === select.value) ?? selected;
174
+ save.addEventListener("click", async () => {
175
+ const preset = currentPreset();
176
+ setBusy(true); setStatus("正在保存模板定义…", "loading");
177
+ try {
178
+ const definition = { ...preset, label: nameInput.value.trim(), parameters: readParameters() };
179
+ const next = await options.onSaveDefinition?.(activeKind, definition);
180
+ if (!next || typeof next !== "object") throw new Error("模板定义未获得读回确认。");
181
+ if (Array.isArray(next.presets)) {
182
+ library = next.presets;
183
+ const saved = presetsFor(library, activeKind).filter((candidate) => candidate.label === definition.label
184
+ && JSON.stringify(candidate.parameters) === JSON.stringify(definition.parameters))
185
+ .sort((left, right) => right.revision - left.revision)[0];
186
+ if (saved) chosenIds[activeKind] = presetValue(saved);
187
+ }
188
+ dirty = false;
189
+ render(); setStatus("模板定义已保存并读回。", "ready");
190
+ } catch (error) {
191
+ setStatus(error instanceof Error ? error.message : String(error || "保存预设失败"), "error");
192
+ } finally { setBusy(false); }
193
+ });
194
+ apply.addEventListener("click", () => {
195
+ if (dirty) { setStatus("模板定义已修改,请先保存新修订后再套用。", ""); return; }
196
+ confirmationCopy.textContent = impactCopy(activeKind);
197
+ confirmation.hidden = false;
198
+ confirmationApply.focus();
199
+ });
200
+ confirmationCancel.onclick = () => {
201
+ confirmation.hidden = true;
202
+ setStatus("已取消套用,未写入项目。", "");
203
+ };
204
+ confirmationApply.onclick = async () => {
205
+ const preset = currentPreset();
206
+ confirmation.hidden = true;
207
+ setBusy(true); setStatus("正在套用模板…", "loading");
208
+ try {
209
+ const next = await options.onApply?.(activeKind, frozenSelection(preset));
210
+ if (!next || typeof next !== "object") throw new Error("模板套用未获得读回确认。");
211
+ configuration = asObject(next.configuration ?? next.templates ?? next);
212
+ setStatus("模板已套用并读回。", "ready"); render();
213
+ } catch (error) {
214
+ setStatus(error instanceof Error ? error.message : String(error || "套用模板失败"), "error");
215
+ } finally { setBusy(false); }
216
+ };
217
+ actions.append(save, apply);
218
+ section.append(label, templateName, revision, fixed, actions);
219
+ content.append(section);
220
+ if (!busy) setStatus("选择权威预设后保存或套用。", "");
221
+ }
222
+
223
+ render();
224
+ return {
225
+ update(next = {}) {
226
+ if (Array.isArray(next.presets)) library = next.presets;
227
+ if (next.configuration && typeof next.configuration === "object") configuration = next.configuration;
228
+ if (KINDS.includes(next.activeKind)) activeKind = next.activeKind;
229
+ render();
230
+ },
231
+ destroy() { root.replaceChildren(); },
232
+ };
233
+ }
@@ -0,0 +1,115 @@
1
+ :root{color-scheme:dark;--bg:#161516;--sidebar:#1a191a;--surface:#201f20;--surface-2:#272627;--surface-3:#2e2d2e;--line:#414041;--line-soft:#343334;--text:#f2f2f2;--muted:#aaa7aa;--muted-2:#817f81;--accent:#32d551;--accent-hover:#48df66;--accent-soft:#213626;--blue:#78a9ff;--red:#ff6b72;--amber:#e8b65c;--radius:8px;--shadow:0 12px 28px rgba(0,0,0,.22);font:16px/1.5 Inter,ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI","Microsoft YaHei",sans-serif}
2
+ .graph-panel-tabs{position:sticky;top:72px;z-index:1;display:grid;grid-template-columns:repeat(4,1fr);border-bottom:1px solid var(--line-soft);background:var(--surface);padding:0 12px}.graph-panel-tabs button{border:0;border-bottom:2px solid transparent;background:transparent;color:var(--muted);padding:10px 4px;cursor:pointer;font-size:.72rem}.graph-panel-tabs button.active{border-bottom-color:var(--accent);color:var(--text)}.graph-property-heading{display:flex;align-items:center;justify-content:space-between}.graph-property-heading button,.graph-property-editor button,.graph-value-tree>button,.graph-value-branch>button{border:1px solid var(--line);background:#272727;color:var(--muted);border-radius:5px;padding:5px 8px;cursor:pointer;font-size:.72rem}.graph-property-editor{display:grid;gap:8px;padding:10px 0;border-bottom:1px solid var(--line-soft)}.graph-property-editor>div:first-child{display:grid;grid-template-columns:1fr auto;gap:7px}.graph-property-editor textarea{min-height:74px;font:12px/1.5 ui-monospace,monospace}.graph-property-metadata{display:grid;grid-template-columns:1fr 1fr;gap:6px}.graph-property-metadata input,.graph-property-metadata select{min-width:0;padding:7px;font-size:.72rem}.graph-capability-note{color:var(--muted);font-size:.72rem;line-height:1.55}.graph-value-tree{display:grid;gap:6px;padding-left:10px;border-left:1px solid #343434}.graph-value-branch{display:grid;grid-template-columns:1fr auto;gap:5px}.graph-value-branch>span{font:12px ui-monospace,monospace;color:var(--muted)}.graph-value-branch>.graph-value-tree{grid-column:1/-1}
3
+ .graph-side-panel{position:absolute;z-index:8;top:58px;right:0;bottom:0;width:min(390px,92vw);overflow:auto;border-left:1px solid var(--line);background:var(--surface);box-shadow:-12px 0 30px rgba(0,0,0,.24)}.graph-side-panel>header{position:sticky;top:0;z-index:1;display:flex;align-items:flex-start;justify-content:space-between;padding:18px 20px 15px;border-bottom:1px solid var(--line-soft);background:var(--surface)}.graph-side-panel h2{font-size:1.08rem;margin:2px 0 0}.graph-panel-kicker{margin:0;color:var(--accent);font-size:.72rem}.graph-side-panel .graph-close{width:32px;height:32px;font-size:0;line-height:1;padding:6px}.graph-side-panel .graph-close svg{width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round}.graph-side-panel footer{display:flex;align-items:center;gap:8px;padding:14px 20px;border-top:1px solid var(--line-soft)}.graph-side-panel footer span{flex:1}.graph-property-list,.graph-relation-group{border-top:1px solid var(--line-soft);padding-top:13px}.graph-property-list h3,.graph-relation-group h3{margin:0 0 9px;font-size:.8rem;color:var(--muted)}.graph-property-list>div{display:grid;grid-template-columns:90px 1fr;gap:12px;margin:7px 0}.graph-relation-empty{color:var(--muted-2);font-size:.78rem}.graph-relation-row{display:grid;grid-template-columns:1fr auto;gap:6px;margin:6px 0}.graph-relation-row button,.graph-edge-endpoints button{border:1px solid var(--line);border-radius:5px;background:#272727;color:var(--text);padding:8px;text-align:left;cursor:pointer;font-size:.76rem}.graph-relation-row button:hover,.graph-edge-endpoints button:hover{border-color:var(--accent)}.graph-relation-node{color:var(--accent)!important}.graph-edge-endpoints{display:grid!important;grid-template-columns:1fr 1fr!important;gap:7px!important}
4
+ .graph-fab-row{display:flex;align-items:center;gap:7px}.graph-fab{display:inline-grid!important;place-items:center;width:34px!important;height:34px!important;min-width:34px;border:1px solid #474747!important;border-radius:50%!important;padding:7px!important;background:transparent!important;color:var(--text)!important;cursor:pointer}.graph-fab svg{width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.graph-fab:hover,.graph-fab:focus-visible{border-color:var(--accent)!important;color:var(--accent)!important;outline:none}.graph-fab.save{border-color:var(--accent)!important;color:var(--accent)!important}.graph-fab.danger:hover,.graph-fab.danger:focus-visible{border-color:var(--danger)!important;color:var(--danger)!important}.graph-fab:disabled{opacity:.38;cursor:not-allowed}.graph-relation-node{align-self:center}
5
+ .graph-friendly-form input[type="checkbox"]{width:auto;min-width:0;margin:0}.graph-friendly-form label>span:has(input[type="checkbox"]){display:inline-flex;align-items:center;gap:7px;min-height:36px}
6
+ .graph-alias-field{position:relative;display:grid;grid-template-columns:1fr auto;gap:7px}.graph-alias-field>span{grid-column:1/-1;color:var(--muted);font-size:.8rem}.graph-alias-field.wide{grid-column:1/-1}.graph-alias-field>.graph-fab{align-self:end}.graph-alias-row{display:grid;grid-template-columns:1fr auto;gap:7px;margin-bottom:7px}.graph-switch-field{display:flex!important;align-items:center;justify-content:space-between;min-height:42px}.graph-switch-field>span{color:var(--text)!important}.graph-switch-field input[role="switch"]{appearance:none;width:42px!important;height:24px;border:1px solid var(--line);border-radius:999px;background:var(--surface-3);position:relative;cursor:pointer;transition:background-color .16s,border-color .16s}.graph-switch-field input[role="switch"]::after{content:"";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:var(--muted);transition:transform .16s,background-color .16s}.graph-switch-field input[role="switch"]:checked{border-color:var(--accent);background:var(--accent-soft)}.graph-switch-field input[role="switch"]:checked::after{transform:translateX(18px);background:var(--accent)}.graph-switch-field input[role="switch"]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
7
+ .graph-switch-field>span small{display:block;color:var(--muted-2);font-size:.68rem;font-weight:400}.graph-switch-unavailable{opacity:.72}.graph-switch-field input[role="switch"]:disabled{cursor:not-allowed}
8
+ .world-map-view{min-height:calc(100vh - 130px);place-items:center;text-align:center}main .world-map-view.active{display:grid}.world-map-view>div{display:grid;justify-items:center;gap:10px;color:var(--muted)}.world-map-view svg{width:54px;height:54px;fill:none;stroke:var(--accent);stroke-width:1.4;stroke-linecap:round;stroke-linejoin:round}.world-map-view h1{margin:0;color:var(--text);font-size:1.15rem}.world-map-view p{margin:0;font-size:.9rem}
9
+ body.graph-active main{height:calc(100vh - 65px);max-width:none;margin:0;padding:0;overflow:hidden}.graph-workspace-view{position:relative;height:100%;min-height:480px;background:var(--bg)}.graph-toolbar{height:58px;padding:0 18px;border-bottom:1px solid var(--line-soft);display:flex;align-items:center;gap:22px;background:#1b1b1b}.graph-title{display:flex;align-items:baseline;gap:10px}.graph-title h1{font-size:1rem;margin:0}.graph-title span{font-size:.75rem;color:var(--muted)}.graph-tabs{display:flex;align-items:center;gap:2px;border:1px solid #353535;border-radius:6px;padding:3px}.graph-tab{border:0;border-radius:4px;background:transparent;color:var(--muted);padding:6px 12px;cursor:pointer;font-size:.78rem}.graph-tab.active{background:#303030;color:var(--text)}.graph-actions{display:flex;gap:8px;margin-left:auto}.graph-stage{height:calc(100% - 58px);position:relative}.graph-stage .graph-canvas{height:100%;border:0;background:var(--bg)}.graph-close{border:0;background:transparent;color:var(--muted);font-size:1.45rem;line-height:1;cursor:pointer;padding:3px 7px}.graph-close:hover{color:var(--text)}.graph-detail-content{padding:18px 22px;display:grid;gap:13px}.graph-detail-content>div{display:grid;grid-template-columns:90px 1fr;gap:14px}.graph-detail-content span{color:var(--muted);font-size:.8rem}.graph-detail-content p{margin:0;font-size:.9rem;white-space:pre-wrap}.graph-detail-advanced,.graph-advanced{border-top:1px solid var(--line-soft);margin-top:5px;padding-top:12px}.graph-detail-advanced summary,.graph-advanced summary{color:var(--muted);cursor:pointer;font-size:.8rem}.graph-detail-advanced pre{white-space:pre-wrap;overflow:auto;color:#bbb;font:12px/1.5 ui-monospace,monospace}.graph-friendly-form{padding:19px 22px 0}.graph-friendly-form #entity-fields,.graph-friendly-form #relation-fields{display:grid;grid-template-columns:1fr 1fr;gap:14px}.graph-friendly-form label{display:grid;gap:6px}.graph-friendly-form label.wide{grid-column:1/-1}.graph-friendly-form label>span{font-size:.8rem;color:var(--muted)}.graph-friendly-form select{width:100%;border:1px solid var(--line);background:var(--bg);color:var(--text);border-radius:6px;padding:9px 10px}.graph-friendly-form textarea{min-height:90px}.graph-friendly-form .graph-advanced{margin-top:16px}.graph-friendly-form .graph-advanced label{margin-top:12px}.graph-friendly-form .graph-advanced textarea{min-height:120px;font:12px/1.5 ui-monospace,monospace}.graph-friendly-form footer{margin:18px -22px 0;justify-content:flex-end}@media(max-width:700px){body.graph-active main{height:calc(100vh - 121px)}.graph-toolbar{padding:0 10px;gap:8px}.graph-title span{display:none}.graph-tabs{margin-left:auto}.graph-actions .button.ghost{display:none}.graph-side-panel{width:84vw}.graph-friendly-form #entity-fields,.graph-friendly-form #relation-fields{grid-template-columns:1fr}.graph-friendly-form label.wide{grid-column:auto}}
10
+ *{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;background:var(--bg);color:var(--text);min-width:320px}button,input,textarea{font:inherit}button{color:inherit}.app-shell{min-height:100vh;display:grid;grid-template-columns:228px minmax(0,1fr)}
11
+ .project-health>p{margin:0;color:var(--muted);font-size:.84rem;line-height:1.7}.preview-panel{min-height:500px;display:grid;grid-template-rows:minmax(460px,1fr) auto;background:#1d1d1d;overflow:hidden}.preview-panel iframe{display:block;width:100%;height:100%;min-height:460px;border:0;background:#fff}.preview-status{color:var(--muted);font-size:.8rem;line-height:1.65}
12
+ .sidebar{position:sticky;top:0;height:100vh;padding:22px 14px 16px;background:var(--sidebar);border-right:1px solid var(--line);display:flex;flex-direction:column}.brand{display:flex;align-items:center;padding:0 6px 24px}.brand-logo{display:block;width:100%;height:auto;aspect-ratio:876/300;object-fit:contain}nav{display:grid;gap:4px}
13
+ .nav-item{border:1px solid transparent;background:transparent;color:#aaa;text-align:left;border-radius:6px;padding:10px 12px;cursor:pointer;display:flex;gap:12px;align-items:center;font-size:.9rem}.nav-icon{width:18px;height:18px;flex:0 0 18px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;color:#858585}.nav-item:hover{background:var(--surface-2);color:var(--text)}.nav-item.active{background:var(--accent-soft);border-color:#315c38;color:#fff;box-shadow:inset 2px 0 var(--accent)}.nav-item.active .nav-icon{color:var(--accent)}.nav-group{display:grid;gap:3px}.nav-group-toggle .nav-chevron{width:16px;height:16px;margin-left:auto;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;transition:transform .16s}.nav-group-toggle[aria-expanded="true"] .nav-chevron{transform:rotate(90deg)}.nav-subitems{display:grid;gap:2px;padding:1px 0 3px 42px}.nav-subitem{border:0;border-radius:5px;background:transparent;color:var(--muted);padding:6px 8px;text-align:left;cursor:pointer;font-size:.76rem}.nav-subitem:hover,.nav-subitem.active{background:var(--surface-2);color:var(--text)}.nav-subitem.active{color:var(--accent)}
14
+ .sidebar-foot{margin-top:auto;padding:13px 9px 2px;border-top:1px solid var(--line-soft);color:var(--muted);font-size:.75rem;display:grid;gap:8px}.sidebar-connection{display:flex;align-items:center;justify-content:center;text-align:center;gap:8px}.pulse-dot{width:8px;height:8px;border-radius:50%;background:var(--amber)}.pulse-dot.online{background:var(--accent)}.pulse-dot.offline{background:var(--red)}.version-status{min-width:0;border:1px solid transparent;border-radius:999px;background:transparent;color:var(--muted-2);padding:4px 8px;display:flex;align-items:center;justify-content:center;gap:5px;font-size:.65rem}.version-status strong{overflow:hidden;color:var(--muted);font:inherit;text-overflow:ellipsis;white-space:nowrap}.version-status small{color:var(--accent);font-size:inherit}.version-status.is-update-available{border-color:var(--accent);background:var(--accent-soft);color:var(--text);cursor:pointer;animation:version-pulse 2.2s ease-in-out infinite}.version-status.is-update-available strong{color:var(--accent)}.version-status:disabled{cursor:default}@keyframes version-pulse{0%,100%{box-shadow:0 0 0 0 rgba(50,213,81,0)}50%{box-shadow:0 0 0 4px rgba(50,213,81,.14)}}@media(prefers-reduced-motion:reduce){.version-status.is-update-available{animation:none}}.workspace{min-width:0}.topbar{height:65px;position:sticky;top:0;z-index:5;background:#1b1b1b;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;padding:0 28px}
15
+ .identity,.top-actions,.button-row{display:flex;align-items:center;gap:10px}.identity strong{font-size:.94rem}.identity code,.revision{display:none}.connection{font-size:.76rem;color:var(--muted);display:flex;align-items:center;gap:7px}.connection i{width:7px;height:7px;border-radius:50%;background:var(--amber)}.connection.online i{background:var(--accent)}.connection.offline i{background:var(--red)}
16
+ main{padding:34px clamp(20px,3.5vw,52px) 64px;max-width:1700px;margin:auto}.view{display:none}.view.active{display:block}.view-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:26px}.eyebrow{margin:0 0 5px;color:var(--accent);font-size:.68rem;letter-spacing:.15em;font-weight:750}.view-heading h1{font-size:1.75rem;line-height:1.2;margin:0 0 7px;letter-spacing:-.025em}.view-heading p:not(.eyebrow){color:var(--muted);margin:0;font-size:.88rem}
17
+ .button{border:1px solid #4a4a4a;background:#292929;color:#eee;border-radius:6px;padding:9px 14px;cursor:pointer;font-size:.82rem;font-weight:650;white-space:nowrap}.button:hover:not(:disabled){background:#333;border-color:#666}.button:focus-visible,.nav-item:focus-visible,.action-card:focus-visible,.tab:focus-visible,.link-button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.button.primary{background:var(--accent);color:#0e2712;border-color:var(--accent)}.button.primary:hover:not(:disabled){background:var(--accent-hover);border-color:var(--accent-hover)}.button.ghost{background:transparent;border-color:var(--line)}.button.danger{background:transparent;border-color:#693b40;color:#ff959a}.button.compact{padding:6px 10px;font-size:.76rem}.button:disabled{opacity:.42;cursor:not-allowed}
18
+ .metric-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-bottom:16px}.metric{background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius);padding:17px 18px}.metric small,.metric strong{display:block}.metric small{color:var(--muted);font-size:.75rem}.metric strong{font:650 1.5rem/1.35 ui-monospace,SFMono-Regular,Consolas,monospace;margin-top:5px}.metric em{font-style:normal;color:var(--accent);font-size:.7rem}.two-column{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px}.panel{background:var(--surface);border:1px solid var(--line-soft);border-radius:var(--radius);box-shadow:var(--shadow);padding:19px}.panel-title{display:flex;justify-content:space-between;align-items:flex-start;gap:15px;margin-bottom:17px}.panel-title h2{font-size:.96rem;margin:0 0 3px}.panel-title p{font-size:.74rem;color:var(--muted);margin:0}.tag{font:700 .66rem/1 ui-monospace,monospace;letter-spacing:.06em;padding:6px 8px;color:var(--accent);background:var(--accent-soft);border:1px solid #315c38;border-radius:4px}
19
+ .detail-list{display:grid;grid-template-columns:1fr 1fr;margin:0;gap:15px 24px}.detail-list div{min-width:0}.detail-list dt{font-size:.7rem;color:var(--muted)}.detail-list dd{margin:3px 0 0;font-size:.82rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.action-list{display:grid;gap:9px}.action-card{display:grid;grid-template-columns:34px 1fr 18px;align-items:center;gap:11px;width:100%;border:1px solid var(--line-soft);background:var(--surface-2);border-radius:7px;text-align:left;padding:10px;cursor:pointer}.action-card:hover{background:var(--surface-3);border-color:#505050}.action-card strong,.action-card small{display:block}.action-card strong{font-size:.8rem}.action-card small{font-size:.69rem;color:var(--muted)}.action-icon{width:32px;height:32px;border:1px solid #3f6645;border-radius:6px;background:#263328;display:grid;place-items:center;color:var(--accent)}.action-icon svg{width:17px;height:17px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}.action-arrow{width:16px;height:16px;fill:none;stroke:currentColor;color:var(--muted);stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}.action-card:hover .action-arrow{color:var(--accent)}
20
+ .event-panel{box-shadow:none}.link-button{background:transparent;border:0;color:var(--accent);font-size:.74rem;cursor:pointer}.event-log{display:grid;gap:7px;max-height:210px;overflow:auto}.event-item{display:grid;grid-template-columns:72px 110px 1fr;gap:12px;border-top:1px solid var(--line-soft);padding-top:8px;font-size:.74rem}.event-item time{color:var(--muted-2)}.event-item b{color:var(--blue)}.event-item.error b{color:var(--red)}.empty-copy{color:var(--muted);font-size:.8rem}
21
+ .graph-layout{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:15px}.graph-panel{padding:0;overflow:hidden;min-height:510px}.tab-row{height:48px;border-bottom:1px solid var(--line-soft);display:flex;align-items:center;padding:0 14px;gap:5px}.tab-row>span{margin-left:auto;color:var(--muted);font-size:.72rem}.tab{border:0;background:transparent;padding:8px 11px;color:var(--muted);cursor:pointer;font-size:.77rem}.tab.active{color:var(--accent);background:var(--accent-soft);border-radius:5px}.graph-canvas{height:460px;overflow:hidden;background:var(--bg);border-top:1px solid #202020}#spatial-canvas>svg{width:100%;height:100%}#spatial-canvas .graph-edge{stroke:#555;stroke-width:1.25}#spatial-canvas .graph-edge-label{fill:#8b8b8b;font-size:10px}#spatial-canvas .graph-node{cursor:pointer}#spatial-canvas .graph-node circle{fill:#292929;stroke:#777;stroke-width:1.5}#spatial-canvas .graph-node.selected circle{fill:#29422d;stroke:var(--accent);stroke-width:2.5}#spatial-canvas .graph-node text{fill:#e5e5e5;font-size:11px;pointer-events:none}#spatial-canvas .graph-node .kind{fill:#999;font-size:9px}.inspector{box-shadow:none}.segmented{background:var(--bg);border:1px solid var(--line-soft);border-radius:6px;padding:3px;display:grid;grid-template-columns:1fr 1fr;margin-bottom:15px}.segmented button{border:0;background:transparent;color:var(--muted);padding:6px;border-radius:4px;font-size:.75rem;cursor:pointer}.segmented button.active{background:var(--surface-3);color:var(--text)}
22
+ .stack-form{display:grid;gap:12px}.stack-form label,.effect-fields label,.code-label{display:grid;gap:5px}.stack-form label span,.effect-fields label span,.code-label>span{font-size:.71rem;color:var(--muted)}input,textarea{width:100%;border:1px solid var(--line);background:var(--bg);color:var(--text);border-radius:6px;padding:9px 10px;outline:none}input:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(50,215,75,.15)}textarea{resize:vertical}.stack-form textarea{min-height:118px;font:12px/1.5 ui-monospace,SFMono-Regular,Consolas,monospace}.form-actions{display:flex;gap:8px;margin-top:3px}.form-note{color:var(--muted-2);font-size:.68rem}
23
+ .table-panel{margin-top:15px;padding:0;overflow:auto;box-shadow:none}table{width:100%;border-collapse:collapse;font-size:.76rem}th{text-align:left;color:var(--muted);font-weight:550;background:#1c1c1c}th,td{padding:10px 14px;border-bottom:1px solid var(--line-soft)}tbody tr{cursor:pointer}tbody tr:hover{background:var(--surface-2)}td code{color:var(--blue)}.editor-grid{display:grid;grid-template-columns:1.3fr .7fr;gap:16px}.code-editor,.result-box,.schema-box{font:12px/1.55 ui-monospace,SFMono-Regular,Consolas,"Liberation Mono",monospace}.code-editor{min-height:370px}.code-editor.small{min-height:115px}.result-box,.schema-box{margin:0;background:#171717;border:1px solid var(--line-soft);border-radius:6px;padding:12px;white-space:pre-wrap;overflow:auto;color:#c9c9c9}.result-box{min-height:90px;max-height:260px}.result-box.tall{min-height:180px;max-height:420px}.schema-box{max-height:165px;margin-bottom:14px;color:#a8cbaa}
24
+ .file-layout{display:grid;grid-template-columns:270px minmax(0,1fr);gap:15px;min-height:620px}.file-tree{padding:15px;box-shadow:none}.tree-list{display:grid;gap:2px;max-height:560px;overflow:auto}.tree-file{border:0;background:transparent;color:#bbb;text-align:left;padding:7px 8px;border-radius:4px;cursor:pointer;font-size:.75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.tree-file:hover,.tree-file.active{background:var(--surface-3);color:var(--text)}.tree-file.active{box-shadow:inset 2px 0 var(--accent)}.file-editor-panel,.doc-reader{padding:0;overflow:hidden}.file-toolbar{height:59px;border-bottom:1px solid var(--line-soft);display:flex;justify-content:space-between;align-items:center;padding:0 16px}.file-toolbar strong,.file-toolbar small{display:block}.file-toolbar strong{font-size:.8rem}.file-toolbar small{color:var(--muted-2);font:10px ui-monospace,monospace;margin-top:3px}.file-content{border:0;border-radius:0;min-height:510px;background:var(--bg);padding:17px;resize:none}.preview-status{border-top:1px solid var(--line-soft);color:var(--muted);font-size:.7rem;padding:9px 16px}.document-content{margin:0;padding:22px;white-space:pre-wrap;overflow:auto;max-height:620px;color:#d0d0d0;font:14px/1.7 ui-monospace,SFMono-Regular,Consolas,monospace}
25
+ .tool-layout{display:grid;grid-template-columns:270px minmax(0,1fr);gap:15px}.tool-list{padding:8px;box-shadow:none;max-height:760px;overflow:auto}.tool-item{display:block;width:100%;border:0;background:transparent;border-radius:5px;padding:9px 10px;text-align:left;cursor:pointer}.tool-item:hover,.tool-item.active{background:var(--surface-3)}.tool-item.active{box-shadow:inset 2px 0 var(--accent)}.tool-item strong,.tool-item small{display:block}.tool-item strong{font:12px ui-monospace,monospace}.tool-item small{color:var(--muted);font-size:.67rem;margin-top:2px}.tool-item[disabled]{opacity:.48;cursor:not-allowed}.tool-runner{box-shadow:none}.effect-fields{display:grid;grid-template-columns:1fr 180px;gap:10px;margin:12px 0}
26
+ .job-compose{display:grid;grid-template-columns:1fr 1fr;gap:15px;margin-bottom:15px}.jobs-table{display:grid}.job-row{display:grid;grid-template-columns:minmax(160px,1fr) 130px 130px 32px;gap:12px;align-items:center;border-top:1px solid var(--line-soft);padding:11px 3px;cursor:pointer;font-size:.76rem}.job-row:hover{background:var(--surface-2)}.job-row code{color:var(--blue);overflow:hidden;text-overflow:ellipsis}.job-state{color:var(--amber)}.job-state.complete,.job-state.completed,.job-state.succeeded{color:var(--accent)}.job-state.failed,.job-state.error{color:var(--red)}.toast-region{position:fixed;z-index:20;right:18px;bottom:18px;display:grid;gap:8px;max-width:min(390px,calc(100vw - 36px))}.toast{background:#292929;border:1px solid #484848;border-left:3px solid var(--accent);border-radius:6px;padding:11px 14px;box-shadow:var(--shadow);font-size:.78rem}.toast.error{border-left-color:var(--red)}
27
+ @media(max-width:1000px){.app-shell{grid-template-columns:72px minmax(0,1fr)}.sidebar{padding-inline:10px}.brand{padding-inline:5px}.brand-logo{height:42px;aspect-ratio:1;object-fit:cover;object-position:left}.nav-item{justify-content:center;padding:12px}.nav-item span{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.sidebar-foot span:last-child{display:none}.graph-layout,.tool-layout{grid-template-columns:1fr}.inspector{order:-1}.metric-grid{grid-template-columns:repeat(2,1fr)}}
28
+ @media(max-width:700px){.app-shell{display:block}.sidebar{height:auto;position:sticky;z-index:9;padding:7px 10px;border-right:0;border-bottom:1px solid var(--line);flex-direction:row}.brand,.sidebar-foot{display:none}.sidebar nav{display:flex;overflow:auto;width:100%}.nav-item{min-width:42px}.topbar{padding:0 14px}.identity code,.connection{display:none}main{padding:24px 14px 44px}.view-heading{align-items:flex-start;flex-direction:column}.metric-grid,.two-column,.editor-grid,.file-layout,.job-compose{grid-template-columns:1fr}.file-layout{min-height:auto}.file-tree{max-height:250px}.detail-list{grid-template-columns:1fr}.job-row{grid-template-columns:minmax(100px,1fr) 90px 24px}.job-row time{display:none}}
29
+ main:has(>.view[data-view-panel="trigger"].active){height:calc(100vh - 65px);max-width:none;margin:0;padding:0;overflow:hidden}.view[data-view-panel="trigger"]{height:100%;background:var(--bg)}.view[data-view-panel="trigger"] #trigger-blueprint-root{height:100%}
30
+ @media(max-width:700px){main:has(>.view[data-view-panel="trigger"].active){height:calc(100vh - 121px);padding:0}}
31
+ #trigger-blueprint-root{height:100%;min-height:0;background:#161516}#trigger-blueprint-root .trigger-loading-shell{position:relative;width:100%;height:100%;min-height:520px;background:#161516}#trigger-blueprint-root .trigger-loading-overlay{position:absolute;z-index:45;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:9px;background:#161516;color:#e7e7e7;text-align:center;pointer-events:auto}#trigger-blueprint-root .trigger-loading-overlay>strong{font-size:.9rem;font-weight:650}#trigger-blueprint-root .trigger-loading-overlay>span{max-width:420px;color:#8f8f8f;font-size:.72rem;line-height:1.55}#trigger-blueprint-root .trigger-loading-mark,#trigger-blueprint-root .trigger-loading-error-icon{width:58px;height:58px;fill:none;stroke:#32d551;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}#trigger-blueprint-root .trigger-loading-mark rect,#trigger-blueprint-root .trigger-loading-mark path{stroke-dasharray:22 10;animation:trigger-loading-line 1.25s linear infinite}#trigger-blueprint-root .trigger-loading-mark circle{fill:#161516;animation:trigger-loading-node 1.25s ease-in-out infinite}#trigger-blueprint-root .trigger-loading-overlay--error{background:#161516}#trigger-blueprint-root .trigger-loading-error-icon{stroke:#ff747f}#trigger-blueprint-root .trigger-loading-overlay--error button{margin-top:4px;border:1px solid #32d551;border-radius:4px;background:transparent;color:#bdf3c7;padding:7px 15px;cursor:pointer}@keyframes trigger-loading-line{to{stroke-dashoffset:-64}}@keyframes trigger-loading-node{0%,100%{opacity:.35}50%{opacity:1}}@media(prefers-reduced-motion:reduce){#trigger-blueprint-root .trigger-loading-mark rect,#trigger-blueprint-root .trigger-loading-mark path,#trigger-blueprint-root .trigger-loading-mark circle{animation:none}}
32
+
33
+ /* 节点属性树必须占满侧栏;避免被 .graph-property-list>div 的 90px label grid 误伤。 */
34
+ .graph-property-list>.graph-property-tree-mount{display:grid;grid-template-columns:minmax(0,1fr);grid-column:1/-1;width:100%;min-width:0;margin:0}
35
+ .world-property-tree{container-type:inline-size;width:100%;min-width:0;overflow:hidden;border:1px solid var(--line);background:var(--bg);color:var(--text);padding:10px;font-size:.78rem}
36
+ .world-property-tree *{min-width:0}
37
+ .world-property-tree__header{display:flex;align-items:center;min-height:30px;margin-bottom:7px}
38
+ .world-property-tree__header strong{font-size:.82rem}
39
+ .world-property-card{width:100%;min-width:0;margin-top:8px;border:1px solid var(--line);border-radius:7px;background:var(--surface);padding:8px}
40
+ .world-property-card__head{display:grid;grid-template-columns:minmax(0,1fr);gap:7px}
41
+ .world-property-card__identity{display:grid;grid-template-columns:30px minmax(0,1fr) minmax(92px,.62fr);align-items:center;gap:6px}
42
+ .world-property-card__metadata{display:flex;align-items:center;gap:6px}
43
+ .world-property-switch{display:inline-flex;align-items:center;gap:6px;border:0;background:transparent;color:var(--muted);padding:2px;cursor:pointer;font-size:.7rem;white-space:nowrap}
44
+ .world-property-switch>span{position:relative;width:30px;height:17px;flex:0 0 30px;border:1px solid #555;border-radius:999px;background:#2b2b2b;transition:background .15s,border-color .15s}
45
+ .world-property-switch>span::after{content:"";position:absolute;top:2px;left:2px;width:11px;height:11px;border-radius:50%;background:#999;transition:transform .15s,background .15s}
46
+ .world-property-switch[aria-checked="true"]>span{border-color:var(--accent);background:var(--accent-soft)}
47
+ .world-property-switch[aria-checked="true"]>span::after{transform:translateX(13px);background:var(--accent)}
48
+ .world-property-switch b{font-weight:500}
49
+ .world-property-switch:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:4px}
50
+ .world-property-switch:disabled{opacity:.4;cursor:not-allowed}
51
+ .world-property-card__actions{display:flex;align-items:center;gap:6px;min-width:0}
52
+ .world-property-cg-summary{min-width:0;flex:1;overflow:hidden;color:var(--muted);font-size:.69rem;text-overflow:ellipsis;white-space:nowrap}
53
+ .world-property-card__badges{display:flex;flex-wrap:wrap;gap:4px}
54
+ .world-property-card__badges span{border:1px solid var(--line);border-radius:999px;color:var(--muted);padding:2px 6px;font-size:.64rem}
55
+ .world-property-key,.world-property-fixed-key,.world-property-type,.world-property-value{width:100%;min-width:0}
56
+ .world-property-fixed-key{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
57
+ .world-property-tree input,.world-property-tree select{min-height:30px;border:1px solid var(--line);border-radius:5px;background:var(--bg);color:var(--text);padding:4px 7px;font-size:.72rem}
58
+ .world-property-value{display:block;margin-top:8px}
59
+ .world-property-fab{display:inline-grid;place-items:center;width:30px;height:30px;min-width:30px;border:1px solid #4a4a4a;border-radius:50%;background:transparent;color:#d7d7d7;padding:0;cursor:pointer}
60
+ .world-property-fab svg,.world-property-chip-button svg{fill:none;stroke:currentColor;stroke-linecap:round;stroke-linejoin:round}
61
+ .world-property-fab:hover:not(:disabled),.world-property-fab:focus-visible{border-color:var(--accent);color:var(--accent)}
62
+ .world-property-fab.is-primary{border-color:var(--accent);color:var(--accent)}
63
+ .world-property-fab.is-danger{border-color:#694044;color:#ff8e94}
64
+ .world-property-fab:disabled,.world-property-chip-button:disabled{opacity:.4;cursor:not-allowed}
65
+ .world-property-fab-spacer{width:30px;height:30px}
66
+ .world-property-add{display:grid;grid-template-columns:minmax(0,1fr) 30px;align-items:center;gap:7px;margin-top:8px}
67
+ .world-property-children{min-width:0;margin-top:8px;margin-left:6px;padding-left:8px;border-left:1px solid var(--accent)}
68
+ .world-property-reference{display:grid;grid-template-columns:86px minmax(0,1fr);gap:6px}
69
+ .world-property-reference>.world-property-validated:nth-child(3){grid-column:1/-1}
70
+ .world-property-validated{display:grid;gap:3px;width:100%;min-width:0}
71
+ .world-property-validated input{width:100%;min-width:0}
72
+ .world-property-validated small{color:var(--red);font-size:.65rem}
73
+ .world-property-validated.is-invalid input{border-color:var(--red)}
74
+ .world-property-rich{display:grid;gap:6px}
75
+ .world-property-rich__row{display:grid;grid-template-columns:minmax(0,1fr) 30px;align-items:start;gap:6px}
76
+ .world-property-rich__add{display:flex;flex-wrap:wrap;gap:6px}
77
+ .world-property-chip-button{display:inline-flex;align-items:center;gap:5px;min-height:30px;border:1px solid var(--accent);border-radius:999px;background:transparent;color:var(--accent);padding:4px 9px;font-size:.68rem;cursor:pointer}
78
+ .world-property-depth-note{margin:8px 0 0;color:var(--muted);font-size:.68rem;line-height:1.5}
79
+ .world-property-tree__error{width:100%;min-width:0;border:1px solid var(--red);background:var(--bg);color:var(--red);padding:10px}
80
+ @container(max-width:300px){.world-property-card__identity{grid-template-columns:30px minmax(0,1fr)}.world-property-type{grid-column:2}.world-property-card__metadata{grid-template-columns:1fr}.world-property-switch{justify-self:start}.world-property-reference{grid-template-columns:1fr}}
81
+
82
+ /* Host 主题由根 token 统一控制;图谱共享样式继续由 graph.css 拥有。 */
83
+ .graph-workspace-view,.graph-stage .graph-canvas,.graph-canvas,.view[data-view-panel="trigger"]{background:var(--bg)}
84
+ .nav-item.active,.tab.active{background:var(--accent-soft)}
85
+ .nav-item.active,.tag{border-color:#32623c}
86
+ .action-icon{border-color:#32623c;background:var(--accent-soft)}
87
+ #spatial-canvas .graph-node.selected circle{fill:var(--accent-soft)}
88
+ input:focus,textarea:focus{box-shadow:0 0 0 2px rgba(50,213,81,.15)}
89
+
90
+ /* CG 只消费项目已有图片,内部 runtime definition 不在 GUI 暴露。 */
91
+ .graph-cg-current-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px;margin-top:8px}
92
+ .graph-cg-current-cell{position:relative;display:block;overflow:hidden;border:1px solid var(--line);border-radius:7px;background:var(--surface-2);padding:0;cursor:pointer;aspect-ratio:1}
93
+ .graph-cg-current-cell:hover,.graph-cg-current-cell:focus-visible,.graph-cg-current-cell.selected{border-color:var(--accent);outline:none;box-shadow:0 0 0 2px color-mix(in srgb,var(--accent) 22%,transparent)}
94
+ .graph-cg-current-cell img{display:block;width:100%;height:100%;object-fit:cover}
95
+ .graph-cg-modal-backdrop{position:fixed;z-index:40;inset:0;display:grid;place-items:center;padding:16px;background:rgba(0,0,0,.72)}
96
+ .graph-cg-modal{display:grid;grid-template-rows:auto minmax(0,1fr);width:min(720px,calc(100vw - 32px));height:min(520px,calc(100vh - 32px));max-width:720px;max-height:520px;overflow:hidden;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:var(--shadow)}
97
+ .graph-cg-modal>header,.graph-cg-modal>footer{display:flex;align-items:center;padding:12px 16px;border-color:var(--line);background:var(--surface)}
98
+ .graph-cg-modal>header{justify-content:space-between;border-bottom:1px solid var(--line)}
99
+ .graph-cg-modal>header h2{margin:0;font-size:1rem}
100
+ .graph-cg-modal>footer{justify-content:flex-end;border-top:1px solid var(--line)}
101
+ .graph-cg-modal-body{min-height:0;overflow:auto;padding:14px 16px}
102
+ .graph-cg-current{min-height:0}
103
+ .graph-cg-current-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:8px}
104
+ .graph-cg-current-heading h3{margin:0;font-size:.8rem;color:var(--muted)}
105
+ .graph-cg-modal-empty{grid-column:1/-1;margin:0;color:var(--muted);font-size:.75rem}
106
+ .graph-cg-modal-status{margin:0 0 10px;color:var(--muted);font-size:.78rem}
107
+ .model-settings-dialog{width:min(520px,calc(100vw - 32px));padding:0;border:1px solid var(--line);border-radius:10px;background:var(--surface);color:var(--text);box-shadow:var(--shadow)}.model-settings-dialog::backdrop{background:rgba(0,0,0,.72)}.model-settings-shell{display:grid;grid-template-rows:auto 1fr auto}.model-settings-shell>header,.model-settings-shell>footer{display:flex;align-items:center;padding:12px 16px;background:var(--surface)}.model-settings-shell>header{justify-content:space-between;border-bottom:1px solid var(--line)}.model-settings-shell>header p{margin:0 0 3px;color:var(--muted);font-size:.72rem}.model-settings-shell>header h2{margin:0;font-size:1rem}.model-settings-shell>footer{justify-content:flex-end;gap:8px;border-top:1px solid var(--line)}.model-settings-body{display:grid;gap:14px;padding:16px}.model-settings-body p{margin:0;color:var(--muted);font-size:.78rem}.model-settings-body label{display:grid;gap:6px}.model-settings-body label span{font-size:.72rem;color:var(--muted)}.model-settings-body select{width:100%;border:1px solid var(--line);background:var(--bg);color:var(--text);border-radius:6px;padding:9px 10px}.model-settings-body select:disabled{opacity:.72}
108
+ @media(max-width:520px){.graph-cg-current-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
109
+ .sidebar-account { display: grid; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--border, #383638); }
110
+ .sidebar-account #auth-start-form:not([hidden]) { display: flex; justify-content: center; }
111
+ .sidebar-account #account-details { display: grid; gap: 5px; min-width: 0; }
112
+ .sidebar-account #account-details[hidden] { display: none; }
113
+ .sidebar-account #account-name { overflow-wrap: anywhere; font-size: 13px; }
114
+ .sidebar-account #account-balance { color: #decda5; font-size: 12px; overflow-wrap: anywhere; }
115
+ .spatial-template-view{min-height:calc(100vh - 65px);background:var(--bg)}main .spatial-template-view.active{display:block}.spatial-template-panel{width:min(920px,100%);margin:0 auto;padding:36px 28px;display:grid;gap:20px}.spatial-template-heading{display:flex;align-items:flex-end;justify-content:space-between;gap:18px}.spatial-template-heading h1{margin:0;font-size:1.3rem}.spatial-template-kicker{margin:0 0 4px;color:var(--accent);font-size:.72rem;letter-spacing:.08em}.spatial-template-status{margin:0;color:var(--muted);font-size:.78rem}.spatial-template-status[data-state="loading"]{color:var(--amber)}.spatial-template-status[data-state="error"]{color:var(--red)}.spatial-template-status[data-state="ready"]{color:var(--accent)}.spatial-template-tabs{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;padding:5px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface)}.spatial-template-tab{border:0;border-radius:5px;background:transparent;color:var(--muted);padding:9px;cursor:pointer;font-size:.8rem}.spatial-template-tab.active{background:var(--accent-soft);color:var(--text);box-shadow:inset 0 0 0 1px #315c38}.spatial-template-card{display:grid;gap:16px;max-width:620px;padding:23px;border:1px solid var(--line);border-radius:var(--radius);background:var(--surface);box-shadow:var(--shadow)}.spatial-template-card h2{margin:0;font-size:1rem}.spatial-template-description,.spatial-template-fixed,.spatial-template-revision,.spatial-template-empty{margin:0;color:var(--muted);font-size:.82rem}.spatial-template-fixed{padding:9px 10px;border-left:2px solid var(--accent);background:var(--accent-soft);color:var(--text)}.spatial-template-field{display:grid;gap:6px}.spatial-template-field>span{color:var(--muted);font-size:.78rem}.spatial-template-field select,.spatial-template-field input{border:1px solid var(--line);border-radius:6px;background:var(--bg);color:var(--text);padding:9px 10px;font:inherit}.spatial-template-parameters{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.spatial-template-actions{display:flex;justify-content:flex-end;gap:8px}.spatial-template-confirmation{max-width:620px;padding:16px 18px;border:1px solid #7f6830;border-radius:var(--radius);background:#2d291d;color:var(--text);box-shadow:var(--shadow)}.spatial-template-confirmation p{margin:0 0 14px;font-size:.84rem;line-height:1.55}@media(max-width:700px){.nav-group-toggle .nav-chevron,.nav-subitems{display:none}.spatial-template-panel{padding:24px 14px}.spatial-template-tabs,.spatial-template-parameters{grid-template-columns:repeat(2,minmax(0,1fr))}.spatial-template-heading{align-items:flex-start;flex-direction:column}}
@@ -0,0 +1,120 @@
1
+ /** 地形高度值是生成器的 -1..1 归一化输出;只在渲染时投影为米,绝不反写世界。 */
2
+ /** 默认开启,避免新建地图在未调图层前丢失可读地形边界。 */
3
+ export const DEFAULT_TERRAIN_LAYERS = Object.freeze({ showContours: true });
4
+
5
+ const LIGHT = Object.freeze(normalize(-0.58, -0.42, 0.7));
6
+
7
+ function normalize(x, y, z) {
8
+ const length = Math.hypot(x, y, z) || 1;
9
+ return [x / length, y / length, z / length];
10
+ }
11
+
12
+ function elevationAt(elevations, width, height, x, y) {
13
+ return Number(elevations[Math.max(0, Math.min(height - 1, y)) * width + Math.max(0, Math.min(width - 1, x))]);
14
+ }
15
+
16
+ /**
17
+ * 用真实世界采样间距计算法线。相同斜坡的 LOD 栅格密度变化不会改变光照。
18
+ */
19
+ export function hillshadeAt({ elevations, width, height, x, y, extentWidthMeters, extentHeightMeters, elevationScaleMeters }) {
20
+ if (!Number.isFinite(elevationScaleMeters) || elevationScaleMeters <= 0) throw new TypeError("缺少权威高程米制");
21
+ const dx = extentWidthMeters / Math.max(1, width - 1);
22
+ const dy = extentHeightMeters / Math.max(1, height - 1);
23
+ const west = elevationAt(elevations, width, height, x - 1, y) * elevationScaleMeters;
24
+ const east = elevationAt(elevations, width, height, x + 1, y) * elevationScaleMeters;
25
+ const north = elevationAt(elevations, width, height, x, y - 1) * elevationScaleMeters;
26
+ const south = elevationAt(elevations, width, height, x, y + 1) * elevationScaleMeters;
27
+ const spanX = x > 0 && x + 1 < width ? 2 * dx : dx;
28
+ const spanY = y > 0 && y + 1 < height ? 2 * dy : dy;
29
+ const [nx, ny, nz] = normalize(-(east - west) / spanX, -(south - north) / spanY, 1);
30
+ return Math.max(0, nx * LIGHT[0] + ny * LIGHT[1] + nz * LIGHT[2]);
31
+ }
32
+
33
+ function crossing(left, right, level) {
34
+ const delta = right - left;
35
+ if (Math.abs(delta) < 1e-12) return 0.5;
36
+ return Math.max(0, Math.min(1, (level - left) / delta));
37
+ }
38
+
39
+ /**
40
+ * 从同一实际高度网格生成米制等高线。每一端点都是世界坐标,tile 边界可无缝拼接。
41
+ * 海、湖相关的 cell 整体跳过,以免把水文掩码渲染成伪等高线。
42
+ */
43
+ export function contourSegments({ elevations, width, height, seaLevel, lakeMask, originXMeters = 0, originYMeters = 0, extentWidthMeters, extentHeightMeters, elevationScaleMeters, intervalMeters, maxSegments = 4096 }) {
44
+ if (!Number.isFinite(elevationScaleMeters) || elevationScaleMeters <= 0) throw new TypeError("缺少权威高程米制");
45
+ if (!Number.isFinite(intervalMeters) || intervalMeters <= 0 || width < 2 || height < 2) return [];
46
+ const dx = extentWidthMeters / (width - 1);
47
+ const dy = extentHeightMeters / (height - 1);
48
+ const segments = [];
49
+ for (let y = 0; y + 1 < height; y += 1) {
50
+ for (let x = 0; x + 1 < width; x += 1) {
51
+ const indices = [y * width + x, y * width + x + 1, (y + 1) * width + x + 1, (y + 1) * width + x];
52
+ if (indices.some((index) => lakeMask?.[index] === true || Number(elevations[index]) <= seaLevel)) continue;
53
+ const values = indices.map((index) => Number(elevations[index]) * elevationScaleMeters);
54
+ const minimum = Math.min(...values);
55
+ const maximum = Math.max(...values);
56
+ const first = Math.ceil(minimum / intervalMeters) * intervalMeters;
57
+ for (let level = first; level <= maximum + 1e-9; level += intervalMeters) {
58
+ const mask = Number(values[0] > level) | Number(values[1] > level) << 1 | Number(values[2] > level) << 2 | Number(values[3] > level) << 3;
59
+ if (mask === 0 || mask === 15) continue;
60
+ const edges = {
61
+ top: [crossing(values[0], values[1], level), 0],
62
+ right: [1, crossing(values[1], values[2], level)],
63
+ bottom: [crossing(values[3], values[2], level), 1],
64
+ left: [0, crossing(values[0], values[3], level)],
65
+ };
66
+ const pairs = {
67
+ 1: [["left", "top"]], 2: [["top", "right"]], 3: [["left", "right"]], 4: [["right", "bottom"]],
68
+ 5: [["left", "top"], ["right", "bottom"]], 6: [["top", "bottom"]], 7: [["left", "bottom"]],
69
+ 8: [["bottom", "left"]], 9: [["top", "bottom"]], 10: [["top", "right"], ["bottom", "left"]],
70
+ 11: [["right", "bottom"]], 12: [["left", "right"]], 13: [["top", "right"]], 14: [["left", "top"]],
71
+ }[mask] ?? [];
72
+ for (const [from, to] of pairs) {
73
+ const a = edges[from]; const b = edges[to];
74
+ segments.push([originXMeters + (x + a[0]) * dx, originYMeters + (y + a[1]) * dy, originXMeters + (x + b[0]) * dx, originYMeters + (y + b[1]) * dy, level]);
75
+ // 不截断半个 tile:提高整个 tile 的间距,保留完整且连续的主等高线。
76
+ if (segments.length > maxSegments) return contourSegments({
77
+ elevations, width, height, seaLevel, lakeMask, originXMeters, originYMeters, extentWidthMeters, extentHeightMeters,
78
+ elevationScaleMeters, intervalMeters: intervalMeters * 2, maxSegments,
79
+ });
80
+ }
81
+ }
82
+ }
83
+ }
84
+ Object.defineProperty(segments, "intervalMeters", { value: intervalMeters, enumerable: false });
85
+ return segments;
86
+ }
87
+
88
+ /** 将世界坐标等高线压缩为可精确计量的连续 buffer;每段固定 5 个 Float64。 */
89
+ export function packContourSegments(segments) {
90
+ const packed = new Float64Array(segments.length * 5);
91
+ for (let index = 0; index < segments.length; index += 1) packed.set(segments[index], index * 5);
92
+ Object.defineProperty(packed, "intervalMeters", { value: segments.intervalMeters, enumerable: false });
93
+ return packed;
94
+ }
95
+
96
+ /** 以屏幕可读性选相邻米制间距,并提供交叉淡化,避免 LOD 阈值跳变。 */
97
+ export function contourPlan(metersPerPixel) {
98
+ const target = Math.max(1, metersPerPixel) * 12;
99
+ const steps = [5, 10, 20, 50, 100, 200, 500, 1000];
100
+ const found = steps.findIndex((step) => step >= target);
101
+ const upperIndex = found < 0 ? steps.length - 1 : found;
102
+ const upper = steps[upperIndex];
103
+ const lower = steps[Math.max(0, upperIndex - 1)];
104
+ const blend = upper === lower ? 1 : Math.max(0, Math.min(1, (target - lower) / (upper - lower)));
105
+ return { fineIntervalMeters: lower, coarseIntervalMeters: upper, coarseAlpha: blend, majorEvery: 5 };
106
+ }
107
+
108
+ /**
109
+ * 多个 tile 自适应缓存后,绘制只能消费它们共同拥有的等高线层级。
110
+ * interval 都由 5m 起成倍提升,向上对齐不会在 tile 边界凭空补线。
111
+ */
112
+ export function sharedContourPlan(metersPerPixel, minimumAvailableIntervalMeters = 5) {
113
+ const plan = contourPlan(metersPerPixel);
114
+ const minimum = Math.max(5, minimumAvailableIntervalMeters);
115
+ const align = (interval) => Math.max(minimum, Math.ceil(interval / minimum) * minimum);
116
+ const fineIntervalMeters = align(plan.fineIntervalMeters);
117
+ const coarseIntervalMeters = align(plan.coarseIntervalMeters);
118
+ if (fineIntervalMeters === coarseIntervalMeters) return { fineIntervalMeters, coarseIntervalMeters, coarseAlpha: 1, majorEvery: plan.majorEvery };
119
+ return { ...plan, fineIntervalMeters, coarseIntervalMeters };
120
+ }
@@ -0,0 +1,17 @@
1
+ .workspace-status{width:100%;min-height:100%;display:grid;place-content:center;justify-items:center;gap:10px;padding:clamp(28px,7vh,76px) 24px;background:var(--bg,#161516);color:var(--text,#f2f2f2);text-align:center}
2
+ .workspace-status>svg{display:block;width:clamp(76px,11vw,112px);height:auto;margin-bottom:5px;fill:none;stroke:var(--accent,#32d551);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vector-effect:non-scaling-stroke}
3
+ .workspace-status--loading>svg path:first-child{stroke-dasharray:8 7;animation:workspace-status-trace 1.5s linear infinite}
4
+ .workspace-status__title{margin:0;color:var(--text,#f2f2f2);font-size:clamp(1rem,1.6vw,1.2rem);font-weight:650;line-height:1.35;letter-spacing:-.01em}
5
+ .workspace-status__description{max-width:420px;margin:0;color:var(--muted,#aaa7aa);font-size:.82rem;line-height:1.7}
6
+ .workspace-status__details{width:min(420px,100%);margin-top:3px;color:var(--muted-2,#817f81);font-size:.7rem;text-align:left}
7
+ .workspace-status__details summary{width:max-content;margin:auto;cursor:pointer;color:var(--muted-2,#817f81);outline:none}
8
+ .workspace-status__details summary:focus-visible{border-radius:3px;outline:2px solid var(--accent,#32d551);outline-offset:2px}
9
+ .workspace-status__details-content{display:grid;gap:6px;margin-top:8px;padding:9px 11px;border:1px solid var(--line-soft,#343334);border-radius:6px;background:var(--surface,#201f20);overflow-wrap:anywhere}
10
+ .workspace-status__code{width:max-content;max-width:100%;overflow:hidden;color:var(--muted,#aaa7aa);font:inherit;text-overflow:ellipsis;white-space:nowrap}
11
+ .workspace-status__message{margin:0;color:var(--muted-2,#817f81);line-height:1.55}
12
+ .workspace-status__action{margin-top:5px;min-width:104px}
13
+ .workspace-status--error>svg{stroke:var(--accent,#32d551)}
14
+ .trigger-workspace-status:not([hidden]){display:grid;min-height:520px;height:calc(100vh - 65px)}
15
+ @keyframes workspace-status-trace{to{stroke-dashoffset:-30}}
16
+ @media(prefers-reduced-motion:reduce){.workspace-status--loading>svg path:first-child{animation:none}}
17
+ @media(max-width:520px){.workspace-status{padding:28px 18px}.workspace-status__description{max-width:310px}}