@tileflow/dev 0.1.0-alpha.31 → 0.1.0-alpha.32
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.
- package/README.md +59 -2
- package/THIRD_PARTY_NOTICES.md +225 -3
- package/dist/artifacts.d.ts +6 -2
- package/dist/artifacts.js +17 -2
- package/dist/{chunk-2QXKVQ6O.js → chunk-CM6ZTYDJ.js} +45 -5
- package/dist/{chunk-IR22ZTQW.js → chunk-F4PLTZS4.js} +447 -27
- package/dist/devtools-browser.js +17353 -0
- package/dist/devtools-observer.js +95 -0
- package/dist/fonts.d.ts +1 -1
- package/dist/icons-B84H8Ymu.d.ts +406 -0
- package/dist/icons.d.ts +3 -339
- package/dist/icons.js +3 -1
- package/dist/{preview-assets-MVUJJ5XR.js → preview-assets-KGUICQHG.js} +6 -0
- package/dist/preview.d.ts +1 -1
- package/dist/preview.js +206 -98
- package/dist/server.d.ts +11 -4
- package/dist/server.js +1594 -277
- package/docs/beacon-map-audits.md +102 -0
- package/docs/layers-inspection.md +87 -0
- package/docs/local-comparison-workbench-and-compiler-inspection.md +74 -171
- package/docs/native-artifacts.md +3 -3
- package/package.json +7 -5
- package/ui-fonts/hubot-sans-OFL.txt +93 -0
- package/ui-fonts/hubot-sans.woff2 +0 -0
- package/ui-fonts/inter-OFL.txt +92 -0
- package/ui-fonts/inter.woff2 +0 -0
- package/ui-fonts/jetbrains-mono-OFL.txt +93 -0
- package/ui-fonts/jetbrains-mono.woff2 +0 -0
- package/ui-fonts/mona-sans-OFL.txt +93 -0
- package/ui-fonts/mona-sans.woff2 +0 -0
package/dist/server.js
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
parseTileflowLineBackgroundGlyphPath,
|
|
6
6
|
tileflowArtifactSessionSchemaVersion,
|
|
7
7
|
tileflowLineBackgroundGlyphRoute
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-CM6ZTYDJ.js";
|
|
9
9
|
import {
|
|
10
10
|
getTileflowAssetBasePath,
|
|
11
11
|
getTileflowAssetFileName,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
normalizeTileflowBasePath,
|
|
14
14
|
resolveTileflowArtifactPublicUrls
|
|
15
15
|
} from "./chunk-UTRO4IBQ.js";
|
|
16
|
-
import "./chunk-
|
|
16
|
+
import "./chunk-F4PLTZS4.js";
|
|
17
17
|
import "./chunk-7C4F6GAX.js";
|
|
18
18
|
import {
|
|
19
19
|
TileflowStyleValidationError
|
|
@@ -34,12 +34,1247 @@ import {
|
|
|
34
34
|
tileflowThemeNameSchema
|
|
35
35
|
} from "@tileflow/core";
|
|
36
36
|
|
|
37
|
+
// src/preview-controls.ts
|
|
38
|
+
import { parseResolvedTileflowMap, tileflowStyleFontFacesMetadataKey } from "@tileflow/core";
|
|
39
|
+
import { createStyleFromCatalogWithInspection } from "@tileflow/core/build";
|
|
40
|
+
var moduleNames = [
|
|
41
|
+
"land",
|
|
42
|
+
"water",
|
|
43
|
+
"nautical",
|
|
44
|
+
"roads",
|
|
45
|
+
"boundaries",
|
|
46
|
+
"labels",
|
|
47
|
+
"poi",
|
|
48
|
+
"aeroways",
|
|
49
|
+
"transit",
|
|
50
|
+
"addresses",
|
|
51
|
+
"landforms",
|
|
52
|
+
"buildings",
|
|
53
|
+
"vegetation"
|
|
54
|
+
];
|
|
55
|
+
var fieldRules = {
|
|
56
|
+
"buildings.mode": ["flat", "3d"],
|
|
57
|
+
"buildings.threeDimensional.lighting": ["style", "sun"],
|
|
58
|
+
"buildings.threeDimensional.shadows": "boolean",
|
|
59
|
+
"buildings.threeDimensional.occlusion": "boolean",
|
|
60
|
+
"buildings.threeDimensional.windows": "boolean",
|
|
61
|
+
"buildings.threeDimensional.edges": [0, 1],
|
|
62
|
+
"buildings.threeDimensional.colorVariation": [0, 1],
|
|
63
|
+
"buildings.threeDimensional.nightLight": [0, 2],
|
|
64
|
+
"vegetation.mode": ["3d", "flat", "auto"],
|
|
65
|
+
"vegetation.fallback": ["flat", "none"],
|
|
66
|
+
"vegetation.visible": "boolean",
|
|
67
|
+
"vegetation.threeDimensional.finish": ["faceted", "smooth", "toon"],
|
|
68
|
+
"vegetation.threeDimensional.edges": [0, 1],
|
|
69
|
+
"vegetation.threeDimensional.heightScale": [0, 10],
|
|
70
|
+
"vegetation.threeDimensional.crownScale": [0, 10]
|
|
71
|
+
};
|
|
72
|
+
function record(value) {
|
|
73
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
+
}
|
|
75
|
+
function parsePreviewControls(text) {
|
|
76
|
+
if (text.length > 8192) throw new Error("Map controls exceed 8 KiB.");
|
|
77
|
+
const input = JSON.parse(text);
|
|
78
|
+
if (!record(input)) throw new Error("Map controls must be an object.");
|
|
79
|
+
for (const [module, patch] of Object.entries(input)) {
|
|
80
|
+
if (!moduleNames.includes(module) || !record(patch))
|
|
81
|
+
throw new Error("Unknown map control module: " + module);
|
|
82
|
+
const validate = (path, value) => {
|
|
83
|
+
const rule = path === module + ".enabled" ? "boolean" : fieldRules[path];
|
|
84
|
+
const valid = rule === "boolean" ? typeof value === "boolean" : rule && (typeof rule[0] === "number" ? typeof value === "number" && Number.isFinite(value) && value >= rule[0] && value <= rule[1] : typeof value === "string" && rule.includes(value));
|
|
85
|
+
if (!valid) throw new Error("Invalid map control: " + path);
|
|
86
|
+
};
|
|
87
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
88
|
+
if (key === "threeDimensional" && record(value) && ["buildings", "vegetation"].includes(module)) {
|
|
89
|
+
for (const [nested, entry] of Object.entries(value))
|
|
90
|
+
validate(module + "." + key + "." + nested, entry);
|
|
91
|
+
} else validate(module + "." + key, value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return input;
|
|
95
|
+
}
|
|
96
|
+
function describePreviewControls(artifacts, map, theme) {
|
|
97
|
+
const definition = artifacts.project.maps[map];
|
|
98
|
+
const style = artifacts.styles[map]?.[theme];
|
|
99
|
+
if (!definition || !style) throw new Error("Unknown map theme.");
|
|
100
|
+
const modules = definition.modules ?? {};
|
|
101
|
+
const metadata = style.metadata ?? {};
|
|
102
|
+
return {
|
|
103
|
+
map,
|
|
104
|
+
theme,
|
|
105
|
+
modules: moduleNames.map((id) => {
|
|
106
|
+
const config = modules[id] ?? { type: id };
|
|
107
|
+
const fields = [];
|
|
108
|
+
const record2 = config;
|
|
109
|
+
const add = (path, label, value, choices, min, max, step, inherit = false) => fields.push({
|
|
110
|
+
path,
|
|
111
|
+
label,
|
|
112
|
+
value,
|
|
113
|
+
...choices ? { choices } : {},
|
|
114
|
+
...inherit ? { inherit: true } : {},
|
|
115
|
+
...min !== void 0 ? { min, max, step } : {}
|
|
116
|
+
});
|
|
117
|
+
if (id === "buildings") {
|
|
118
|
+
const design = metadata["tileflow:buildings"];
|
|
119
|
+
const authored = record2.threeDimensional;
|
|
120
|
+
const value = (key, fallback) => {
|
|
121
|
+
const resolved = design?.[key] ?? authored?.[key];
|
|
122
|
+
return typeof resolved === typeof fallback ? resolved : fallback;
|
|
123
|
+
};
|
|
124
|
+
add("threeDimensional.lighting", "Building light", value("lighting", "style"), [
|
|
125
|
+
"style",
|
|
126
|
+
"sun"
|
|
127
|
+
]);
|
|
128
|
+
for (const [key, label] of [
|
|
129
|
+
["shadows", "Cast shadows"],
|
|
130
|
+
["occlusion", "Contact shadows"],
|
|
131
|
+
["windows", "Night windows"]
|
|
132
|
+
])
|
|
133
|
+
add("threeDimensional." + key, label, value(key, false));
|
|
134
|
+
for (const [key, label] of [
|
|
135
|
+
["edges", "Edge definition"],
|
|
136
|
+
["colorVariation", "Color variation"],
|
|
137
|
+
["nightLight", "Night glow"]
|
|
138
|
+
])
|
|
139
|
+
add(
|
|
140
|
+
"threeDimensional." + key,
|
|
141
|
+
label,
|
|
142
|
+
value(key, 0),
|
|
143
|
+
void 0,
|
|
144
|
+
0,
|
|
145
|
+
key === "nightLight" ? 2 : 1,
|
|
146
|
+
0.05
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
if (id === "vegetation") {
|
|
150
|
+
add(
|
|
151
|
+
"mode",
|
|
152
|
+
"Tree representation",
|
|
153
|
+
String(record2.mode ?? "3d"),
|
|
154
|
+
["3d", "auto", "flat"],
|
|
155
|
+
void 0,
|
|
156
|
+
void 0,
|
|
157
|
+
void 0,
|
|
158
|
+
true
|
|
159
|
+
);
|
|
160
|
+
add("fallback", "Without 3D support", String(record2.fallback ?? "flat"), ["flat", "none"]);
|
|
161
|
+
add("visible", "Visible by default", record2.visible !== false);
|
|
162
|
+
const authored = record2.threeDimensional;
|
|
163
|
+
const design = metadata["tileflow:vegetation"];
|
|
164
|
+
add("threeDimensional.finish", "Tree finish", String(authored?.finish ?? "faceted"), [
|
|
165
|
+
"faceted",
|
|
166
|
+
"smooth",
|
|
167
|
+
"toon"
|
|
168
|
+
]);
|
|
169
|
+
for (const [key, label] of [
|
|
170
|
+
["heightScale", "Tree height"],
|
|
171
|
+
["crownScale", "Crown size"]
|
|
172
|
+
]) {
|
|
173
|
+
const value = design?.[key] ?? authored?.[key];
|
|
174
|
+
add(
|
|
175
|
+
"threeDimensional." + key,
|
|
176
|
+
label,
|
|
177
|
+
typeof value === "number" ? value : 1,
|
|
178
|
+
void 0,
|
|
179
|
+
0,
|
|
180
|
+
10,
|
|
181
|
+
0.1
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { id, enabled: record2.enabled !== false, fields };
|
|
186
|
+
})
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function compilePreviewControls(artifacts, map, theme, text, apiBaseUrl) {
|
|
190
|
+
if (artifacts.nativeBuild) throw new Error("Map controls require a web preview.");
|
|
191
|
+
const overrides = parsePreviewControls(text);
|
|
192
|
+
const definition = artifacts.project.maps[map];
|
|
193
|
+
const base = artifacts.styles[map]?.[theme];
|
|
194
|
+
if (!definition || !base) throw new Error("Unknown map theme.");
|
|
195
|
+
const modules = { ...definition.modules };
|
|
196
|
+
for (const [id, patch] of Object.entries(overrides)) {
|
|
197
|
+
const key = id;
|
|
198
|
+
const original = modules[key] ?? { type: id };
|
|
199
|
+
const merged = { ...original, ...patch };
|
|
200
|
+
if ("threeDimensional" in patch && patch.threeDimensional)
|
|
201
|
+
Object.assign(merged, {
|
|
202
|
+
threeDimensional: {
|
|
203
|
+
..."threeDimensional" in original ? original.threeDimensional : {},
|
|
204
|
+
...patch.threeDimensional
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
Object.assign(modules, { [id]: merged });
|
|
208
|
+
}
|
|
209
|
+
const resolved = parseResolvedTileflowMap({ ...definition, modules });
|
|
210
|
+
const result = createStyleFromCatalogWithInspection(
|
|
211
|
+
{ ...artifacts.project, maps: { ...artifacts.project.maps, [map]: resolved } },
|
|
212
|
+
map,
|
|
213
|
+
{
|
|
214
|
+
theme,
|
|
215
|
+
apiBaseUrl,
|
|
216
|
+
preparedAssets: artifacts.mapAssets?.[map]
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
result.style.sources = Object.fromEntries(
|
|
220
|
+
Object.entries(result.style.sources).map(([id, source]) => [id, base.sources[id] ?? source])
|
|
221
|
+
);
|
|
222
|
+
if (base.glyphs) result.style.glyphs = base.glyphs;
|
|
223
|
+
if (base.sprite) result.style.sprite = base.sprite;
|
|
224
|
+
if (base.metadata?.[tileflowStyleFontFacesMetadataKey])
|
|
225
|
+
result.style.metadata = {
|
|
226
|
+
...result.style.metadata,
|
|
227
|
+
[tileflowStyleFontFacesMetadataKey]: base.metadata[tileflowStyleFontFacesMetadataKey]
|
|
228
|
+
};
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
|
|
37
232
|
// src/comparison.ts
|
|
38
233
|
import { randomBytes } from "crypto";
|
|
234
|
+
import { readFile } from "fs/promises";
|
|
235
|
+
|
|
236
|
+
// src/devtools/inspector-styles.ts
|
|
237
|
+
var inspectorStyles = `
|
|
238
|
+
body.devtools-ready {
|
|
239
|
+
--paper:#F8F6F2; --surface:#F0EDE7; --ink:#30251F; --muted:#635449; --faint:#A0907F;
|
|
240
|
+
--border:#E1DCD3; --border-strong:#C7BFB3; --accent:#38668B; --dt-selected:#E6ECF2;
|
|
241
|
+
--hover:#E9E5DE; --accent-soft:#DFE8F0; --ok:#2F704B; --ok-soft:#E3EBDB; --warn:#815D21; --warn-soft:#F3E6CF;
|
|
242
|
+
--danger:#B13C34; --dt-string:var(--ink); --dt-number:var(--accent); --mono:'JetBrains Mono',ui-monospace,Menlo,monospace;
|
|
243
|
+
color-scheme:light; font:13px/1.4 Inter,system-ui,-apple-system,sans-serif; -webkit-font-smoothing:antialiased;
|
|
244
|
+
}
|
|
245
|
+
body.devtools-ready[data-inspector-theme=dark] {
|
|
246
|
+
--paper:#1D1816; --surface:#171311; --ink:#F5EDD9; --muted:#C3B7A9; --faint:#857869; --border:#40352F; --border-strong:#5A4B43;
|
|
247
|
+
--accent:#83ADD1; --dt-selected:#22303B; --hover:#2E2622; --accent-soft:#223039; --ok:#91B887; --ok-soft:#24301F; --warn:#E2B36F; --warn-soft:#3A2F20; --danger:#F1746B; color-scheme:dark;
|
|
248
|
+
}
|
|
249
|
+
@media(prefers-color-scheme:dark) { body.devtools-ready[data-inspector-theme=system] {
|
|
250
|
+
--paper:#1D1816; --surface:#171311; --ink:#F5EDD9; --muted:#C3B7A9; --faint:#857869; --border:#40352F; --border-strong:#5A4B43;
|
|
251
|
+
--accent:#83ADD1; --dt-selected:#22303B; --hover:#2E2622; --accent-soft:#223039; --ok:#91B887; --ok-soft:#24301F; --warn:#E2B36F; --warn-soft:#3A2F20; --danger:#F1746B; color-scheme:dark;
|
|
252
|
+
}}
|
|
253
|
+
.devtools-ready button,.devtools-ready input,.devtools-ready select { font:inherit; }
|
|
254
|
+
.devtools-ready button { border:1px solid var(--border); border-radius:6px; min-height:28px; padding:4px 10px; cursor:pointer; }
|
|
255
|
+
.devtools-ready button:disabled { cursor:default; opacity:.45; }
|
|
256
|
+
.devtools-ready button:hover:not(:disabled) { background:var(--hover); }
|
|
257
|
+
.devtools-ready input[type=range] { accent-color:var(--accent); }
|
|
258
|
+
.devtools-ready code,.devtools-ready pre { font-family:var(--mono); }
|
|
259
|
+
.devtools-ready .dt-head { height:40px; min-height:40px; flex-wrap:nowrap; gap:2px; padding:0 8px; background:var(--paper); }
|
|
260
|
+
.devtools-ready .dt-head-main { display:flex; align-items:center; flex:1; min-width:0; }
|
|
261
|
+
.devtools-ready .dt-tabs { flex:1; align-items:center; gap:2px; }
|
|
262
|
+
.devtools-ready .dt-tabs button { border:0; border-radius:6px; min-height:28px; padding:5px 10px; color:var(--muted); background:transparent; }
|
|
263
|
+
.devtools-ready .dt-tabs button[aria-selected=true] { color:var(--ink); background:var(--surface); }
|
|
264
|
+
.devtools-ready .dt-head > .dt-icon { width:28px; height:28px; margin:0; }
|
|
265
|
+
.devtools-ready .dt-icon[aria-pressed=true] { background:var(--accent-soft); color:var(--accent); }
|
|
266
|
+
.devtools-ready .dt-camera { display:flex; align-items:center; flex:0 0 auto; flex-wrap:wrap; gap:6px; margin:0; padding:8px 12px; border:0; border-bottom:1px solid var(--border); background:var(--paper); overflow:visible; }
|
|
267
|
+
.devtools-ready .dt-camera-field { display:flex; flex:0 0 auto; gap:0; height:30px; border:1px solid var(--border); border-radius:6px; background:var(--surface); overflow:hidden; }
|
|
268
|
+
.devtools-ready .dt-camera-field > span:first-child { padding:0 4px 0 9px; color:var(--faint); font-size:10px; text-transform:uppercase; letter-spacing:.04em; }
|
|
269
|
+
.devtools-ready .dt-camera-field input[type=number] { width:58px; min-height:0; padding:0 5px; border:0; background:transparent; font:12px var(--mono); appearance:textfield; color:var(--ink); }
|
|
270
|
+
.devtools-ready .dt-camera-field input::-webkit-inner-spin-button { appearance:none; }
|
|
271
|
+
.devtools-ready .dt-camera-field input[type=range] { width:110px; padding:0; margin:0 10px 0 2px; min-height:0; }
|
|
272
|
+
.devtools-ready .dt-unit { padding-right:9px; color:var(--faint); }
|
|
273
|
+
.devtools-ready .dt-copy-agent { margin-left:auto; background:var(--ink); color:var(--paper); border-color:var(--ink); font-weight:500; }
|
|
274
|
+
.devtools-ready .dt-copy-agent:hover { background:var(--ink); opacity:.88; }
|
|
275
|
+
.devtools-ready .dt-menu { top:38px; right:8px; width:236px; padding:10px; border-radius:10px; box-shadow:0 8px 28px #0002; }
|
|
276
|
+
.devtools-ready .dt-dock-options { display:grid; grid-template-columns:repeat(3,1fr); gap:6px; margin-bottom:8px; }
|
|
277
|
+
.devtools-ready .dt-dock-options button { width:auto; height:34px; border:1px solid var(--border); justify-content:center; }
|
|
278
|
+
.devtools-ready .dt-menu-label { font-size:11px; }
|
|
279
|
+
.devtools-ready .dt-segments,.devtools-ready .dt-layer-modes,.devtools-ready .controls-segments { display:inline-flex; padding:2px; background:var(--surface); border:1px solid var(--border); border-radius:6px; gap:2px; }
|
|
280
|
+
.devtools-ready .dt-segments button,.devtools-ready .dt-layer-modes button,.devtools-ready .controls-segments button { min-height:23px; border:0; border-radius:4px; padding:3px 9px; font-size:12px; background:transparent; color:var(--muted); }
|
|
281
|
+
.devtools-ready .dt-segments button[aria-pressed=true],.devtools-ready .dt-layer-modes button[aria-pressed=true],.devtools-ready .controls-segments button[aria-checked=true] { color:var(--ink); background:var(--paper); box-shadow:0 1px 2px #0001; }
|
|
282
|
+
.devtools-ready .dt-menu .dt-segments { display:flex; }.devtools-ready .dt-menu .dt-segments button { flex:1; }
|
|
283
|
+
.devtools-ready .dt-tools { padding:10px 12px; gap:8px; background:var(--paper); }
|
|
284
|
+
.devtools-ready .dt-tools input[type=search] { height:30px; padding:0 9px; background:var(--surface); border-radius:6px; width:auto; }
|
|
285
|
+
.devtools-ready .dt-subtabs { padding:0 20px; gap:18px; }
|
|
286
|
+
.devtools-ready .dt-subtabs button { background:transparent; border:0; border-radius:0; border-bottom:2px solid transparent; padding:8px 0 9px; color:var(--muted); min-height:36px; }
|
|
287
|
+
.devtools-ready .dt-subtabs button[aria-selected=true] { color:var(--ink); border-bottom-color:var(--ink); }
|
|
288
|
+
.devtools-ready input[role=switch] { appearance:none; width:26px!important; height:15px!important; min-height:0; flex:none; border:0; border-radius:8px; background:var(--faint); position:relative; padding:0; margin:0; cursor:pointer; }
|
|
289
|
+
.devtools-ready input[role=switch]::before { content:''; position:absolute; width:11px; height:11px; left:2px; top:2px; border-radius:50%; background:#fff; transition:transform .15s; }
|
|
290
|
+
.devtools-ready input[role=switch]:checked { background:var(--accent); }
|
|
291
|
+
.devtools-ready input[role=switch]:checked::before { transform:translateX(11px); }
|
|
292
|
+
.devtools-ready .dt-layer-split { display:grid; grid-template-columns:minmax(300px,360px) minmax(0,1fr); }
|
|
293
|
+
.devtools-ready .dt-layer-sidebar { display:flex; flex-direction:column; min-height:0; border-right:1px solid var(--border); }
|
|
294
|
+
.devtools-ready .dt-layer-list { flex:1; border:0; }.devtools-ready .dt-layer-tools { flex-direction:column; align-items:stretch; }
|
|
295
|
+
.devtools-ready .dt-layer-tools-row { display:flex; align-items:center; gap:8px; }.devtools-ready .dt-layer-total { margin-left:auto; font:11px var(--mono); }
|
|
296
|
+
.devtools-ready .dt-layer-visible { font-size:12px; }.devtools-ready .dt-layer-group-title { height:32px; font-size:11px; padding:0 12px 0 8px!important; }
|
|
297
|
+
.devtools-ready .dt-layer-group-title strong { text-transform:uppercase; letter-spacing:.05em; }
|
|
298
|
+
.devtools-ready .dt-layer-row { display:grid; grid-template-columns:20px 18px minmax(0,1fr) 64px; height:30px; padding:0 12px 0 6px; gap:8px; border-left:2px solid transparent; }
|
|
299
|
+
.devtools-ready .dt-layer-row[aria-pressed=true] { background:var(--dt-selected); border-left-color:var(--accent); color:var(--ink); }
|
|
300
|
+
.devtools-ready .dt-layer-glyph { color:var(--muted); font-size:15px; }.devtools-ready .dt-layer-label { font-size:12px; }
|
|
301
|
+
.devtools-ready .dt-layer-prefix { color:var(--faint); }.devtools-ready .dt-layer-name { font-weight:500; }
|
|
302
|
+
.devtools-ready .dt-eye { padding:0; border:0; width:20px; height:20px; min-height:20px; color:var(--faint); background:transparent; display:grid; place-items:center; opacity:0; }
|
|
303
|
+
.devtools-ready .dt-eye svg { width:14px; height:14px; }
|
|
304
|
+
.devtools-ready :is(.dt-layer-row,.dt-layer-group-title):hover .dt-eye,.devtools-ready .dt-eye:focus-visible,.devtools-ready .dt-eye[aria-pressed=true] { opacity:1; }
|
|
305
|
+
.dt-zrange { position:relative; display:block; width:64px; height:14px; }.dt-zrange::before { content:''; position:absolute; inset:6px 0; height:2px; background:var(--border); }
|
|
306
|
+
.dt-zrange i { position:absolute; top:5px; height:4px; border-radius:2px; background:var(--accent); opacity:.65; }.dt-zrange em { position:absolute; top:1px; height:12px; width:1px; background:var(--accent); }
|
|
307
|
+
.devtools-ready .dt-layer-heading { padding:14px 20px 6px; align-items:flex-start; border:0; }
|
|
308
|
+
.devtools-ready .dt-layer-title strong { font-size:16px; font-weight:600; }.devtools-ready .dt-layer-title > code { font-size:10px; color:var(--faint); }
|
|
309
|
+
.dt-layer-status { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); margin-top:6px; }.dt-layer-status[data-state=Enabled] { color:var(--ok); }.dt-layer-status[data-state="Outside zoom"] { color:var(--warn); }
|
|
310
|
+
.devtools-ready .dt-layer-rules { font-size:12px; }.devtools-ready .dt-layer-rules th { padding:8px 20px; font-size:11px; text-transform:uppercase; letter-spacing:.05em; }
|
|
311
|
+
.devtools-ready .dt-layer-rules th:first-child { width:42%; }.devtools-ready .dt-layer-rules td { padding:8px 20px; vertical-align:middle; min-height:44px; }
|
|
312
|
+
.devtools-ready .dt-layer-rules code { font:12px/1.5 var(--mono); }.devtools-ready .dt-layer-rule-name { color:var(--ink); }.devtools-ready .dt-layer-dependency { color:var(--accent); font:10px var(--mono); }
|
|
313
|
+
.devtools-ready .dt-layer-rule-editor { gap:9px; flex-wrap:wrap; }.devtools-ready .dt-layer-rule-editor input:not([type=color]):not([type=checkbox]):not([type=range]),.devtools-ready .dt-layer-rule-editor select { min-height:26px; border:1px solid var(--border); font:12px var(--mono); border-radius:6px; }
|
|
314
|
+
.devtools-ready .dt-layer-rule-editor input[type=color] { width:22px; height:22px; }
|
|
315
|
+
.devtools-ready .dt-layer-rule-editor input[type=range] { width:110px; min-width:70px; padding:0; flex:0 1 110px; }
|
|
316
|
+
.dt-rule-toggle { display:flex; align-items:center; gap:10px; }.dt-rule-toggle input { width:14px; height:14px; min-height:0; margin:0; accent-color:var(--accent); }
|
|
317
|
+
.dt-rule-choices { display:inline-flex; flex-wrap:wrap; border:1px solid var(--border); border-radius:6px; overflow:hidden; }.devtools-ready .dt-rule-choices button { min-height:26px; border:0; border-radius:0; font:11px var(--mono); padding:3px 8px; }
|
|
318
|
+
.dt-rule-spark { display:flex; align-items:center; gap:10px; color:var(--accent); font:11px var(--mono); margin-bottom:5px; }.dt-rule-spark svg { width:130px; height:26px; overflow:visible; }
|
|
319
|
+
.dt-filter-chips { display:flex; flex-wrap:wrap; gap:6px; margin-block:10px; }.dt-filter-chip { padding:4px 9px; background:var(--surface); border:1px solid var(--border); border-radius:13px; font:11px var(--mono); }
|
|
320
|
+
.devtools-ready .dt-layer-section,.devtools-ready .dt-layer-origin { padding:12px 20px; }.devtools-ready .dt-layer-section h4 { font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
|
|
321
|
+
.devtools-ready .dt-layer-evidence { padding:0; }.devtools-ready .dt-layer-evidence-row { padding:12px 20px; font-size:12px; grid-template-columns:20px 130px 1fr; }
|
|
322
|
+
.devtools-ready .dt-layer-evidence-row[data-state=pass] .dt-layer-indicator { color:var(--ok); }.devtools-ready .dt-layer-evidence-row[data-state=warn] .dt-layer-indicator { color:var(--warn); }
|
|
323
|
+
.devtools-ready .dt-layer-json { font:11px/1.6 var(--mono); color:var(--ink); }
|
|
324
|
+
.devtools-ready .dt-table { font:12px var(--mono); }.devtools-ready .dt-table th { padding:8px 12px; font:11px Inter,system-ui,sans-serif; text-transform:uppercase; border-right:0; }.devtools-ready .dt-table td { height:32px; padding:6px 12px; }.devtools-ready .dt-table tbody tr:nth-child(even) { background:transparent; }
|
|
325
|
+
.devtools-ready .dt-network-body { display:grid; grid-template-columns:minmax(300px,360px) minmax(0,1fr); }.dt-tiles-sidebar { display:flex; flex-direction:column; min-height:0; border-right:1px solid var(--border); }.devtools-ready .dt-network-detail { border:0; min-width:0; }
|
|
326
|
+
.devtools-ready .dt-tile-heading { padding:14px 20px 8px; }.dt-tile-heading strong { font:15px var(--mono); }.dt-tile-status { padding:0 20px 10px; color:var(--muted); font-size:12px; }
|
|
327
|
+
.dt-tile-grid { display:grid; gap:3px; padding:12px; }.devtools-ready .dt-tile-cell { aspect-ratio:1; padding:0; font:10px var(--mono); min-width:0; min-height:20px; border-radius:4px; background:color-mix(in srgb,var(--accent) var(--weight,10%),var(--paper)); }
|
|
328
|
+
.devtools-ready .dt-tile-cell[aria-pressed=true] { border-color:var(--accent); box-shadow:0 0 0 2px var(--accent-soft); }.dt-tile-grid-title { margin:10px 12px 0; color:var(--muted); font-size:11px; }
|
|
329
|
+
.dt-tile-chip-list { display:flex; gap:6px; flex-wrap:wrap; padding:10px 20px; border-bottom:1px solid var(--border); }.devtools-ready .dt-tile-chip { font:11px var(--mono); border-radius:13px; }
|
|
330
|
+
.dt-asset-layout { display:grid; grid-template-columns:minmax(0,1fr) 300px; min-height:0; flex:1; }.dt-asset-main { min-width:0; overflow:auto; padding:12px; }.dt-asset-aside { overflow:auto; border-left:1px solid var(--border); }.dt-asset-aside h3 { font:500 15px var(--mono); margin:16px 20px; }.dt-asset-variants { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1px; background:var(--border); }.dt-asset-variant { padding:20px 10px; display:flex; flex-direction:column; align-items:center; gap:10px; background:var(--surface); font:10px var(--mono); overflow-wrap:anywhere; }.dt-asset-variant[data-night=true] { background:#2e2622; color:#f5edd9; }
|
|
331
|
+
.devtools-ready .dt-sprite-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(104px,1fr)); gap:4px; }.devtools-ready .dt-sprite-card { border:1px solid transparent; border-radius:8px; background:transparent; padding:16px 6px 10px; cursor:pointer; }.dt-sprite-card[aria-pressed=true] { background:var(--dt-selected)!important; border-color:var(--accent)!important; }
|
|
332
|
+
.dt-debug-overlay { display:grid; grid-template-columns:84px 1fr auto; align-items:center; gap:16px; padding:10px 20px; border-bottom:1px solid var(--border); cursor:pointer; }.dt-debug-overlay:hover { background:var(--hover); }.dt-debug-overlay small { display:block; color:var(--muted); font-size:12px; }.dt-debug-preview { width:84px; height:58px; border-radius:6px; overflow:hidden; }.dt-debug-preview svg { width:100%; height:100%; }
|
|
333
|
+
.dt-section-heading { background:var(--surface); padding:8px 20px; font-size:11px; font-weight:600; letter-spacing:.05em; text-transform:uppercase; border-bottom:1px solid var(--border); }
|
|
334
|
+
.devtools-ready .dt-overdraw-help { margin:0; border:0; border-bottom:1px solid var(--border); border-radius:0; padding:12px 20px 12px 120px; background:var(--surface); }.dt-debug-scale { display:flex; align-items:center; gap:12px; padding:16px 20px; flex-wrap:wrap; }.dt-debug-scale input[type=number] { width:64px; }
|
|
335
|
+
.devtools-ready .map-controls { padding:0!important; background:var(--paper); }.devtools-ready .controls-heading { display:none; }.devtools-ready .controls-grid { display:block; }.devtools-ready .controls-selection,.devtools-ready .controls-runtime { border:0; padding:0; }
|
|
336
|
+
.devtools-ready .controls-section-title { margin:0; padding:8px 20px; background:var(--surface); border-bottom:1px solid var(--border); font-size:11px; letter-spacing:.05em; }.devtools-ready .control-field { display:grid; grid-template-columns:200px 1fr; align-items:center; margin:0; padding:10px 20px; border-bottom:1px solid var(--border); gap:16px; font-size:13px; }.devtools-ready .control-field select { max-width:100%; width:max-content; min-width:132px; }
|
|
337
|
+
.devtools-ready #controls-modules { display:block; }.devtools-ready .control-module { border:0; border-bottom:1px solid var(--border); border-radius:0; margin:0; background:var(--paper); }.devtools-ready .control-module-header { padding:10px 20px; }.devtools-ready .control-module-header label { justify-content:flex-start; font-size:13px; font-weight:500; }.devtools-ready .control-module-header input { order:-1; margin-right:12px; }.devtools-ready .control-module fieldset { padding:0; background:var(--surface); }.devtools-ready .control-module .control-field { padding-left:58px; border:0; }.devtools-ready .dt-scene-content { max-width:1000px; width:100%; }.dt-palettes { display:flex; gap:8px; flex-wrap:wrap; }.devtools-ready .dt-palette { display:flex; align-items:center; gap:8px; padding:5px 10px 5px 5px; }.dt-palette-swatches { display:grid; grid-template-columns:repeat(2,11px); grid-template-rows:repeat(2,11px); overflow:hidden; border-radius:4px; }
|
|
338
|
+
@container(max-width:760px) {
|
|
339
|
+
.devtools-ready .dt-layer-split,.devtools-ready .dt-network-body { display:flex; flex-direction:column; }
|
|
340
|
+
.devtools-ready .dt-layer-sidebar,.devtools-ready .dt-tiles-sidebar { flex:0 0 36%; min-height:150px; border:0; border-bottom:1px solid var(--border); }
|
|
341
|
+
.devtools-ready .dt-layer-detail,.devtools-ready .dt-network-detail { flex:1; min-height:0; border:0; }
|
|
342
|
+
.devtools-ready .dt-layer-title { flex-basis:180px; }.devtools-ready .dt-camera-field input[type=range] { width:80px; }
|
|
343
|
+
.devtools-ready .dt-layer-rules th:first-child { width:45%; }.devtools-ready .dt-layer-rules td { padding:8px 12px; }
|
|
344
|
+
.dt-asset-layout { grid-template-columns:1fr; }.dt-asset-aside { border:0; border-top:1px solid var(--border); }
|
|
345
|
+
.devtools-ready .control-field { grid-template-columns:160px 1fr; }.devtools-ready .control-module .control-field { padding-left:30px; }
|
|
346
|
+
}
|
|
347
|
+
@media(prefers-reduced-motion:reduce) { .devtools-ready * { scroll-behavior:auto!important; transition:none!important; animation:none!important; } }
|
|
348
|
+
|
|
349
|
+
.dt-palettes{display:flex;gap:8px;flex-wrap:wrap}.devtools-ready .dt-palettes button{display:flex;flex-direction:column;gap:7px;padding:10px 14px;min-width:110px;background:var(--paper);text-align:left;text-transform:capitalize}.devtools-ready .dt-palettes button[aria-checked=true]{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.dt-palette-swatches{display:flex;height:26px;width:100%;overflow:hidden;border-radius:4px}.dt-palette-swatches i{flex:1;min-width:20px}.devtools-ready .dt-palette-field{align-items:flex-start;flex-direction:column}.dt-model-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}.devtools-ready .dt-model-card{display:flex;flex-direction:column;align-items:center;padding:0;overflow:hidden;border-radius:10px}.dt-model-card canvas{background:linear-gradient(var(--surface),var(--paper));max-width:100%;height:auto!important}.dt-model-card strong{font:500 12px var(--mono);padding:9px 12px 2px;overflow-wrap:anywhere}.dt-model-card small{font-size:11px;color:var(--muted);padding:0 12px 9px}.dt-model-card[aria-pressed=true]{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent-soft)}.dt-asset-aside>canvas{display:block;margin:auto;max-width:100%;touch-action:none;cursor:grab}.dt-font-ranges{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.dt-font-ranges small{color:var(--muted);font-size:11px}
|
|
350
|
+
|
|
351
|
+
.dt-debug-specimen{display:flex;align-items:center;gap:18px;min-height:70px;padding:12px 20px;border:1px solid var(--border);border-radius:8px;background:var(--surface)}.dt-rule-font-preview{overflow:hidden;max-width:100%}.dt-rule-font-preview canvas{max-width:100%;height:auto!important}
|
|
352
|
+
/* Keep the new shell independent of the legacy in-header camera and toolbar breakpoints. */
|
|
353
|
+
.devtools-ready .dt-shell>.dt-camera{order:0;flex:0 0 auto;margin:0;border-top:0;background:var(--paper);padding:8px 12px}
|
|
354
|
+
.devtools-ready .dt-shell>.dt-head{flex-wrap:nowrap}.devtools-ready .dt-shell .dt-head-main{display:flex;gap:2px}
|
|
355
|
+
.devtools-ready .dt-shell .dt-layer-tools input[type=search]{order:0;flex:0 0 auto;width:100%;height:30px}
|
|
356
|
+
.devtools-ready .dt-shell .dt-layer-tools{flex-wrap:nowrap}.devtools-ready .dt-shell .dt-layer-tools-row{flex:none;min-height:28px}
|
|
357
|
+
.devtools-ready .dt-shell .dt-layer-row{display:grid;grid-template-columns:20px 18px minmax(0,1fr) 64px;height:30px;min-height:30px;padding:0 12px 0 6px;gap:8px}
|
|
358
|
+
.devtools-ready .dt-shell .dt-layer-label{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
|
|
359
|
+
.devtools-ready .dt-shell .dt-layer-heading{flex:none}.devtools-ready .dt-shell .dt-layer-title{flex:1 1 180px}
|
|
360
|
+
.devtools-ready .dt-shell .dt-layer-sidebar .dt-layer-list{flex:1;min-height:0}.devtools-ready .dt-shell .map-controls .control-field{display:grid;grid-template-columns:160px minmax(0,1fr);padding:12px 20px;margin:0}
|
|
361
|
+
.devtools-ready .dt-shell .control-module-header label{display:flex;gap:12px}.devtools-ready .dt-shell .map-controls input[role=switch]{width:26px!important;height:15px!important;min-height:0;border:0;background:var(--faint)}.devtools-ready .dt-shell .map-controls input[role=switch]:checked{background:var(--accent)}.devtools-ready .dt-shell .map-controls input[role=switch]::before{width:11px;height:11px;top:2px;left:2px;background:white}.devtools-ready .dt-shell .map-controls input[role=switch]:checked::before{transform:translateX(11px)}
|
|
362
|
+
@container(max-width:760px){.devtools-ready .dt-shell .dt-layer-sidebar,.devtools-ready .dt-shell .dt-tiles-sidebar{min-height:110px;flex:0 0 38%}.devtools-ready .dt-shell .dt-layer-tools{padding:8px 12px}.devtools-ready .dt-shell .dt-tabs button{padding-inline:7px}.devtools-ready .dt-shell .dt-head{padding-inline:4px}.devtools-ready .dt-shell .dt-camera-field{flex:0 0 auto}}
|
|
363
|
+
`;
|
|
364
|
+
|
|
365
|
+
// src/devtools/styles.ts
|
|
366
|
+
var legacyStyles = `
|
|
367
|
+
body.devtools-ready { color-scheme:light dark; --paper:light-dark(#fff,#202124); --surface:light-dark(#f1f3f4,#292a2d); --ink:light-dark(#202124,#e8eaed); --muted:light-dark(#62676e,#9aa0a6); --border:light-dark(#dadce0,#3c4043); --accent:light-dark(#1a73e8,#8ab4f8); --dt-selected:light-dark(#e8f0fe,#394457); --dt-string:light-dark(#a31515,#e6b98b); --dt-number:light-dark(#1a1aa6,#a8c7fa); font:12px/1.45 Arial,system-ui,sans-serif; background:var(--paper); }
|
|
368
|
+
.devtools-ready button,.devtools-ready input,.devtools-ready select,.devtools-ready summary { min-height:24px; border-radius:2px; font:inherit; }
|
|
369
|
+
.devtools-ready button { padding:3px 8px; background:var(--paper); color:var(--ink); }
|
|
370
|
+
.devtools-ready button:hover { background:var(--surface); border-color:var(--border); }
|
|
371
|
+
.devtools-ready button:focus-visible,.devtools-ready input:focus-visible,.devtools-ready select:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
|
|
372
|
+
.devtools-ready button[aria-pressed=true] { color:var(--accent); background:var(--dt-selected); }
|
|
373
|
+
.devtools-ready .tool-icon { width:14px; height:14px; }
|
|
374
|
+
.devtools-ready .workspace { display:grid; grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(180px,1fr) var(--dt-height,340px); flex:1; min-height:0; }
|
|
375
|
+
.devtools-ready .workspace[data-dock=right] { grid-template-columns:minmax(200px,1fr) var(--dt-width,460px); grid-template-rows:minmax(0,1fr); }
|
|
376
|
+
.devtools-ready .workspace[data-dock=left] { grid-template-columns:var(--dt-width,460px) minmax(200px,1fr); grid-template-rows:minmax(0,1fr); }
|
|
377
|
+
.devtools-ready .workspace[data-dock=left] > .stage { grid-column:2; grid-row:1; }
|
|
378
|
+
.devtools-ready .workspace[data-dock=left] > .dt-shell { grid-column:1; grid-row:1; border-right:1px solid var(--border); }
|
|
379
|
+
.devtools-ready .workspace[data-collapsed=true] { grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(0,1fr); }
|
|
380
|
+
.devtools-ready .workspace[data-collapsed=true] > .dt-shell { display:none; }
|
|
381
|
+
.devtools-ready .workspace > .inspector { display:none !important; }
|
|
382
|
+
.devtools-ready .inspect-hint { top:8px; font-size:11px; padding:4px 7px; background:var(--paper); color:var(--ink); border:1px solid var(--accent); }
|
|
383
|
+
#dt-reopen { display:none; position:absolute; top:8px; left:8px; z-index:10; box-shadow:0 1px 4px #0002; }
|
|
384
|
+
.workspace[data-collapsed=true] > #dt-reopen { display:block; }
|
|
385
|
+
.devtools-ready .workspace[data-collapsed=true] > .stage { grid-column:1; grid-row:1; }
|
|
386
|
+
.dt-shell { container-type:inline-size; display:flex; flex-direction:column; min-width:0; min-height:0; overflow:visible; position:relative; background:var(--paper); border-top:1px solid var(--border); color:var(--ink); }
|
|
387
|
+
.dt-head { min-height:40px; display:flex; align-items:center; border-bottom:1px solid var(--border); background:var(--surface); padding:0 3px; position:relative; flex:none; }
|
|
388
|
+
.dt-head-main { display:flex; align-items:center; gap:12px; flex:1; min-width:0; align-self:stretch; }
|
|
389
|
+
.dt-tabs { display:flex; min-width:0; overflow-x:auto; scrollbar-width:none; flex:0 1 auto; align-self:stretch; }
|
|
390
|
+
.dt-tabs button { border:0; border-bottom:2px solid transparent; background:transparent; border-radius:0; white-space:nowrap; padding:8px 10px; min-height:38px; }
|
|
391
|
+
.dt-tabs button[aria-selected=true] { color:var(--accent); border-bottom-color:var(--accent); background:var(--paper); }
|
|
392
|
+
.dt-head .dt-icon,.dt-tools .dt-icon { display:inline-flex; align-items:center; justify-content:center; border:0; background:transparent; padding:3px; width:26px; height:26px; flex:none; }
|
|
393
|
+
.dt-head .dt-icon:disabled { opacity:.35; cursor:default; }
|
|
394
|
+
.dt-icon svg { width:16px; height:16px; pointer-events:none; }
|
|
395
|
+
.dt-divider { height:16px; border-left:1px solid var(--border); margin:0 4px; }
|
|
396
|
+
.dt-camera { display:flex; align-items:center; flex-wrap:nowrap; gap:8px; min-width:0; flex:none; border:0; border-left:1px solid var(--border); margin:5px 0; padding:0 0 0 12px; overflow-x:auto; scrollbar-width:thin; }
|
|
397
|
+
.dt-camera-field { display:flex; align-items:center; gap:4px; min-width:0; flex:none; }
|
|
398
|
+
.dt-camera-field > label { color:var(--muted); font-size:11px; line-height:1; }
|
|
399
|
+
.dt-stepper { display:flex; align-items:stretch; height:26px; border:1px solid var(--border); border-radius:4px; overflow:hidden; background:var(--paper); }
|
|
400
|
+
.dt-stepper input[type=number] { appearance:textfield; -moz-appearance:textfield; width:54px; min-width:0; min-height:0; padding:2px 0; border:0; border-radius:0; background:transparent; color:var(--ink); text-align:center; font-variant-numeric:tabular-nums; }
|
|
401
|
+
.dt-stepper input::-webkit-inner-spin-button,.dt-stepper input::-webkit-outer-spin-button { -webkit-appearance:none; margin:0; }
|
|
402
|
+
.dt-stepper button { width:24px; min-height:0; padding:0; border:0; border-radius:0; background:transparent; font-size:15px; line-height:1; }
|
|
403
|
+
.dt-stepper button:disabled { color:var(--muted); opacity:.4; cursor:default; }
|
|
404
|
+
.dt-camera:disabled { opacity:.5; }
|
|
405
|
+
.dt-camera-reset { flex:none; height:26px; }
|
|
406
|
+
.dt-menu { position:absolute; z-index:40; right:5px; top:40px; padding:8px; background:var(--paper); color:var(--ink); border:1px solid var(--border); box-shadow:0 4px 16px #0003; min-width:190px; border-radius:4px; }
|
|
407
|
+
:is([data-dock=left],[data-dock=right]) .dt-head { flex-wrap:wrap; }
|
|
408
|
+
:is([data-dock=left],[data-dock=right]) .dt-head > .dt-icon { margin-block:6px; }
|
|
409
|
+
:is([data-dock=left],[data-dock=right]) .dt-head-main { display:contents; }
|
|
410
|
+
:is([data-dock=left],[data-dock=right]) .dt-tabs { flex:1; }
|
|
411
|
+
:is([data-dock=left],[data-dock=right]) .dt-camera-field { flex:1 0 96px; }
|
|
412
|
+
:is([data-dock=left],[data-dock=right]) .dt-stepper { flex:1; min-width:0; }
|
|
413
|
+
:is([data-dock=left],[data-dock=right]) .dt-stepper input[type=number] { flex:1; width:0; }
|
|
414
|
+
:is([data-dock=left],[data-dock=right]) .dt-stepper button { flex:none; width:22px; }
|
|
415
|
+
:is([data-dock=left],[data-dock=right]) .dt-menu { top:38px; }
|
|
416
|
+
@container(max-width:1050px) {
|
|
417
|
+
.dt-head { flex-wrap:wrap; }
|
|
418
|
+
.dt-head > .dt-icon { margin-block:6px; }
|
|
419
|
+
.dt-head-main { display:contents; }
|
|
420
|
+
.dt-tabs { flex:1; }
|
|
421
|
+
.dt-camera-field { flex:1 0 96px; }
|
|
422
|
+
.dt-stepper { flex:1; min-width:0; }
|
|
423
|
+
.dt-stepper input[type=number] { flex:1; width:0; }
|
|
424
|
+
.dt-stepper button { flex:none; width:22px; }
|
|
425
|
+
.dt-menu { top:38px; }
|
|
426
|
+
}
|
|
427
|
+
.dt-menu-label { color:var(--muted); margin:2px 0 6px; }
|
|
428
|
+
.dt-dock-options { display:flex; gap:4px; }
|
|
429
|
+
.dt-dock-options button { padding:6px; display:flex; }
|
|
430
|
+
.dt-dock-options svg { width:24px; height:20px; }
|
|
431
|
+
.dt-resize { position:absolute; z-index:5; height:6px; left:0; right:0; top:-4px; cursor:ns-resize; touch-action:none; }
|
|
432
|
+
[data-dock=right] > .dt-shell > .dt-resize { top:0; bottom:0; left:-4px; width:6px; height:auto; right:auto; cursor:ew-resize; }
|
|
433
|
+
[data-dock=left] > .dt-shell > .dt-resize { top:0; bottom:0; right:-4px; width:6px; height:auto; left:auto; cursor:ew-resize; }
|
|
434
|
+
.dt-resize-shield { position:absolute; inset:0; z-index:30; background:transparent; }
|
|
435
|
+
.dt-body { flex:1; display:flex; flex-direction:column; min-height:0; overflow:hidden; }
|
|
436
|
+
.dt-panel { flex:1; min-height:0; min-width:0; display:flex; flex-direction:column; overflow:hidden; }
|
|
437
|
+
.dt-tools { min-height:29px; display:flex; flex-wrap:wrap; align-items:center; gap:5px; border-bottom:1px solid var(--border); padding:3px 6px; flex:none; }
|
|
438
|
+
.dt-tools input[type=search] { width:160px; flex:1; min-width:75px; border:1px solid var(--border); background:var(--paper); color:var(--ink); padding:2px 6px; }
|
|
439
|
+
.dt-tools label { display:flex; align-items:center; gap:4px; white-space:nowrap; color:var(--muted); }
|
|
440
|
+
.dt-tools select { max-width:160px; padding:2px; }
|
|
441
|
+
.dt-tools input[type=checkbox] { min-height:0; margin:0; accent-color:var(--accent); }
|
|
442
|
+
.dt-tools button { border-color:transparent; background:transparent; }
|
|
443
|
+
.dt-footer { min-height:23px; padding:3px 8px; border-top:1px solid var(--border); color:var(--muted); font-size:11px; flex:none; display:flex; gap:12px; }
|
|
444
|
+
.dt-scroll { flex:1; overflow:auto; min-height:0; min-width:0; }
|
|
445
|
+
.dt-empty { color:var(--muted); padding:22px; text-align:center; margin:auto; max-width:420px; }
|
|
446
|
+
.dt-empty strong { color:var(--ink); display:block; margin-bottom:6px; font-weight:400; }
|
|
447
|
+
.dt-inspect-grid { display:grid; grid-template-columns:minmax(220px,1fr) minmax(300px,42%); flex:1; min-height:0; }
|
|
448
|
+
.dt-tree-column { display:flex; flex-direction:column; min-height:0; border-right:1px solid var(--border); }
|
|
449
|
+
.dt-tree { flex:1; min-height:0; overflow:auto; font:12px/1.65 ui-monospace,SFMono-Regular,Consolas,monospace; padding:5px 0; }
|
|
450
|
+
.dt-tree-row { display:flex; width:100%; gap:6px; align-items:center; min-height:23px; padding:2px 8px; white-space:nowrap; border:0 !important; border-radius:0 !important; text-align:left; background:transparent !important; font:inherit !important; }
|
|
451
|
+
.dt-tree-row[aria-selected=true] { background:var(--dt-selected) !important; }
|
|
452
|
+
.dt-tree-row:hover { background:var(--surface) !important; }
|
|
453
|
+
.dt-tree-row .dt-token { color:var(--dt-number); }
|
|
454
|
+
.dt-tree-row .dt-attr { color:var(--dt-string); }
|
|
455
|
+
.dt-tree-row .dt-dim { color:var(--muted); }
|
|
456
|
+
.dt-tree-row .dt-truncate { overflow:hidden; text-overflow:ellipsis; }
|
|
457
|
+
.dt-details { min-width:0; min-height:0; display:flex; flex-direction:column; }
|
|
458
|
+
.dt-subtabs { display:flex; gap:2px; border-bottom:1px solid var(--border); flex:none; overflow-x:auto; }
|
|
459
|
+
.dt-subtabs button { border:0; border-bottom:2px solid transparent; border-radius:0; padding:4px 8px; white-space:nowrap; }
|
|
460
|
+
.dt-subtabs button[aria-selected=true] { border-bottom-color:var(--accent); color:var(--accent); }
|
|
461
|
+
.dt-rule { border-bottom:1px solid var(--border); padding:7px 9px; font:12px/1.7 ui-monospace,SFMono-Regular,Consolas,monospace; }
|
|
462
|
+
.dt-rule-title { display:flex; justify-content:space-between; gap:8px; color:var(--ink); overflow-wrap:anywhere; margin-bottom:4px; }
|
|
463
|
+
.dt-rule-origin { color:var(--muted); font:11px/1.6 Arial,sans-serif; text-align:right; }
|
|
464
|
+
.dt-property { display:flex; align-items:center; gap:4px; padding:1px 0 1px 14px; min-height:24px; }
|
|
465
|
+
.dt-property-name { color:light-dark(#881391,#c58af9); flex:none; }
|
|
466
|
+
.dt-property input[type=text],.dt-property input[type=number],.dt-property select { border:1px solid transparent; background:transparent; min-width:30px; flex:1; width:100%; padding:0 2px; color:var(--dt-string); border-radius:0; font:inherit; min-height:22px; }
|
|
467
|
+
.dt-property input:focus,.dt-property input:hover { border-color:var(--border); }
|
|
468
|
+
.dt-property input[type=color] { width:14px; height:14px; padding:0; min-height:0; border:0; flex:none; background:transparent; cursor:pointer; }
|
|
469
|
+
.dt-property input[type=color]::-webkit-color-swatch-wrapper { padding:0; }
|
|
470
|
+
.dt-property input[type=color]::-webkit-color-swatch { border:1px solid var(--border); }
|
|
471
|
+
.dt-property .dt-revert { border:0; padding:0 3px; min-height:18px; color:var(--muted); background:transparent; }
|
|
472
|
+
.dt-property[data-modified=true] { background:color-mix(in srgb,var(--accent) 8%,transparent); }
|
|
473
|
+
.dt-expression { display:block; padding-left:14px; color:var(--muted); white-space:pre-wrap; overflow-wrap:anywhere; font-size:11px; }
|
|
474
|
+
.dt-table { border-collapse:collapse; width:100%; font-size:11px; text-align:left; }
|
|
475
|
+
.dt-table th { position:sticky; top:0; z-index:1; background:var(--surface); font-weight:400; border-bottom:1px solid var(--border); border-right:1px solid var(--border); padding:3px 7px; white-space:nowrap; }
|
|
476
|
+
.dt-table th button { font:inherit; border:0; padding:0; min-height:18px; background:transparent; }
|
|
477
|
+
.dt-table td { padding:3px 7px; border-bottom:1px solid color-mix(in srgb,var(--border) 45%,transparent); max-width:320px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; height:24px; }
|
|
478
|
+
.dt-table tbody tr:nth-child(even) { background:color-mix(in srgb,var(--surface) 55%,transparent); }
|
|
479
|
+
.dt-table tbody tr[aria-selected=true] { background:var(--dt-selected); }
|
|
480
|
+
.dt-table tbody tr:hover { background:var(--surface); }
|
|
481
|
+
.dt-table .dt-cell-button { text-align:left; border:0; background:transparent; min-height:18px; padding:0; color:inherit; max-width:100%; overflow:hidden; text-overflow:ellipsis; font:inherit; }
|
|
482
|
+
.dt-table .dt-number { font-variant-numeric:tabular-nums; text-align:right; }
|
|
483
|
+
.dt-table .dt-error { color:light-dark(#b3261e,#f2b8b5); }
|
|
484
|
+
.dt-network-body { display:flex; flex:1; min-height:0; min-width:0; }
|
|
485
|
+
.dt-network-body > .dt-scroll { flex:1; min-width:0; }
|
|
486
|
+
.dt-network-body[data-selected=true] > .dt-scroll { flex:1 1 55%; }
|
|
487
|
+
.dt-network-detail { display:flex; flex-direction:column; flex:1 1 45%; min-width:290px; min-height:0; border-left:1px solid var(--border); background:var(--paper); }
|
|
488
|
+
.dt-network-detail[hidden] { display:none; }
|
|
489
|
+
.dt-tile-row { cursor:pointer; }
|
|
490
|
+
.dt-tile-heading { flex:none; justify-content:space-between; }
|
|
491
|
+
.dt-tile-heading strong { font-size:12px; font-weight:500; }
|
|
492
|
+
.dt-tile-heading button { border:0; font-size:18px; }
|
|
493
|
+
.dt-tile-content { min-height:0; }
|
|
494
|
+
.dt-tile-content select { width:100%; min-width:0; background:var(--paper); color:var(--ink); border:1px solid var(--border); padding:4px; }
|
|
495
|
+
.dt-tile-paging { justify-content:space-between; }
|
|
496
|
+
.dt-tile-feature { border-bottom:1px solid var(--border); padding:5px 8px; }
|
|
497
|
+
.dt-tile-feature summary { cursor:pointer; overflow-wrap:anywhere; font-size:11px; }
|
|
498
|
+
.dt-tile-feature details { padding-left:12px; }
|
|
499
|
+
.dt-tile-feature pre { white-space:pre-wrap; overflow-wrap:anywhere; font:11px/1.6 ui-monospace,monospace; color:var(--dt-string); margin:6px 0; }
|
|
500
|
+
:is([data-dock=left],[data-dock=right]) .dt-network-body { flex-direction:column; }
|
|
501
|
+
:is([data-dock=left],[data-dock=right]) .dt-network-body[data-selected=true] > .dt-scroll { flex:0 0 35%; }
|
|
502
|
+
:is([data-dock=left],[data-dock=right]) .dt-network-detail { flex:1; min-width:0; border-left:0; border-top:1px solid var(--border); }
|
|
503
|
+
@container(max-width:680px) {
|
|
504
|
+
.dt-network-body { flex-direction:column; }
|
|
505
|
+
.dt-network-body[data-selected=true] > .dt-scroll { flex:0 0 35%; }
|
|
506
|
+
.dt-network-detail { flex:1; min-width:0; border-left:0; border-top:1px solid var(--border); }
|
|
507
|
+
}
|
|
508
|
+
.dt-kv { display:grid; grid-template-columns:minmax(100px,auto) minmax(0,1fr); gap:5px 12px; font:12px/1.5 ui-monospace,monospace; padding:8px; margin:0; }
|
|
509
|
+
.dt-kv dt { color:var(--muted); overflow-wrap:anywhere; }.dt-kv dd { margin:0; overflow-wrap:anywhere; }
|
|
510
|
+
.dt-note { color:var(--muted); font-size:11px; padding:6px 8px; margin:0; }
|
|
511
|
+
.dt-status-message { color:var(--accent); padding:4px 8px; border-top:1px solid var(--border); font-size:11px; }
|
|
512
|
+
.dt-shell .notice { position:static; padding:5px 9px; border:0; font:11px/1.5 ui-monospace,monospace; box-shadow:none; color:light-dark(#b3261e,#f2b8b5); }
|
|
513
|
+
.dt-scene-content { display:flex; flex-direction:column; flex:1; min-height:0; min-width:0; width:100%; max-width:1000px; align-self:flex-start; }
|
|
514
|
+
.dt-shell .map-controls { flex:1; width:100%; max-width:none; padding:0 16px 16px; border:0; overflow:auto; background:var(--paper); }
|
|
515
|
+
.dt-shell .controls-grid,.dt-shell #controls-modules { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 24px; align-items:start; }
|
|
516
|
+
.dt-shell .controls-grid { border-bottom:1px solid var(--border); padding-bottom:8px; }
|
|
517
|
+
.dt-shell .controls-selection,.dt-shell .controls-runtime { border:0; padding-bottom:0; }
|
|
518
|
+
.dt-shell .map-controls .control-field,.dt-shell .control-module-header label { display:grid; grid-template-columns:min(36%,160px) minmax(0,1fr); align-items:center; gap:12px; min-height:28px; }
|
|
519
|
+
.dt-shell .map-controls .control-field > select,.dt-shell .map-controls .control-field > input[type=number] { justify-self:start; width:min(200px,100%); max-width:100%; }
|
|
520
|
+
.dt-shell .map-controls input[type=checkbox] { appearance:none; -webkit-appearance:none; position:relative; justify-self:start; flex:none; width:34px; height:20px; min-height:20px; margin:0; padding:0; border:1px solid light-dark(#9aa0a6,#7c838d); border-radius:999px; background:light-dark(#e3e7ed,#444950); cursor:pointer; transition:background-color 120ms ease,border-color 120ms ease; }
|
|
521
|
+
.dt-shell .map-controls input[type=checkbox]::before { content:''; position:absolute; top:2px; left:2px; width:14px; height:14px; border-radius:50%; background:light-dark(#fff,#c4c8cf); box-shadow:0 1px 2px #0003; transition:transform 120ms ease,background-color 120ms ease; }
|
|
522
|
+
.dt-shell .map-controls input[type=checkbox]:checked { background:var(--accent); border-color:var(--accent); }
|
|
523
|
+
.dt-shell .map-controls input[type=checkbox]:checked::before { transform:translateX(14px); background:light-dark(#fff,#202124); }
|
|
524
|
+
.dt-shell .map-controls input[type=checkbox]:hover:not(:disabled) { box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 12%,transparent); }
|
|
525
|
+
.dt-shell .map-controls input[type=checkbox]:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
|
|
526
|
+
.dt-shell .map-controls input[type=checkbox]:disabled { cursor:not-allowed; }
|
|
527
|
+
.dt-shell .map-controls .control-field:has(input[type=checkbox]:not(:disabled)) { cursor:pointer; }
|
|
528
|
+
.dt-shell .controls-heading button { min-width:60px; }
|
|
529
|
+
.dt-shell .controls-segments button { padding:4px 10px; border-radius:0; }
|
|
530
|
+
@media (prefers-reduced-motion:reduce) { .dt-shell .map-controls input[type=checkbox],.dt-shell .map-controls input[type=checkbox]::before { transition:none; } }
|
|
531
|
+
.dt-shell .control-module { border:0; border-bottom:1px solid var(--border); border-radius:0; background:var(--paper); margin:0; }
|
|
532
|
+
.dt-shell .control-module-header { padding:5px 0; }.dt-shell .control-module-header label { font-size:12px; font-weight:400; }
|
|
533
|
+
.dt-shell .control-module fieldset { min-width:0; padding:0 0 8px 16px; }.dt-shell .control-module summary { padding:2px 0 5px; }
|
|
534
|
+
.dt-shell .controls-section-title { margin:10px 0 4px; font-size:11px; letter-spacing:0; text-transform:none; }
|
|
535
|
+
.dt-shell .control-field { margin:5px 0; }
|
|
536
|
+
@container(max-width:680px) { .dt-shell .controls-grid,.dt-shell #controls-modules { grid-template-columns:minmax(0,1fr); }.dt-shell .map-controls .control-field,.dt-shell .control-module-header label { grid-template-columns:min(36%,160px) minmax(0,1fr); }.dt-shell .controls-segments button { padding-inline:8px; } }
|
|
537
|
+
.dt-shell .controls-code { background:var(--surface); }.dt-shell .controls-selection .control-field select { max-width:100%; }
|
|
538
|
+
.dt-shell .panel-heading h2 { font:12px Arial,sans-serif; }.dt-shell .review-tabs { margin:0; padding:0; }
|
|
539
|
+
.dt-assets-content { scrollbar-gutter:stable; }
|
|
540
|
+
.dt-font-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,350px),1fr)); max-width:1100px; }
|
|
541
|
+
.dt-font-card { min-width:0; margin:0; padding:14px 12px; border-bottom:1px solid var(--border); }
|
|
542
|
+
.dt-font-card figcaption { margin-bottom:8px; font:12px/1.5 ui-monospace,monospace; }
|
|
543
|
+
.dt-font-sample { min-height:94px; overflow-x:auto; border:1px solid var(--border); border-radius:3px; background:#fff; color:#303b4b; }
|
|
544
|
+
.dt-font-preview { display:block; }
|
|
545
|
+
.dt-font-loading { display:block; padding:15px 10px; font-size:11px; color:#62676e; }
|
|
546
|
+
.dt-sprite-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(112px,1fr)); gap:1px; padding:1px; background:var(--border); }
|
|
547
|
+
.dt-sprite-card { margin:0; min-width:0; padding:10px 8px 8px; background:var(--paper); text-align:center; }
|
|
548
|
+
.dt-sprite-card:hover { background:var(--dt-selected); }
|
|
549
|
+
.dt-sprite-preview { height:60px; display:flex; align-items:center; justify-content:center; border:1px solid #dadce0; border-radius:3px; background:repeating-conic-gradient(#e8eaed 0% 25%,#fff 0% 50%) 0 0 / 12px 12px; color:#62676e; }
|
|
550
|
+
.dt-sprite-card figcaption { margin-top:7px; font:11px/1.4 ui-monospace,monospace; overflow-wrap:anywhere; }
|
|
551
|
+
.dt-sprite-size { display:block; min-height:15px; margin-top:3px; color:var(--muted); font-size:10px; }
|
|
552
|
+
.dt-geometry-json { width:100%; min-height:100px; resize:vertical; background:var(--paper); color:var(--ink); border:1px solid var(--border); font:11px/1.5 ui-monospace,monospace; padding:6px; }
|
|
553
|
+
.dt-vertex { position:absolute; width:10px; height:10px; border:2px solid #1a73e8; background:#fff; transform:translate(-50%,-50%); pointer-events:auto; padding:0; min-height:0; border-radius:1px; cursor:move; touch-action:none; }
|
|
554
|
+
.dt-geometry-handles { position:absolute; inset:0; pointer-events:none; z-index:5; overflow:hidden; }
|
|
555
|
+
.dt-performance { padding:10px; }.dt-performance svg { width:100%; height:90px; display:block; }.dt-performance dl { max-width:600px; }
|
|
556
|
+
.dt-overdraw-help { max-width:640px; margin:10px 10px 0; padding:10px 12px; border:1px solid var(--border); border-left:3px solid var(--accent); border-radius:4px; background:var(--surface); }
|
|
557
|
+
.dt-overdraw-help[hidden] { display:none; }
|
|
558
|
+
.dt-overdraw-help strong { font-size:12px; }.dt-overdraw-help p { margin:5px 0 0; line-height:1.5; }
|
|
559
|
+
.dt-overdraw-legend { display:flex; flex-wrap:wrap; gap:6px 20px; margin:10px 0 8px; }
|
|
560
|
+
.dt-overdraw-key { display:inline-flex; align-items:center; gap:6px; font:11px/1.4 ui-monospace,monospace; }
|
|
561
|
+
.dt-overdraw-swatch { width:24px; height:10px; border:1px solid #0002; border-radius:2px; }
|
|
562
|
+
.dt-overdraw-caption { color:var(--muted); font-size:11px; }
|
|
563
|
+
.dt-badge { background:var(--dt-selected); color:var(--accent); border-radius:7px; padding:0 5px; margin-left:3px; font:10px/1.5 Arial,sans-serif; }
|
|
564
|
+
[data-dock=left] .dt-inspect-grid,[data-dock=right] .dt-inspect-grid { display:flex; flex-direction:column; }
|
|
565
|
+
[data-dock=left] .dt-tree-column,[data-dock=right] .dt-tree-column { flex:0 0 32%; border-right:0; border-bottom:1px solid var(--border); }
|
|
566
|
+
[data-dock=left] .dt-details,[data-dock=right] .dt-details { flex:1; }
|
|
567
|
+
.dt-zoom { padding:8px; }.dt-zoom > select { width:100%; margin-bottom:8px; }
|
|
568
|
+
@container(max-width:680px) { .dt-inspect-grid { display:flex;flex-direction:column; }.dt-tree-column { flex:0 0 32%;border-right:0;border-bottom:1px solid var(--border); }.dt-details { flex:1; } }
|
|
569
|
+
`;
|
|
570
|
+
var devtoolsStyles = legacyStyles + inspectorStyles;
|
|
571
|
+
|
|
572
|
+
// src/workbench-client.ts
|
|
573
|
+
var workbenchClient = String.raw`
|
|
574
|
+
let wbCatalog;
|
|
575
|
+
let wbLastContext = "";
|
|
576
|
+
let wbInterval;
|
|
577
|
+
let wbRefreshing = false;
|
|
578
|
+
let wbError = "";
|
|
579
|
+
let wbLastPublished = 0;
|
|
580
|
+
let wbPickerKey = "";
|
|
581
|
+
let wbTimeOfDay = "";
|
|
582
|
+
const wbPaletteColors = new Map();
|
|
583
|
+
const wbClient = "view-" + Math.random().toString(36).slice(2);
|
|
584
|
+
const wb = (id) => document.getElementById("wb-" + id);
|
|
585
|
+
if (options.workbenchUrl) initializeWorkbench();
|
|
586
|
+
|
|
587
|
+
function initializeWorkbench() {
|
|
588
|
+
wb("theme").addEventListener("change", () => selectWorkbenchAppearance());
|
|
589
|
+
for (const [moment, label] of Object.entries({day:"Day", dawn:"Dawn", dusk:"Dusk", night:"Night"})) {
|
|
590
|
+
const segment = document.createElement("button");
|
|
591
|
+
segment.type = "button"; segment.value = moment; segment.textContent = label;
|
|
592
|
+
segment.setAttribute("role", "radio"); segment.setAttribute("aria-checked", "false");
|
|
593
|
+
segment.tabIndex = -1; segment.disabled = true;
|
|
594
|
+
segment.addEventListener("click", () => selectWorkbenchAppearance(moment));
|
|
595
|
+
segment.addEventListener("keydown", (event) => {
|
|
596
|
+
const direction = {ArrowRight:1, ArrowDown:1, ArrowLeft:-1, ArrowUp:-1}[event.key];
|
|
597
|
+
if (!direction && event.key !== "Home" && event.key !== "End") return;
|
|
598
|
+
event.preventDefault();
|
|
599
|
+
const enabled = [...wb("time-of-day").querySelectorAll("button")].filter((item) => !item.disabled);
|
|
600
|
+
if (!enabled.length) return;
|
|
601
|
+
const index = event.key === "Home" ? 0 : event.key === "End" ? enabled.length - 1 : (enabled.indexOf(segment) + direction + enabled.length) % enabled.length;
|
|
602
|
+
const next = enabled[index];
|
|
603
|
+
selectWorkbenchAppearance(next.value); next.focus();
|
|
604
|
+
});
|
|
605
|
+
wb("time-of-day").append(segment);
|
|
606
|
+
}
|
|
607
|
+
void refreshWorkbench();
|
|
608
|
+
wbInterval = setInterval(refreshWorkbench, 2500);
|
|
609
|
+
addEventListener("beforeunload", () => clearInterval(wbInterval));
|
|
610
|
+
addEventListener("keydown", (event) => {
|
|
611
|
+
if (event.target.closest?.("input,select,textarea,[contenteditable]")) return;
|
|
612
|
+
if (event.key === "i" && !event.metaKey && !event.ctrlKey) setInspectionEnabled(!inspectionEnabled, true);
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function workbenchRequest(path, body) {
|
|
617
|
+
const response = await fetch(options.workbenchUrl + path, body === undefined
|
|
618
|
+
? {cache:"no-store"}
|
|
619
|
+
: {method:"POST", headers:{"Content-Type":"application/json", "X-Tileflow-Workbench":"1"}, body:JSON.stringify(body)});
|
|
620
|
+
const result = await response.json();
|
|
621
|
+
if (!response.ok) throw new Error(result.message || "Map DevTools request failed.");
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function refreshWorkbench() {
|
|
626
|
+
if (wbRefreshing) return;
|
|
627
|
+
wbRefreshing = true;
|
|
628
|
+
try {
|
|
629
|
+
wbCatalog = await workbenchRequest("");
|
|
630
|
+
const side = states[activeSideId];
|
|
631
|
+
const owner = wbCatalog.sides[side.serviceId || side.id];
|
|
632
|
+
resolveWorkbenchAppearance(side, owner);
|
|
633
|
+
if (side.bridgeConnected && side.workbenchMap && side.workbenchTheme && !side.workbenchRestored) {
|
|
634
|
+
side.workbenchRestored = true;
|
|
635
|
+
if (side.id === "left") {
|
|
636
|
+
const query = new URL(location.href).searchParams;
|
|
637
|
+
const requested = owner?.selections.find((entry) => !entry.scene && entry.map === side.workbenchMap && entry.id === query.get("view"));
|
|
638
|
+
if (requested) {
|
|
639
|
+
side.requestedTimeOfDay = query.get("time-of-day");
|
|
640
|
+
applyWorkbenchAppearance(side, requested);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
syncWorkbenchPickers();
|
|
645
|
+
const failure = side.runtimeFailed ? side.runtimeErrors.at(-1) : owner?.status === "invalid" ? owner.diagnostics.map((item) => item.message).join(" ") : wbError;
|
|
646
|
+
wb("notice").hidden = !failure;
|
|
647
|
+
wb("notice").textContent = failure ? (owner?.status === "invalid" ? "Showing the last available preview. " : "") + failure : "";
|
|
648
|
+
await publishWorkbenchContext();
|
|
649
|
+
} catch (error) { showWorkbenchError(error); }
|
|
650
|
+
finally { wbRefreshing = false; }
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function resolveWorkbenchAppearance(side, owner) {
|
|
654
|
+
const source = new URL(side.definition.previewUrl, location.href);
|
|
655
|
+
const startup = owner?.selections.find((entry) => entry.scene && entry.scene === source.searchParams.get("scene"));
|
|
656
|
+
const metadata = side.bridgeConnected ? side.map?.getStyle?.()?.metadata : undefined;
|
|
657
|
+
// The command chooses the map. Never infer it from catalog order or a saved URL's view.
|
|
658
|
+
side.workbenchMap ||= source.searchParams.get("map") || startup?.map || metadata?.["tileflow:map"];
|
|
659
|
+
if (!side.pendingAppearance && metadata?.["tileflow:map"] === side.workbenchMap) side.workbenchTheme = metadata?.["tileflow:theme"] || side.workbenchTheme;
|
|
660
|
+
side.workbenchTheme ||= source.searchParams.get("theme") || startup?.theme;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function applyWorkbenchAppearance(side, selected) {
|
|
664
|
+
if (selected.map !== side.workbenchMap || selected.scene) return;
|
|
665
|
+
const changed = selected.theme !== side.workbenchTheme;
|
|
666
|
+
side.workbenchTheme = selected.theme;
|
|
667
|
+
if (changed || side.pendingAppearance) {
|
|
668
|
+
side.pendingAppearance = selected;
|
|
669
|
+
side.definition.label = selected.label;
|
|
670
|
+
scheduleMapControls();
|
|
671
|
+
void loadMapControls();
|
|
672
|
+
}
|
|
673
|
+
syncWorkbenchPickers();
|
|
674
|
+
writeUrlState();
|
|
675
|
+
if (side.id === "left") {
|
|
676
|
+
const url = new URL(location.href);
|
|
677
|
+
url.searchParams.set("view", selected.id);
|
|
678
|
+
const moment = wbTimeOfDay;
|
|
679
|
+
if (moment) url.searchParams.set("time-of-day", moment); else url.searchParams.delete("time-of-day");
|
|
680
|
+
history.replaceState(null, "", url);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function syncWorkbenchPickers() {
|
|
685
|
+
const side = states[activeSideId];
|
|
686
|
+
const owner = wbCatalog?.sides[side.serviceId || side.id];
|
|
687
|
+
const selections = owner?.selections.filter((entry) => entry.map === side.workbenchMap) || [];
|
|
688
|
+
const selectedTheme = side.workbenchTheme;
|
|
689
|
+
if (!side.camera || !selections.length || !selectedTheme) {
|
|
690
|
+
wbPickerKey = "";
|
|
691
|
+
wb("theme").replaceChildren(new Option("Waiting for map…", ""));
|
|
692
|
+
wb("theme").disabled = true;
|
|
693
|
+
wb("time-of-day-field").hidden = true;
|
|
694
|
+
wbTimeOfDay = "";
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
const variants = owner?.timeOfDay?.[side.workbenchMap] || {};
|
|
698
|
+
const key = JSON.stringify([activeSideId, selectedTheme, selections, variants, side.requestedTimeOfDay]);
|
|
699
|
+
if (key === wbPickerKey) return;
|
|
700
|
+
wbPickerKey = key;
|
|
701
|
+
const allThemes = [...new Set(selections.map((entry) => entry.theme))];
|
|
702
|
+
const family = Object.keys(variants).find((name) => Object.values(variants[name]).includes(selectedTheme));
|
|
703
|
+
const members = new Set(Object.values(variants).flatMap((moments) => Object.values(moments)));
|
|
704
|
+
const themes = allThemes.filter((name) => !members.has(name) || Object.hasOwn(variants, name));
|
|
705
|
+
const moments = family ? variants[family] : {};
|
|
706
|
+
wbTimeOfDay = moments[side.requestedTimeOfDay] === selectedTheme ? side.requestedTimeOfDay : Object.keys(moments).find((moment) => moments[moment] === selectedTheme) || "";
|
|
707
|
+
for (const segment of wb("time-of-day").querySelectorAll("button")) {
|
|
708
|
+
const selected = segment.value === wbTimeOfDay;
|
|
709
|
+
segment.setAttribute("aria-checked", String(selected));
|
|
710
|
+
segment.tabIndex = selected ? 0 : -1;
|
|
711
|
+
segment.disabled = !moments[segment.value];
|
|
712
|
+
}
|
|
713
|
+
wb("time-of-day-field").hidden = !family;
|
|
714
|
+
wb("theme").replaceChildren(...themes.map((theme) => new Option(theme, theme)));
|
|
715
|
+
wb("theme").value = family || selectedTheme;
|
|
716
|
+
wb("theme").disabled = themes.length < 2;
|
|
717
|
+
wb("palettes").replaceChildren(...themes.map(theme => {
|
|
718
|
+
const button=document.createElement("button");button.type="button";button.setAttribute("role","radio");button.setAttribute("aria-checked",String(theme===(family||selectedTheme)));
|
|
719
|
+
const swatches=document.createElement("span");swatches.className="dt-palette-swatches";
|
|
720
|
+
const label=document.createElement("span");label.textContent=theme.replace(/[-_]/g," ");button.append(swatches,label);
|
|
721
|
+
button.onclick=()=>{wb("theme").value=theme;selectWorkbenchAppearance();};
|
|
722
|
+
const concrete=variants[theme]?.[wbTimeOfDay] || variants[theme]?.day || theme;
|
|
723
|
+
const fill=colors=>{if(!button.isConnected)return;swatches.replaceChildren(...colors.map(color=>{const swatch=document.createElement("i");swatch.style.background=color;return swatch;}));};
|
|
724
|
+
if(wbPaletteColors.has(concrete))queueMicrotask(()=>fill(wbPaletteColors.get(concrete)));
|
|
725
|
+
else {
|
|
726
|
+
const url=new URL(side.definition.basePath+"/styles/"+side.workbenchMap+"/"+concrete+".json",location.href);
|
|
727
|
+
fetch(url).then(response=>{if(!response.ok)throw new Error("Palette unavailable");return response.json();}).then(style=>{
|
|
728
|
+
const choices=[/background/,/water/,/land.*(?:park|vegetation)|vegetation/,/building/];
|
|
729
|
+
const colors=choices.map(pattern=>style.layers.flatMap(layer=>pattern.test(layer.id)||pattern.test(layer.type)?Object.entries(layer.paint||{}).filter(([key,value])=>key.endsWith("-color")&&typeof value==="string").map(([,value])=>value):[])[0]).filter(Boolean);
|
|
730
|
+
wbPaletteColors.set(concrete,colors);fill(colors);
|
|
731
|
+
}).catch(()=>{swatches.textContent="—";});
|
|
732
|
+
}
|
|
733
|
+
return button;
|
|
734
|
+
}));
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function selectWorkbenchAppearance(requested = wbTimeOfDay) {
|
|
738
|
+
const side = states[activeSideId];
|
|
739
|
+
const owner = wbCatalog?.sides[side.serviceId || side.id];
|
|
740
|
+
const moments = owner?.timeOfDay?.[side.workbenchMap]?.[wb("theme").value];
|
|
741
|
+
const moment = moments?.[requested] ? requested : moments ? Object.keys(moments)[0] : undefined;
|
|
742
|
+
const theme = moments?.[moment] || wb("theme").value;
|
|
743
|
+
const selected = owner?.selections.find((entry) => !entry.scene && entry.map === side.workbenchMap && entry.theme === theme);
|
|
744
|
+
if (!selected) { wbPickerKey = ""; syncWorkbenchPickers(); return; }
|
|
745
|
+
side.requestedTimeOfDay = moment;
|
|
746
|
+
applyWorkbenchAppearance(side, selected);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function currentWorkbenchDefinition() {
|
|
750
|
+
const side = states[activeSideId];
|
|
751
|
+
const camera = side.camera || canonicalCamera;
|
|
752
|
+
const metadata = side.map?.getStyle?.().metadata;
|
|
753
|
+
if (!camera || !validPortableName(metadata?.["tileflow:map"]) || !validConcreteThemeName(metadata?.["tileflow:theme"]))
|
|
754
|
+
throw new Error("Wait for the current map to finish loading.");
|
|
755
|
+
const container = side.map?.getContainer?.();
|
|
756
|
+
const width = boundedInteger(container?.clientWidth, 64, 4096, 1280);
|
|
757
|
+
const height = boundedInteger(container?.clientHeight, 64, 4096, 800);
|
|
758
|
+
return {map:metadata["tileflow:map"], theme:metadata["tileflow:theme"],
|
|
759
|
+
camera:{type:"center", center:[roundNumber(camera.center[0]),roundNumber(camera.center[1])], zoom:roundNumber(camera.zoom), bearing:roundNumber(camera.bearing), pitch:roundNumber(camera.pitch)},
|
|
760
|
+
viewport:{width,height,dpr:1}, target:{kind:"map"}};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function workbenchContext() {
|
|
764
|
+
const side = states[activeSideId];
|
|
765
|
+
const layerObservation = globalThis.__tileflowDevtoolsUI?.layerContext?.();
|
|
766
|
+
const selected = !layerObservation && selectedInspection?.side === side ? selectedInspection : undefined;
|
|
767
|
+
const feature = selected?.features[selected.featureIndex];
|
|
768
|
+
const layer = feature && side.map?.getStyle?.().layers?.find((entry) => entry.id === feature.layerId);
|
|
769
|
+
const provenance = feature && findSidecarLayer(side.sidecar, feature.layerId);
|
|
770
|
+
let definition;
|
|
771
|
+
try { definition = currentWorkbenchDefinition(); } catch { /* Loading previews have no camera yet. */ }
|
|
772
|
+
const configuration = side.definition.captureConfig ? " --config " + quoteCliArgument(side.definition.captureConfig) : "";
|
|
773
|
+
const semanticOwner = provenance && sidecarLayerWithSemanticAttribution(provenance).semanticAttribution[0]?.semanticOwner;
|
|
774
|
+
const captureCommand = definition ? "tileflow capture" + configuration + " --map " + quoteCliArgument(definition.map) + " --theme " + quoteCliArgument(definition.theme) +
|
|
775
|
+
" --center=" + definition.camera.center.join(",") + " --zoom=" + definition.camera.zoom + " --bearing=" + definition.camera.bearing + " --pitch=" + definition.camera.pitch +
|
|
776
|
+
" --width=" + definition.viewport.width + " --height=" + definition.viewport.height + " --dpr=" + definition.viewport.dpr + " --json" : undefined;
|
|
777
|
+
return {schemaVersion:1, source:"browser-observation", config:side.definition.captureConfig || "tileflow.config.ts",
|
|
778
|
+
status:side.artifactStatus, generation:side.artifactGeneration, inspectionEnabled,
|
|
779
|
+
displayedGeneration:Number(side.frame.contentDocument?.documentElement.dataset.tileflowArtifactGeneration), definition,
|
|
780
|
+
captureSemantics:"Compiled map at the observed camera and viewport; temporary preview edits are excluded.",
|
|
781
|
+
review:{referenceId:null, before:null, after:null, liveMode:stage.dataset.mode, source:"map"},
|
|
782
|
+
commands:{inspect:"tileflow inspect" + configuration + (semanticOwner ? " --domain " + quoteCliArgument(semanticOwner) : " --limit 80") + " --json", capture:captureCommand,
|
|
783
|
+
session:"tileflow inspect session --url " + quoteCliArgument(location.origin) + " --json"},
|
|
784
|
+
selection:layerObservation || (feature ? {coordinate:selected.coordinate,
|
|
785
|
+
provenance:provenance ? sidecarLayerWithSemanticAttribution(provenance).semanticAttribution : [],
|
|
786
|
+
provenanceScope:"Semantic ownership only; exact file, line and token are not inferred.",
|
|
787
|
+
feature, layer:layer ? {id:layer.id,type:layer.type,source:layer.source,sourceLayer:layer["source-layer"],minzoom:layer.minzoom,maxzoom:layer.maxzoom,filter:layer.filter,paint:layer.paint,layout:layer.layout} : undefined} : null)};
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function boundedWorkbenchContext() {
|
|
791
|
+
const observed = workbenchContext();
|
|
792
|
+
const nodeLimit = observed.selection?.kind === "layer" ? 2400 : 200;
|
|
793
|
+
let entries = 0;
|
|
794
|
+
const omitted = [];
|
|
795
|
+
function visit(value, path, depth) {
|
|
796
|
+
if (++entries > nodeLimit || depth > 16) { omitted.push(path); return undefined; }
|
|
797
|
+
if (typeof value === "string") { if (value.length > 512) omitted.push(path); return value.slice(0,512); }
|
|
798
|
+
if (!value || typeof value !== "object") return value;
|
|
799
|
+
if (Array.isArray(value)) {
|
|
800
|
+
if (value.length > 32) omitted.push(path);
|
|
801
|
+
return value.slice(0,32).map((entry,index) => visit(entry,path + "/" + index,depth + 1));
|
|
802
|
+
}
|
|
803
|
+
return Object.fromEntries(Object.entries(value).flatMap(([key,entry]) => {
|
|
804
|
+
const result = visit(entry,path + "/" + key.replaceAll("~","~0").replaceAll("/","~1"),depth + 1);
|
|
805
|
+
return result === undefined ? [] : [[key,result]];
|
|
806
|
+
}));
|
|
807
|
+
}
|
|
808
|
+
const context = visit(observed,"",0);
|
|
809
|
+
context.projection = {truncated:omitted.length > 0, omitted:omitted.slice(0,32)};
|
|
810
|
+
return context;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function publishWorkbenchContext() {
|
|
814
|
+
const side = states[activeSideId];
|
|
815
|
+
if (!side.bridgeReady) return;
|
|
816
|
+
const context = boundedWorkbenchContext();
|
|
817
|
+
const key = JSON.stringify(context);
|
|
818
|
+
if (key === wbLastContext && Date.now() - wbLastPublished < 60_000) return;
|
|
819
|
+
await workbenchRequest("/context", {client:wbClient, side:side.serviceId || side.id, generation:side.artifactGeneration, context});
|
|
820
|
+
wbLastContext = key;
|
|
821
|
+
wbLastPublished = Date.now();
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function featureDisplayName(feature) {
|
|
825
|
+
return String(feature.properties?.name || feature.properties?.class || feature.layerId.replace(/^tileflow-/, "").replace(/-render-/, " · ") || "Map element")
|
|
826
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2").replaceAll("_", " ").replaceAll("-", " ").replace(/^./, (letter) => letter.toUpperCase());
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function renderFeatureSummary(side, feature, layer, picker, raw) {
|
|
830
|
+
const section = document.createElement("section"); section.className = "feature-summary";
|
|
831
|
+
const heading = document.createElement("h3");
|
|
832
|
+
heading.textContent = featureDisplayName(feature);
|
|
833
|
+
const kind = document.createElement("div"); kind.className = "compact-note";
|
|
834
|
+
const kinds = {fill:"Area", line:"Line", symbol:"Label or icon", "fill-extrusion":"3D building", circle:"Point", raster:"Imagery", hillshade:"Terrain shading", background:"Background"};
|
|
835
|
+
kind.textContent = (kinds[layer?.type] || layer?.type || feature.geometryType) + " · Zoom " + (side.camera?.zoom ?? 0).toFixed(2);
|
|
836
|
+
const headingStyle = document.createElement("h4"); headingStyle.textContent = "Appearance here";
|
|
837
|
+
section.append(heading, kind, headingStyle);
|
|
838
|
+
const values = document.createElement("dl"); values.className = "property-list";
|
|
839
|
+
const labels = {"fill-color":"Fill color", "fill-opacity":"Opacity", "fill-outline-color":"Outline", "line-color":"Line color", "line-width":"Width", "line-opacity":"Opacity", "line-gap-width":"Gap", "text-color":"Text color", "text-halo-color":"Text outline", "text-halo-width":"Outline width", "text-opacity":"Text opacity", "text-size":"Text size", "icon-color":"Icon color", "icon-opacity":"Icon opacity", "fill-extrusion-color":"Building color", "fill-extrusion-height":"Height", "fill-extrusion-opacity":"Opacity"};
|
|
840
|
+
for (const [name, rawValue] of Object.entries({...layer?.paint, ...(layer?.layout?.["text-size"] !== undefined ? {"text-size":layer.layout["text-size"]} : {})}).slice(0,8)) {
|
|
841
|
+
const value = evaluateBasicZoomValue(rawValue, side.camera?.zoom ?? 0);
|
|
842
|
+
const label = document.createElement("dt"); label.textContent = labels[name] || name.replaceAll("-", " "); label.title = name;
|
|
843
|
+
const detail = document.createElement("dd"); detail.textContent = value === unsupportedValue ? "Varies by feature" : formatCurveValue(value);
|
|
844
|
+
if (typeof value === "string" && /^(#[0-9a-f]{3,8}|rgba?\(|hsla?\()/i.test(value)) {
|
|
845
|
+
const swatch = document.createElement("span"); swatch.className = "color-swatch"; swatch.style.backgroundColor = value; swatch.setAttribute("aria-hidden", "true"); detail.append(swatch);
|
|
846
|
+
}
|
|
847
|
+
values.append(label,detail);
|
|
848
|
+
}
|
|
849
|
+
section.append(values);
|
|
850
|
+
const provenance = findSidecarLayer(side.sidecar, feature.layerId);
|
|
851
|
+
const owners = provenance ? sidecarLayerWithSemanticAttribution(provenance).semanticAttribution : [];
|
|
852
|
+
const label = document.createElement("h4"); label.textContent = "Where to edit";
|
|
853
|
+
section.append(label);
|
|
854
|
+
const paths = [...new Set(owners.flatMap((owner) => owner.authoringPaths))];
|
|
855
|
+
for (const path of paths.slice(0,8)) { const code = document.createElement("code"); code.className = "authoring-path"; code.textContent = path; section.append(code); }
|
|
856
|
+
const note = document.createElement("p"); note.className = "compact-note";
|
|
857
|
+
note.textContent = paths.length ? "Edit these configuration sections in your editor or ask your coding agent. The map updates here." : "No semantic configuration path is available for this layer.";
|
|
858
|
+
section.append(note);
|
|
859
|
+
const details = document.createElement("details"); const summary = document.createElement("summary"); summary.textContent = "Feature data";
|
|
860
|
+
const provenanceNote = document.createElement("p"); provenanceNote.className = "compact-note"; provenanceNote.textContent = "Configuration paths describe semantic ownership. Exact source lines and tokens are not reported.";
|
|
861
|
+
details.append(summary,provenanceNote,raw);
|
|
862
|
+
inspectionOutput.append(section,picker,details);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function showWorkbenchError(error) {
|
|
866
|
+
const message = safeErrorMessage(error);
|
|
867
|
+
wbError = message;
|
|
868
|
+
setFeedback(message);
|
|
869
|
+
wb("notice").textContent = message; wb("notice").hidden = false;
|
|
870
|
+
}
|
|
871
|
+
`;
|
|
872
|
+
|
|
873
|
+
// src/workbench-controls.ts
|
|
874
|
+
var controlsStyles = `
|
|
875
|
+
.map-controls { flex:0 0 360px; width:360px; max-width:45vw; min-height:0; overflow:auto; border-left:1px solid var(--border); background:var(--paper); padding:0 18px 18px; }
|
|
876
|
+
.map-controls[hidden] { display:none; }
|
|
877
|
+
.controls-heading { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:8px 0; border-bottom:1px solid var(--border); }
|
|
878
|
+
.controls-heading h2 { margin:0; font:inherit; }
|
|
879
|
+
.controls-segments { display:flex; min-width:0; width:max-content; max-width:100%; border:1px solid var(--border); border-radius:4px; overflow:hidden; }
|
|
880
|
+
.controls-segments button { flex:1; min-width:0; padding:4px 10px; border:0; border-radius:0; background:var(--paper); color:var(--muted); cursor:pointer; }
|
|
881
|
+
.controls-segments button + button { border-left:1px solid var(--border); }
|
|
882
|
+
.controls-segments button[aria-checked=true] { color:var(--accent); background:var(--dt-selected,var(--surface)); box-shadow:inset 0 -2px var(--accent); }
|
|
883
|
+
.controls-segments button:disabled { opacity:.4; cursor:not-allowed; }
|
|
884
|
+
.controls-selection { border-bottom:1px solid var(--border); padding-bottom:8px; }
|
|
885
|
+
.controls-selection .control-field select { width:190px; max-width:70%; }
|
|
886
|
+
.controls-selection [hidden] { display:none; }
|
|
887
|
+
.controls-section-title { font-size:10px; letter-spacing:.1em; text-transform:uppercase; color:var(--muted); margin:20px 0 9px; }
|
|
888
|
+
.control-module { border:1px solid var(--border); border-radius:8px; margin:7px 0; background:#fffdf7; overflow:hidden; }
|
|
889
|
+
.control-module-header { display:flex; align-items:center; justify-content:space-between; padding:10px 12px; gap:8px; }
|
|
890
|
+
.control-module-header label { display:flex; align-items:center; justify-content:space-between; flex:1; flex-direction:row; gap:10px; font-size:13px; font-weight:600; cursor:pointer; }
|
|
891
|
+
.control-module-header input { appearance:none; width:30px; height:18px; min-height:0; padding:0; border:0; border-radius:12px; background:#d5cec1; cursor:pointer; position:relative; flex:none; }
|
|
892
|
+
.control-module-header input::before { content:''; position:absolute; width:12px; height:12px; top:3px; left:3px; border-radius:50%; background:#fff; }
|
|
893
|
+
.control-module-header input:checked { background:var(--accent); }
|
|
894
|
+
.control-module-header input:checked::before { transform:translateX(12px); }
|
|
895
|
+
.control-module-header input:focus-visible { outline:2px solid var(--accent); outline-offset:3px; }
|
|
896
|
+
.control-module summary { cursor:pointer; padding:0 12px 10px; font-size:11px; color:var(--muted); }
|
|
897
|
+
.control-module fieldset { border:0; padding:0 12px 12px; margin:0; }
|
|
898
|
+
.control-module fieldset:disabled { opacity:.45; }
|
|
899
|
+
.control-field { display:flex; align-items:center; justify-content:space-between; flex-direction:row; gap:12px; font-size:12px; margin:9px 0; }
|
|
900
|
+
.control-field select,.control-field input[type=number] { width:132px; min-width:0; padding:5px 7px; font:inherit; }
|
|
901
|
+
.control-field input[type=checkbox] { accent-color:var(--accent); }
|
|
902
|
+
.controls-runtime { border:0; border-bottom:1px solid var(--border); padding:0 0 12px; margin:0; min-width:0; }
|
|
903
|
+
#controls-modules { border:0; padding:0; margin:0; min-width:0; }
|
|
904
|
+
.controls-runtime:disabled,#controls-modules:disabled { opacity:.6; }
|
|
905
|
+
#controls-status { font-size:12px; line-height:1.5; color:var(--muted); margin:8px 0; overflow-wrap:anywhere; }
|
|
906
|
+
#controls-status[data-error=true] { color:#a32e36; }
|
|
907
|
+
@media(max-width:760px) { .workspace:has(.map-controls:not([hidden])) { display:flex; flex-direction:column; } .map-controls { flex:0 1 55vh; width:auto; max-width:none; border-left:0; border-top:1px solid var(--border); } .workspace:has(.map-controls:not([hidden])) .stage { flex:1 0 35vh; } }
|
|
908
|
+
`;
|
|
909
|
+
var controlsPanel = `
|
|
910
|
+
<aside class="map-controls" id="map-controls" aria-label="Scene settings" tabindex="-1" hidden>
|
|
911
|
+
<div class="controls-heading"><h2>Styles</h2><button id="controls-reset">Reset</button></div>
|
|
912
|
+
<div class="controls-grid">
|
|
913
|
+
<div class="controls-selection workbench-only" id="controls-selection" hidden>
|
|
914
|
+
<div class="controls-section-title">Appearance</div>
|
|
915
|
+
<div class="control-field" id="wb-time-of-day-field" hidden><span>Time of day</span><div class="controls-segments" id="wb-time-of-day" role="radiogroup" aria-label="Time of day"></div></div>
|
|
916
|
+
<div class="control-field dt-palette-field" id="wb-theme-field"><span>Theme</span><select id="wb-theme" aria-label="Palette" disabled hidden><option value="">Waiting for map\u2026</option></select><div id="wb-palettes" class="dt-palettes" role="radiogroup" aria-label="Theme palette"></div></div>
|
|
917
|
+
</div>
|
|
918
|
+
<fieldset class="controls-runtime" id="controls-runtime">
|
|
919
|
+
<div class="controls-section-title">Preview rendering</div>
|
|
920
|
+
<label class="control-field">3D view<input id="controls-3d" type="checkbox" role="switch"></label>
|
|
921
|
+
<label class="control-field">Building animation<select id="controls-animation" hidden><option value="0">Off</option><option value="800">Grow</option><option value="fade">Fade</option></select><span class="controls-segments" id="controls-animation-segments" role="radiogroup" aria-label="Building animation"></span></label>
|
|
922
|
+
</fieldset>
|
|
923
|
+
</div>
|
|
924
|
+
<div class="controls-section-title">Map modules</div>
|
|
925
|
+
<fieldset id="controls-modules"></fieldset>
|
|
926
|
+
<p id="controls-status" role="status" hidden></p>
|
|
927
|
+
</aside>
|
|
928
|
+
`;
|
|
929
|
+
var controlsClient = String.raw`
|
|
930
|
+
const ctl = (id) => document.getElementById("controls-" + id);
|
|
931
|
+
const controlsPanel = document.getElementById("map-controls");
|
|
932
|
+
const controlsButton = document.getElementById("controls-toggle");
|
|
933
|
+
let controlsKey = "", controlsSequence = 0, controlsModel, controlsDraft = {}, controlsBusy = false;
|
|
934
|
+
let controlsApplyTimer, controlsRevision = 0, controlsPending = false, controlsResetPending = false;
|
|
935
|
+
let controlsLoadingKey = "", controlsBindings = [], controlsLastError = "";
|
|
936
|
+
let controlsHistoryCurrent, controlsInitial, controlsHistoryQuiet = 0;
|
|
937
|
+
const controlsUndo = [], controlsRedo = [];
|
|
938
|
+
ctl("selection").hidden = !options.workbenchUrl;
|
|
939
|
+
controlsButton.addEventListener("click", () => toggleMapControls(controlsPanel.hidden, true));
|
|
940
|
+
ctl("reset").addEventListener("click", () => {
|
|
941
|
+
if (!controlsModel) return;
|
|
942
|
+
controlsDraft = {};
|
|
943
|
+
ctl("3d").checked = false; ctl("animation").value = "0";
|
|
944
|
+
renderMapControls();
|
|
945
|
+
scheduleMapControls(true);
|
|
946
|
+
});
|
|
947
|
+
for (const name of ["3d", "animation"]) ctl(name).addEventListener("change", () => { syncAnimationSegments(); scheduleMapControls(); });
|
|
948
|
+
ctl("3d").addEventListener("change", () => {
|
|
949
|
+
const map = states[activeSideId].map;
|
|
950
|
+
if (map) map.easeTo({pitch:ctl("3d").checked ? Math.min(60,map.getMaxPitch()) : 0, duration:matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 350}, {tileflowDevtools:true});
|
|
951
|
+
});
|
|
952
|
+
for (const [value,label] of [["0","Off"],["800","Grow"],["fade","Fade"]]) {
|
|
953
|
+
const button=document.createElement("button"); button.type="button";button.value=value;button.textContent=label;button.setAttribute("role","radio");
|
|
954
|
+
button.addEventListener("click",()=>{ctl("animation").value=value;ctl("animation").dispatchEvent(new Event("change"));});ctl("animation-segments").append(button);
|
|
955
|
+
}
|
|
956
|
+
function syncAnimationSegments(){for(const button of ctl("animation-segments").children)button.setAttribute("aria-checked",String(button.value===ctl("animation").value));}
|
|
957
|
+
syncAnimationSegments();
|
|
958
|
+
addEventListener("keydown", (event) => { if (event.key === "Escape" && !controlsPanel.hidden) toggleMapControls(false, true); });
|
|
959
|
+
const controlsTimer = setInterval(() => { if (!controlsPanel.hidden) void loadMapControls(); }, 1000);
|
|
960
|
+
addEventListener("beforeunload", () => { clearInterval(controlsTimer); cancelMapControlsApply(); });
|
|
961
|
+
|
|
962
|
+
function cancelMapControlsApply() {
|
|
963
|
+
clearTimeout(controlsApplyTimer); controlsApplyTimer = undefined;
|
|
964
|
+
controlsPending = false; controlsResetPending = false; controlsRevision++;
|
|
965
|
+
}
|
|
966
|
+
function setMapControlsDisabled(disabled) {
|
|
967
|
+
for (const name of ["modules", "runtime", "reset"]) ctl(name).disabled = disabled;
|
|
968
|
+
}
|
|
969
|
+
function scheduleMapControls(reset = false) {
|
|
970
|
+
clearTimeout(controlsApplyTimer);
|
|
971
|
+
controlsRevision++; controlsPending = true; controlsResetPending = reset;
|
|
972
|
+
controlsNotice(reset ? "Restoring the original map…" : "Updating preview…");
|
|
973
|
+
controlsApplyTimer = setTimeout(() => {
|
|
974
|
+
controlsApplyTimer = undefined;
|
|
975
|
+
void applyMapControls();
|
|
976
|
+
}, reset ? 0 : 300);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function toggleMapControls(open, focus = false) {
|
|
980
|
+
if (globalThis.__tileflowDevtoolsUI) { globalThis.__tileflowDevtoolsUI.controls(open); return; }
|
|
981
|
+
if (open) {
|
|
982
|
+
setInspectionEnabled(false);
|
|
983
|
+
}
|
|
984
|
+
controlsPanel.hidden = !open;
|
|
985
|
+
controlsButton.setAttribute("aria-expanded", String(open));
|
|
986
|
+
for (const side of Object.values(states)) side.map?.resize?.();
|
|
987
|
+
if (open) void loadMapControls();
|
|
988
|
+
if (focus) (open ? controlsPanel : controlsButton).focus();
|
|
989
|
+
}
|
|
990
|
+
function controlsNotice(text, error = false) {
|
|
991
|
+
ctl("status").hidden = !error;
|
|
992
|
+
ctl("status").textContent = error ? text : "";
|
|
993
|
+
ctl("status").dataset.error = String(error);
|
|
994
|
+
}
|
|
995
|
+
function controlsIdentity(side) {
|
|
996
|
+
const meta = side.map?.getStyle?.()?.metadata;
|
|
997
|
+
return meta ? {map:meta["tileflow:map"], theme:meta["tileflow:theme"]} : undefined;
|
|
998
|
+
}
|
|
999
|
+
function livePreviewUrl(side) {
|
|
1000
|
+
return new URL(side.frame.contentWindow.location.href, location.href);
|
|
1001
|
+
}
|
|
1002
|
+
async function loadMapControls() {
|
|
1003
|
+
const side = states[activeSideId], identity = controlsIdentity(side);
|
|
1004
|
+
if (!side.bridgeConnected || !identity?.map || !identity.theme) {
|
|
1005
|
+
setMapControlsDisabled(!side.bridgeConnected || !controlsModel);
|
|
1006
|
+
controlsNotice("Waiting for the map to finish loading…");
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
identity.theme = side.workbenchTheme || identity.theme;
|
|
1010
|
+
const key = JSON.stringify([side.id, side.definition.basePath, identity.map, side.loadEpoch]);
|
|
1011
|
+
const epoch = side.loadEpoch;
|
|
1012
|
+
const loadingKey = key + identity.theme;
|
|
1013
|
+
if (key === controlsKey && controlsModel?.theme === identity.theme) {
|
|
1014
|
+
setMapControlsDisabled(false);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (loadingKey === controlsLoadingKey) return;
|
|
1018
|
+
const preserve = key === controlsKey && controlsModel;
|
|
1019
|
+
if (!preserve) {
|
|
1020
|
+
const pendingAppearance = side.pendingAppearance;
|
|
1021
|
+
cancelMapControlsApply();
|
|
1022
|
+
controlsPending = Boolean(pendingAppearance);
|
|
1023
|
+
controlsKey = key;
|
|
1024
|
+
controlsModel = undefined;
|
|
1025
|
+
ctl("modules").replaceChildren(); setMapControlsDisabled(true);
|
|
1026
|
+
}
|
|
1027
|
+
controlsLoadingKey = loadingKey;
|
|
1028
|
+
const sequence = ++controlsSequence;
|
|
1029
|
+
controlsNotice("Reading this map’s modules…");
|
|
1030
|
+
try {
|
|
1031
|
+
const url = new URL(side.definition.basePath + "/__controls", location.href);
|
|
1032
|
+
url.search = new URLSearchParams(identity).toString();
|
|
1033
|
+
const response = await fetch(url, {cache:"no-store"});
|
|
1034
|
+
const model = await response.json();
|
|
1035
|
+
if (!response.ok) throw new Error(model.error || "Controls are unavailable for this preview.");
|
|
1036
|
+
if (sequence !== controlsSequence || activeSideId !== side.id || key !== controlsKey || epoch !== side.loadEpoch || identity.theme !== (side.workbenchTheme || controlsIdentity(side)?.theme)) return;
|
|
1037
|
+
if (preserve) {
|
|
1038
|
+
// Keep the actual inputs and their handlers mounted, including focus/selection.
|
|
1039
|
+
for (const module of controlsModel.modules) {
|
|
1040
|
+
const updated = model.modules.find((item) => item.id === module.id);
|
|
1041
|
+
if (!updated) continue;
|
|
1042
|
+
module.enabled = updated.enabled;
|
|
1043
|
+
for (const field of module.fields) Object.assign(field, updated.fields.find((item) => item.path === field.path));
|
|
1044
|
+
}
|
|
1045
|
+
controlsModel.theme = model.theme;
|
|
1046
|
+
for (const sync of controlsBindings) sync();
|
|
1047
|
+
} else {
|
|
1048
|
+
controlsModel = {...model, side, key, epoch};
|
|
1049
|
+
const query = livePreviewUrl(side).searchParams;
|
|
1050
|
+
controlsDraft = JSON.parse(query.get("controls") || "{}");
|
|
1051
|
+
side.appliedSceneStyle ||= JSON.stringify([identity.map, controlsIdentity(side).theme, controlsDraft]);
|
|
1052
|
+
ctl("3d").checked = query.get("buildings3d") === "on";
|
|
1053
|
+
ctl("animation").value = query.get("animation") === "fade" ? "fade" : Number(query.get("animation")) > 0 ? "800" : "0"; syncAnimationSegments();
|
|
1054
|
+
renderMapControls();
|
|
1055
|
+
controlsUndo.length = controlsRedo.length = 0;
|
|
1056
|
+
controlsHistoryCurrent = globalThis.__tileflowScene.snapshot();
|
|
1057
|
+
controlsInitial = structuredClone(controlsHistoryCurrent);
|
|
1058
|
+
}
|
|
1059
|
+
setMapControlsDisabled(false);
|
|
1060
|
+
controlsNotice(Object.keys(controlsDraft).length ? "Preview overrides applied." : "Using the map’s original settings.");
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
if (sequence === controlsSequence) { if (!preserve) controlsKey = ""; controlsNotice(safeErrorMessage(error), true); }
|
|
1063
|
+
} finally {
|
|
1064
|
+
if (sequence === controlsSequence) {
|
|
1065
|
+
controlsLoadingKey = "";
|
|
1066
|
+
if (controlsPending && controlsApplyTimer === undefined) void applyMapControls();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function controlValue(object, path) { return path.split(".").reduce((value, key) => value?.[key], object); }
|
|
1071
|
+
function changeControl(module, path, value, original) {
|
|
1072
|
+
const patch = controlsDraft[module] ||= {};
|
|
1073
|
+
const parts = path.split("."); const leaf = parts.pop();
|
|
1074
|
+
let parent = patch;
|
|
1075
|
+
for (const part of parts) parent = parent[part] ||= {};
|
|
1076
|
+
if (value === original) delete parent[leaf]; else parent[leaf] = value;
|
|
1077
|
+
for (const [key, item] of Object.entries(patch)) if (item && typeof item === "object" && !Object.keys(item).length) delete patch[key];
|
|
1078
|
+
if (!Object.keys(patch).length) delete controlsDraft[module];
|
|
1079
|
+
scheduleMapControls();
|
|
1080
|
+
}
|
|
1081
|
+
function renderMapControls(expanded = [...ctl("modules").querySelectorAll("details[open]")].map((item) => item.dataset.module)) {
|
|
1082
|
+
ctl("modules").replaceChildren();
|
|
1083
|
+
controlsBindings = [];
|
|
1084
|
+
const names = {poi:"Points of interest", land:"Land", water:"Water", nautical:"Nautical", roads:"Roads", buildings:"Buildings", boundaries:"Boundaries", labels:"Labels", aeroways:"Airports", transit:"Transit", vegetation:"Vegetation", addresses:"Addresses", landforms:"Landforms"};
|
|
1085
|
+
const modules = [...controlsModel.modules].sort((a,b) => {
|
|
1086
|
+
const priority = {vegetation:0, buildings:1};
|
|
1087
|
+
return (priority[a.id] ?? 2) - (priority[b.id] ?? 2);
|
|
1088
|
+
});
|
|
1089
|
+
for (const module of modules) {
|
|
1090
|
+
const card = document.createElement("section"); card.className = "control-module";
|
|
1091
|
+
const row = document.createElement("div"); row.className = "control-module-header";
|
|
1092
|
+
const label = document.createElement("label"); label.textContent = names[module.id] || module.id;
|
|
1093
|
+
const toggle = document.createElement("input"); toggle.type = "checkbox"; toggle.checked = controlsDraft[module.id]?.enabled ?? module.enabled;
|
|
1094
|
+
toggle.setAttribute("role", "switch");
|
|
1095
|
+
toggle.setAttribute("aria-label", "Enable " + (names[module.id] || module.id));
|
|
1096
|
+
label.append(toggle); row.append(label); card.append(row);
|
|
1097
|
+
const fieldset = document.createElement("fieldset"); fieldset.disabled = !toggle.checked;
|
|
1098
|
+
controlsBindings.push(() => {
|
|
1099
|
+
if (document.activeElement !== toggle) toggle.checked = controlsDraft[module.id]?.enabled ?? module.enabled;
|
|
1100
|
+
fieldset.disabled = !toggle.checked;
|
|
1101
|
+
});
|
|
1102
|
+
toggle.addEventListener("change", () => { changeControl(module.id, "enabled", toggle.checked, module.enabled); fieldset.disabled = !toggle.checked; });
|
|
1103
|
+
if (module.fields.length) {
|
|
1104
|
+
const details = document.createElement("details");
|
|
1105
|
+
details.dataset.module = module.id; details.open = expanded.includes(module.id);
|
|
1106
|
+
const summary = document.createElement("summary"); summary.textContent = "Appearance & behavior";
|
|
1107
|
+
details.append(summary, fieldset); card.append(details);
|
|
1108
|
+
for (const field of module.fields) {
|
|
1109
|
+
const line = document.createElement("label"); line.className = "control-field";
|
|
1110
|
+
const title = document.createElement("span"); title.textContent = field.label;
|
|
1111
|
+
const input = document.createElement(field.choices ? "select" : "input");
|
|
1112
|
+
const override = controlValue(controlsDraft[module.id], field.path);
|
|
1113
|
+
const value = field.inherit && override === undefined ? "inherit" : override ?? field.value;
|
|
1114
|
+
const labels = {"3d":"Always 3D",auto:"Follow 2D / 3D view",flat:"Flat (2D)",none:"Hidden",style:"Map light",sun:"Scene sun",faceted:"Faceted",smooth:"Smooth",toon:"Toon"};
|
|
1115
|
+
if (field.choices) {
|
|
1116
|
+
const choices = field.choices.map((choice) => new Option(labels[choice] || choice, choice));
|
|
1117
|
+
if (field.inherit) choices.unshift(new Option("Auto · " + (labels[field.value] || field.value), "inherit"));
|
|
1118
|
+
input.replaceChildren(...choices); input.value = String(value);
|
|
1119
|
+
} else if (typeof field.value === "boolean") { input.type = "checkbox"; input.setAttribute("role", "switch"); input.checked = value; }
|
|
1120
|
+
else { input.type = "number"; input.required = true; input.min = String(field.min); input.max = String(field.max); input.step = "any"; input.value = String(value); }
|
|
1121
|
+
controlsBindings.push(() => {
|
|
1122
|
+
if (document.activeElement === input || !input.checkValidity()) return;
|
|
1123
|
+
const override = controlValue(controlsDraft[module.id], field.path);
|
|
1124
|
+
const value = field.inherit && override === undefined ? "inherit" : override ?? field.value;
|
|
1125
|
+
if (input.type === "checkbox") input.checked = value;
|
|
1126
|
+
else input.value = String(value);
|
|
1127
|
+
if (field.inherit) input.options[0].textContent = "Auto · " + (labels[field.value] || field.value);
|
|
1128
|
+
});
|
|
1129
|
+
input.addEventListener(input.type === "number" ? "input" : "change", () => {
|
|
1130
|
+
if (!input.checkValidity()) { cancelMapControlsApply(); controlsNotice("Enter a valid value to update the preview.", true); return; }
|
|
1131
|
+
const value = field.inherit && input.value === "inherit" ? undefined : input.type === "checkbox" ? input.checked : input.type === "number" ? Number(input.value) : input.value;
|
|
1132
|
+
changeControl(module.id, field.path, value, field.inherit ? undefined : field.value);
|
|
1133
|
+
});
|
|
1134
|
+
line.append(title, input); fieldset.append(line);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
ctl("modules").append(card);
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
async function applyMapControls() {
|
|
1141
|
+
if (!controlsModel || controlsBusy || !controlsPending) return;
|
|
1142
|
+
const revision = controlsRevision, reset = controlsResetPending;
|
|
1143
|
+
const {side, map, key} = controlsModel;
|
|
1144
|
+
const theme = side.workbenchTheme || controlsModel.theme;
|
|
1145
|
+
const epoch = side.loadEpoch, source = side.frame.src;
|
|
1146
|
+
if (!side.bridgeConnected || side.id !== activeSideId || side.loadEpoch !== controlsModel.epoch) return;
|
|
1147
|
+
if (!reset && [...ctl("modules").querySelectorAll("input")].some((input) => !input.checkValidity())) {
|
|
1148
|
+
controlsPending = false;
|
|
1149
|
+
controlsNotice("Enter valid field values to update the preview.", true); return;
|
|
1150
|
+
}
|
|
1151
|
+
const draft = reset ? {} : structuredClone(controlsDraft);
|
|
1152
|
+
const next = {map, theme, controls:draft, buildings3d:!reset && ctl("3d").checked, animation:reset ? 0 : ctl("animation").value === "fade" ? "fade" : Number(ctl("animation").value)};
|
|
1153
|
+
const styleKey = JSON.stringify([map, theme, draft]);
|
|
1154
|
+
const isCurrent = () => revision === controlsRevision && controlsKey === key && side.id === activeSideId && side.bridgeConnected && epoch === side.loadEpoch && source === side.frame.src;
|
|
1155
|
+
const before = controlsHistoryCurrent && {...controlsHistoryCurrent, style:side.map.getStyle()};
|
|
1156
|
+
controlsBusy = true; controlsPending = false; controlsLastError = "";
|
|
1157
|
+
controlsNotice("Compiling preview…");
|
|
1158
|
+
try {
|
|
1159
|
+
if (styleKey !== side.appliedSceneStyle) {
|
|
1160
|
+
const style = new URL(side.definition.basePath + "/styles/" + map + "/" + theme + ".json", location.href);
|
|
1161
|
+
if (Object.keys(draft).length) style.searchParams.set("controls", JSON.stringify(draft));
|
|
1162
|
+
const response = await fetch(style, {cache:"no-store"});
|
|
1163
|
+
const document = await response.json();
|
|
1164
|
+
if (!response.ok) throw new Error(document.error || "Unable to compile these settings.");
|
|
1165
|
+
next.style = document;
|
|
1166
|
+
}
|
|
1167
|
+
if (!isCurrent()) return;
|
|
1168
|
+
const apply = side.frame.contentWindow.__tileflowApplyScene;
|
|
1169
|
+
if (!apply) throw new Error("Reload DevTools once to enable live scene updates.");
|
|
1170
|
+
if (!await apply(JSON.stringify(next), isCurrent)) return;
|
|
1171
|
+
side.appliedSceneStyle = styleKey;
|
|
1172
|
+
const after = globalThis.__tileflowScene.snapshot();
|
|
1173
|
+
if (!controlsHistoryQuiet && before && JSON.stringify([before.theme,before.controls,before.buildings3d,before.animation]) !== JSON.stringify([after.theme,after.controls,after.buildings3d,after.animation])) {
|
|
1174
|
+
controlsUndo.push({before,after,time:performance.now()}); if(controlsUndo.length>100)controlsUndo.shift(); controlsRedo.length=0;
|
|
1175
|
+
}
|
|
1176
|
+
controlsHistoryCurrent=after;
|
|
1177
|
+
side.pendingAppearance = undefined;
|
|
1178
|
+
side.definition.previewUrl = livePreviewUrl(side).href;
|
|
1179
|
+
side.sidecarAbort?.abort(); side.sidecar = undefined; side.sidecarPromise = undefined;
|
|
1180
|
+
if (selectedInspection?.side === side) void loadSidecar(side);
|
|
1181
|
+
if (isCurrent()) controlsNotice("");
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
if (revision === controlsRevision && controlsKey === key) { controlsLastError = safeErrorMessage(error); controlsNotice(controlsLastError + " The previous map is still shown.", true); }
|
|
1184
|
+
} finally {
|
|
1185
|
+
controlsBusy = false;
|
|
1186
|
+
if (controlsPending && controlsApplyTimer === undefined) void applyMapControls();
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
globalThis.__tileflowScene = {
|
|
1190
|
+
get history() {return {undo:controlsUndo.at(-1)?.time || 0,redo:controlsRedo.at(-1)?.redoTime || 0,changed:controlsUndo.length>0||controlsRedo.length>0};},
|
|
1191
|
+
async undo() {const entry=controlsUndo.at(-1);if(!entry)return;await this.restore(entry.before);controlsUndo.pop();entry.redoTime=performance.now();controlsRedo.push(entry);},
|
|
1192
|
+
async redo() {const entry=controlsRedo.at(-1);if(!entry)return;await this.restore(entry.after);controlsRedo.pop();entry.time=performance.now();controlsUndo.push(entry);},
|
|
1193
|
+
async reset() {if(controlsInitial)await this.restore(controlsInitial);controlsUndo.length=controlsRedo.length=0;},
|
|
1194
|
+
audit(active) {controlsHistoryQuiet=Math.max(0,controlsHistoryQuiet+(active?1:-1));},
|
|
1195
|
+
variants() {
|
|
1196
|
+
const side = states[activeSideId], owner = wbCatalog?.sides[side.serviceId || side.id];
|
|
1197
|
+
const variants = owner?.timeOfDay?.[side.workbenchMap] || {};
|
|
1198
|
+
return (owner?.selections || []).filter(entry => entry.map === side.workbenchMap && !entry.scene).map(entry => {
|
|
1199
|
+
const family = Object.keys(variants).find(name => Object.values(variants[name]).includes(entry.theme));
|
|
1200
|
+
return {theme:entry.theme, palette:family || entry.theme, timeOfDay:family ? Object.keys(variants[family]).find(moment => variants[family][moment] === entry.theme) : undefined};
|
|
1201
|
+
});
|
|
1202
|
+
},
|
|
1203
|
+
snapshot() {
|
|
1204
|
+
const side = states[activeSideId];
|
|
1205
|
+
return {side:side.id, theme:side.workbenchTheme || controlsIdentity(side)?.theme, controls:structuredClone(controlsDraft), buildings3d:ctl("3d").checked, animation:ctl("animation").value, style:side.map?.getStyle()};
|
|
1206
|
+
},
|
|
1207
|
+
async style(theme, signal) {
|
|
1208
|
+
const side = states[activeSideId], identity = controlsIdentity(side);
|
|
1209
|
+
const url = new URL(side.definition.basePath + "/styles/" + identity.map + "/" + theme + ".json", location.href);
|
|
1210
|
+
if (Object.keys(controlsDraft).length) url.searchParams.set("controls", JSON.stringify(controlsDraft));
|
|
1211
|
+
const response = await fetch(url, {signal});
|
|
1212
|
+
if (!response.ok) throw new Error("Could not read theme " + theme);
|
|
1213
|
+
return response.json();
|
|
1214
|
+
},
|
|
1215
|
+
async apply(theme, signal) {
|
|
1216
|
+
if (signal?.aborted) throw new Error("Audit cancelled.");
|
|
1217
|
+
const side = states[activeSideId], owner = wbCatalog?.sides[side.serviceId || side.id];
|
|
1218
|
+
const entry = owner?.selections.find(item => item.map === side.workbenchMap && item.theme === theme && !item.scene);
|
|
1219
|
+
if (!entry) throw new Error("Theme is not available: " + theme);
|
|
1220
|
+
applyWorkbenchAppearance(side, entry);
|
|
1221
|
+
const started = performance.now();
|
|
1222
|
+
do {
|
|
1223
|
+
if (signal?.aborted) throw new Error("Audit cancelled.");
|
|
1224
|
+
if (states[activeSideId] !== side) throw new Error("The active map changed.");
|
|
1225
|
+
if (performance.now() - started > 30000) throw new Error("Theme update timed out.");
|
|
1226
|
+
await new Promise(resolve => setTimeout(resolve,40));
|
|
1227
|
+
} while (controlsBusy || controlsPending || controlsLoadingKey || controlsApplyTimer !== undefined);
|
|
1228
|
+
if (controlsLastError) throw new Error(controlsLastError);
|
|
1229
|
+
if (controlsIdentity(side)?.theme !== theme) throw new Error("Theme could not be applied.");
|
|
1230
|
+
},
|
|
1231
|
+
async restore(saved) {
|
|
1232
|
+
if (states[activeSideId].id !== saved.side) return;
|
|
1233
|
+
controlsHistoryQuiet++;
|
|
1234
|
+
try {
|
|
1235
|
+
controlsDraft = structuredClone(saved.controls);
|
|
1236
|
+
ctl("3d").checked = saved.buildings3d; ctl("animation").value = saved.animation;
|
|
1237
|
+
scheduleMapControls();
|
|
1238
|
+
await this.apply(saved.theme);
|
|
1239
|
+
const side = states[activeSideId];
|
|
1240
|
+
if (saved.style) await side.frame.contentWindow.__tileflowApplyScene(JSON.stringify({map:controlsIdentity(side).map,theme:saved.theme,controls:saved.controls,buildings3d:saved.buildings3d,animation:saved.animation === "fade" ? "fade" : Number(saved.animation),style:saved.style}), () => states[activeSideId] === side);
|
|
1241
|
+
renderMapControls();syncAnimationSegments();controlsHistoryCurrent=this.snapshot();
|
|
1242
|
+
} finally {controlsHistoryQuiet--;}
|
|
1243
|
+
}
|
|
1244
|
+
};
|
|
1245
|
+
`;
|
|
1246
|
+
|
|
1247
|
+
// src/workbench-ui.ts
|
|
1248
|
+
function workbenchIcon(name) {
|
|
1249
|
+
const paths = {
|
|
1250
|
+
inspect: '<path d="M9 3H4a1 1 0 0 0-1 1v5m12-6h5a1 1 0 0 1 1 1v5M3 15v5a1 1 0 0 0 1 1h5"/><path d="m11 11 10 4-5 1-1 5z"/>',
|
|
1251
|
+
settings: '<path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="3"/><circle cx="15" cy="17" r="3"/>',
|
|
1252
|
+
close: '<path d="m6 6 12 12M6 18 18 6"/>'
|
|
1253
|
+
};
|
|
1254
|
+
return `<svg class="tool-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${paths[name]}</svg>`;
|
|
1255
|
+
}
|
|
1256
|
+
var workbenchStyles = `
|
|
1257
|
+
body { display:flex; flex-direction:column; }
|
|
1258
|
+
.workspace { flex:1; min-height:0; }
|
|
1259
|
+
input[type=range] { accent-color:var(--accent); }
|
|
1260
|
+
button:disabled { opacity:.5; cursor:not-allowed; }
|
|
1261
|
+
select,input[type=text],input[type=number] { background:var(--paper); color:var(--ink); border:1px solid var(--border); padding:5px 8px; }
|
|
1262
|
+
summary { cursor:pointer; }
|
|
1263
|
+
.tool-icon { width:17px; height:17px; flex:none; vertical-align:middle; }
|
|
1264
|
+
button:has(.tool-icon) { display:inline-flex; align-items:center; justify-content:center; gap:7px; }
|
|
1265
|
+
.inspect-hint { position:absolute; z-index:4; top:8px; left:8px; pointer-events:none; }
|
|
1266
|
+
.compact-note { font-size:12px; color:var(--muted); line-height:1.5; }
|
|
1267
|
+
.property-list { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:8px; }
|
|
1268
|
+
.property-list dd { margin:0; }
|
|
1269
|
+
.authoring-path { display:block; overflow-wrap:anywhere; }
|
|
1270
|
+
.color-swatch { width:16px; height:16px; border:1px solid var(--border); border-radius:2px; display:inline-block; }
|
|
1271
|
+
`;
|
|
1272
|
+
var workbenchPanels = `<div id="wb-notice" role="status" hidden></div>`;
|
|
39
1273
|
|
|
40
1274
|
// src/comparison-html.ts
|
|
41
1275
|
function renderTileflowComparisonHtml(options) {
|
|
42
1276
|
const documentOptions = serializeInlineJson(options);
|
|
1277
|
+
const fontBasePath = options.basePath.split("/").map((segment) => encodeURIComponent(decodeURIComponent(segment)).replaceAll("'", "%27")).join("/");
|
|
43
1278
|
const nonceAttribute = options.cspNonce ? ` nonce="${escapeHtml(options.cspNonce)}"` : "";
|
|
44
1279
|
return `<!doctype html>
|
|
45
1280
|
<html lang="en" data-tileflow-state="loading">
|
|
@@ -48,6 +1283,10 @@ function renderTileflowComparisonHtml(options) {
|
|
|
48
1283
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
49
1284
|
<title>${escapeHtml(options.title)}</title>
|
|
50
1285
|
<style${nonceAttribute}>
|
|
1286
|
+
@font-face { font-family:'Inter'; font-weight:100 900; font-display:swap; src:url('${fontBasePath}/__workbench-fonts/inter.woff2') format('woff2'); }
|
|
1287
|
+
@font-face { font-family:'JetBrains Mono'; font-weight:400; font-display:swap; src:url('${fontBasePath}/__workbench-fonts/jetbrains-mono.woff2') format('woff2'); }
|
|
1288
|
+
@font-face { font-family:'Mona Sans'; font-weight:200 900; font-display:swap; src:url('${fontBasePath}/__workbench-fonts/mona-sans.woff2') format('woff2'); }
|
|
1289
|
+
@font-face { font-family:'Hubot Sans'; font-weight:200 900; font-display:swap; src:url('${fontBasePath}/__workbench-fonts/hubot-sans.woff2') format('woff2'); }
|
|
51
1290
|
:root {
|
|
52
1291
|
color-scheme: light;
|
|
53
1292
|
--accent: #3157d5;
|
|
@@ -59,6 +1298,8 @@ function renderTileflowComparisonHtml(options) {
|
|
|
59
1298
|
--overlay-alpha: .5;
|
|
60
1299
|
}
|
|
61
1300
|
* { box-sizing: border-box; }
|
|
1301
|
+
[hidden] { display: none !important; }
|
|
1302
|
+
.stage[data-mode="single"] .pane-right { display: none; }
|
|
62
1303
|
html, body { height: 100%; margin: 0; }
|
|
63
1304
|
body {
|
|
64
1305
|
display: grid;
|
|
@@ -83,49 +1324,10 @@ function renderTileflowComparisonHtml(options) {
|
|
|
83
1324
|
outline: 3px solid color-mix(in srgb, var(--accent) 45%, transparent);
|
|
84
1325
|
outline-offset: 2px;
|
|
85
1326
|
}
|
|
86
|
-
.toolbar {
|
|
87
|
-
z-index: 10;
|
|
88
|
-
display: flex;
|
|
89
|
-
min-height: 48px;
|
|
90
|
-
align-items: center;
|
|
91
|
-
gap: 8px;
|
|
92
|
-
padding: 7px 10px;
|
|
93
|
-
border-bottom: 1px solid var(--border);
|
|
94
|
-
background: rgba(255, 255, 255, .97);
|
|
95
|
-
flex-wrap: wrap;
|
|
96
|
-
}
|
|
97
|
-
.toolbar-title { font-weight: 750; margin-right: 4px; }
|
|
98
|
-
.toolbar-group { display: inline-flex; min-width: 0; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
99
|
-
.toolbar-separator { width: 1px; height: 25px; flex: 0 0 auto; background: var(--border); }
|
|
100
|
-
.toolbar label { display: inline-flex; align-items: center; gap: 5px; color: var(--muted); }
|
|
101
|
-
.toolbar input[type="range"] { width: 96px; }
|
|
102
|
-
.toolbar input[type="text"] { width: 150px; padding: 5px 7px; border: 1px solid var(--border); border-radius: 6px; }
|
|
103
1327
|
.workspace { position: relative; min-height: 0; display: grid; grid-template-columns: minmax(0, 1fr) auto; }
|
|
104
1328
|
.stage { position: relative; min-width: 0; min-height: 0; overflow: hidden; background: #d8dadd; }
|
|
105
1329
|
.pane { position: absolute; inset: 0; min-width: 0; min-height: 0; overflow: hidden; background: #d8dadd; }
|
|
106
1330
|
.pane iframe { width: 100%; height: 100%; display: block; border: 0; background: #d8dadd; }
|
|
107
|
-
.pane-label {
|
|
108
|
-
position: absolute;
|
|
109
|
-
z-index: 3;
|
|
110
|
-
top: 10px;
|
|
111
|
-
left: 10px;
|
|
112
|
-
display: flex;
|
|
113
|
-
max-width: calc(100% - 20px);
|
|
114
|
-
align-items: center;
|
|
115
|
-
gap: 7px;
|
|
116
|
-
border: 1px solid rgba(0, 0, 0, .12);
|
|
117
|
-
border-radius: 999px;
|
|
118
|
-
background: rgba(255, 255, 255, .93);
|
|
119
|
-
box-shadow: 0 3px 14px rgba(0, 0, 0, .12);
|
|
120
|
-
padding: 5px 9px;
|
|
121
|
-
pointer-events: none;
|
|
122
|
-
}
|
|
123
|
-
.pane-label-text { overflow: hidden; text-overflow: ellipsis; }
|
|
124
|
-
.side-state-text { flex: 0 0 auto; color: var(--muted); font-size: 10px; text-transform: uppercase; letter-spacing: .03em; }
|
|
125
|
-
.side-state { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; background: #8d929b; }
|
|
126
|
-
.side-state[data-status="ready"] { background: #248a4a; }
|
|
127
|
-
.side-state[data-status="building"] { background: #cf8114; }
|
|
128
|
-
.side-state[data-status="invalid"], .side-state[data-status="error"] { background: #ba3838; }
|
|
129
1331
|
.stage[data-mode="side-by-side"] { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 1px; }
|
|
130
1332
|
.stage[data-mode="side-by-side"] .pane { position: relative; inset: auto; }
|
|
131
1333
|
.stage[data-mode="side-by-side"] .pane-right { grid-column: 2; }
|
|
@@ -165,74 +1367,46 @@ function renderTileflowComparisonHtml(options) {
|
|
|
165
1367
|
.sprite-card { min-width: 0; border: 1px solid var(--border); border-radius: 7px; padding: 7px; background: var(--paper); text-align: center; }
|
|
166
1368
|
.sprite-card canvas { display: block; margin: auto; max-width: 64px; max-height: 64px; image-rendering: auto; }
|
|
167
1369
|
.sprite-name { display: block; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; font-size: 10px; }
|
|
168
|
-
.feedback { color: var(--muted); min-width: 64px; }
|
|
169
1370
|
.visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
170
1371
|
@media (max-width: 760px) {
|
|
171
|
-
.toolbar { align-content: center; gap: 6px; padding: 6px; }
|
|
172
|
-
.toolbar-title { flex: 1 0 calc(100% - 12px); }
|
|
173
|
-
.toolbar-separator { display: none; }
|
|
174
|
-
.toolbar-group { flex: 1 1 100%; gap: 5px; }
|
|
175
|
-
.toolbar-group button { padding-inline: 7px; }
|
|
176
|
-
.toolbar input[type="range"] { width: min(24vw, 86px); }
|
|
177
|
-
.toolbar input[type="text"] { width: min(34vw, 130px); }
|
|
178
1372
|
.workspace { grid-template-columns: minmax(0, 1fr); }
|
|
179
1373
|
.inspector { position: absolute; z-index: 20; right: 0; inset-block: 0; width: min(92vw, 390px); box-shadow: -8px 0 28px rgba(0,0,0,.18); }
|
|
180
1374
|
}
|
|
181
1375
|
@media (prefers-reduced-motion: reduce) {
|
|
182
1376
|
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
|
|
183
1377
|
}
|
|
1378
|
+
${workbenchStyles}
|
|
1379
|
+
${controlsStyles}
|
|
1380
|
+
${devtoolsStyles}
|
|
184
1381
|
</style>
|
|
185
1382
|
</head>
|
|
186
1383
|
<body>
|
|
187
|
-
<header class="toolbar" aria-label="Comparison controls">
|
|
188
|
-
<span class="toolbar-title" id="title"></span>
|
|
189
|
-
<div class="toolbar-group" role="group" aria-label="Comparison mode">
|
|
190
|
-
<button type="button" data-mode="side-by-side">Side by side</button>
|
|
191
|
-
<button type="button" data-mode="split">Split</button>
|
|
192
|
-
<button type="button" data-mode="overlay">Overlay</button>
|
|
193
|
-
<button type="button" data-mode="blink">Blink</button>
|
|
194
|
-
</div>
|
|
195
|
-
<span class="toolbar-separator"></span>
|
|
196
|
-
<div class="toolbar-group">
|
|
197
|
-
<label id="split-control">Split <input id="split" type="range" min="5" max="95" value="50" aria-label="Split position" /></label>
|
|
198
|
-
<label id="alpha-control">Opacity <input id="alpha" type="range" min="0" max="100" value="50" aria-label="Overlay opacity" /></label>
|
|
199
|
-
<button type="button" id="driver">Interact: left</button>
|
|
200
|
-
<button type="button" id="blink-pause">Pause blink</button>
|
|
201
|
-
<button type="button" id="inspect-toggle" aria-controls="inspector" aria-expanded="false" aria-pressed="false">Inspect</button>
|
|
202
|
-
</div>
|
|
203
|
-
<span class="toolbar-separator"></span>
|
|
204
|
-
<div class="toolbar-group">
|
|
205
|
-
<input id="scene-name" type="text" value="comparison-view" aria-label="Scene name" />
|
|
206
|
-
<label>DPR <select id="scene-dpr"><option value="1">1</option><option value="2">2</option></select></label>
|
|
207
|
-
<button type="button" id="copy-scene">Copy scene</button>
|
|
208
|
-
<button type="button" id="copy-command">Copy command</button>
|
|
209
|
-
</div>
|
|
210
|
-
<span class="feedback" id="feedback" role="status"></span>
|
|
211
|
-
</header>
|
|
212
1384
|
<div class="workspace">
|
|
213
1385
|
<main class="stage" id="stage" data-driver="left" data-tileflow-capture-id="tileflow-comparison" data-tileflow-state="loading">
|
|
214
1386
|
<section class="pane pane-left" data-side="left">
|
|
215
|
-
<
|
|
216
|
-
<iframe id="left-frame" title="Left Tileflow preview" referrerpolicy="no-referrer"></iframe>
|
|
1387
|
+
<iframe id="left-frame" title="${options.right ? "Left Tileflow preview" : "Tileflow map preview"}" referrerpolicy="no-referrer"></iframe>
|
|
217
1388
|
</section>
|
|
218
1389
|
<section class="pane pane-right" data-side="right">
|
|
219
|
-
<div class="pane-label"><span class="side-state" id="right-state" role="status" aria-live="polite"></span><span class="pane-label-text" id="right-label"></span><span class="side-state-text" id="right-status-text">Building</span></div>
|
|
220
1390
|
<iframe id="right-frame" title="Right Tileflow preview" referrerpolicy="no-referrer"></iframe>
|
|
221
1391
|
</section>
|
|
222
1392
|
<div class="split-line" aria-hidden="true"></div>
|
|
1393
|
+
<div class="inspect-hint" id="wb-inspect-hint" hidden>Click an element to inspect \xB7 Esc to exit</div>
|
|
223
1394
|
</main>
|
|
224
1395
|
<aside class="inspector" id="inspector" tabindex="-1" aria-labelledby="inspector-title" hidden>
|
|
225
|
-
<
|
|
226
|
-
<
|
|
227
|
-
<
|
|
1396
|
+
<button type="button" id="inspect-toggle" aria-controls="inspector" aria-expanded="false" aria-pressed="false" aria-keyshortcuts="i" title="Select a map element (I). Escape to stop.">${workbenchIcon("inspect")}Inspect</button>
|
|
1397
|
+
<button type="button" id="controls-toggle" aria-controls="map-controls" aria-expanded="false">Scene</button>
|
|
1398
|
+
<div class="inspector-heading"><h2 id="inspector-title">${workbenchIcon("inspect")} Inspect the map</h2><button type="button" id="inspector-close" aria-label="Close inspector">${workbenchIcon("close")}</button></div>
|
|
1399
|
+
<div id="inspection" class="empty">Click a feature to understand how it is drawn and find its configuration.</div>
|
|
1400
|
+
<details class="zoom-details"><summary>Appearance at different zoom levels</summary>
|
|
228
1401
|
<select id="curve-property" disabled><option>Select a rendered layer first</option></select>
|
|
229
|
-
<div id="curve-output" class="empty">
|
|
230
|
-
<h3>Compiler sidecar</h3>
|
|
231
|
-
<
|
|
232
|
-
<h3>Sprite atlas</h3>
|
|
1402
|
+
<div id="curve-output" class="empty">Select an element to explore its appearance.</div></details>
|
|
1403
|
+
<details><summary>Compiler details</summary><h3>Compiler sidecar</h3><pre id="sidecar-output">No layer selected.</pre></details>
|
|
1404
|
+
<details><summary>Sprite atlas</summary>
|
|
233
1405
|
<button type="button" id="load-sprites">Load active side sprites</button>
|
|
234
|
-
<div id="sprite-output" class="sprite-grid"></div>
|
|
1406
|
+
<div id="sprite-output" class="sprite-grid"></div></details>
|
|
235
1407
|
</aside>
|
|
1408
|
+
${workbenchPanels}
|
|
1409
|
+
${controlsPanel}
|
|
236
1410
|
</div>
|
|
237
1411
|
<script type="module"${nonceAttribute}>
|
|
238
1412
|
const options = ${documentOptions};
|
|
@@ -250,24 +1424,16 @@ function renderTileflowComparisonHtml(options) {
|
|
|
250
1424
|
const curveOutput = document.getElementById("curve-output");
|
|
251
1425
|
const sidecarOutput = document.getElementById("sidecar-output");
|
|
252
1426
|
const spriteOutput = document.getElementById("sprite-output");
|
|
253
|
-
const feedback = document.getElementById("feedback");
|
|
254
|
-
const splitInput = document.getElementById("split");
|
|
255
|
-
const alphaInput = document.getElementById("alpha");
|
|
256
|
-
const driverButton = document.getElementById("driver");
|
|
257
|
-
const blinkPauseButton = document.getElementById("blink-pause");
|
|
258
1427
|
const inspectButton = document.getElementById("inspect-toggle");
|
|
259
1428
|
const inspectorCloseButton = document.getElementById("inspector-close");
|
|
260
|
-
const sceneNameInput = document.getElementById("scene-name");
|
|
261
|
-
const sceneDprInput = document.getElementById("scene-dpr");
|
|
262
1429
|
const reducedMotion = matchMedia("(prefers-reduced-motion: reduce)");
|
|
263
|
-
const
|
|
1430
|
+
const sideIds = options.right ? ["left", "right"] : ["left"];
|
|
1431
|
+
const states = Object.fromEntries(sideIds.map((id) => {
|
|
264
1432
|
const definition = options[id];
|
|
265
1433
|
return [id, {
|
|
266
1434
|
id,
|
|
267
1435
|
definition,
|
|
268
1436
|
frame: document.getElementById(id + "-frame"),
|
|
269
|
-
stateElement: document.getElementById(id + "-state"),
|
|
270
|
-
statusTextElement: document.getElementById(id + "-status-text"),
|
|
271
1437
|
map: undefined,
|
|
272
1438
|
camera: undefined,
|
|
273
1439
|
bridgeReady: false,
|
|
@@ -296,45 +1462,25 @@ function renderTileflowComparisonHtml(options) {
|
|
|
296
1462
|
let spriteLoadSequence = 0;
|
|
297
1463
|
let spriteOwnerId;
|
|
298
1464
|
|
|
299
|
-
document.getElementById("title").textContent = options.title;
|
|
300
1465
|
const initialUi = readUiFromUrl();
|
|
301
|
-
|
|
302
|
-
alphaInput.value = String(initialUi.alpha);
|
|
1466
|
+
|
|
303
1467
|
applySliders();
|
|
304
1468
|
setMode(initialUi.mode || options.initialMode);
|
|
305
|
-
|
|
306
|
-
for (const id of ["left", "right"]) {
|
|
1469
|
+
for (const id of sideIds) {
|
|
307
1470
|
const side = states[id];
|
|
308
|
-
document.getElementById(id + "-label").textContent = side.definition.label;
|
|
309
1471
|
side.frame.src = side.definition.previewUrl;
|
|
310
1472
|
side.frame.addEventListener("load", () => connectSide(side));
|
|
311
1473
|
side.frame.addEventListener("focus", () => selectSide(side.id));
|
|
312
1474
|
startStatus(side);
|
|
313
1475
|
}
|
|
314
1476
|
|
|
315
|
-
for (const button of document.querySelectorAll("[data-mode]")) {
|
|
316
|
-
button.addEventListener("click", () => setMode(button.dataset.mode));
|
|
317
|
-
}
|
|
318
|
-
splitInput.addEventListener("input", () => { applySliders(); writeUrlState(); });
|
|
319
|
-
alphaInput.addEventListener("input", () => { applySliders(); writeUrlState(); });
|
|
320
|
-
driverButton.addEventListener("click", () => {
|
|
321
|
-
selectSide(stage.dataset.driver === "left" ? "right" : "left");
|
|
322
|
-
});
|
|
323
|
-
blinkPauseButton.addEventListener("click", () => {
|
|
324
|
-
blinkPaused = !blinkPaused;
|
|
325
|
-
updateBlinkMotionPreference();
|
|
326
|
-
restartBlink();
|
|
327
|
-
});
|
|
328
1477
|
inspectButton.addEventListener("click", () => setInspectionEnabled(!inspectionEnabled, true));
|
|
329
1478
|
inspectorCloseButton.addEventListener("click", () => setInspectionEnabled(false, true));
|
|
330
|
-
document.getElementById("copy-scene").addEventListener("click", () => copyScene(false));
|
|
331
|
-
document.getElementById("copy-command").addEventListener("click", () => copyScene(true));
|
|
332
1479
|
document.getElementById("load-sprites").addEventListener("click", () => loadSprites(states[activeSideId]));
|
|
333
1480
|
curveProperty.addEventListener("change", renderCurve);
|
|
334
1481
|
reducedMotion.addEventListener?.("change", () => {
|
|
335
1482
|
blinkPaused = reducedMotion.matches;
|
|
336
|
-
|
|
337
|
-
restartBlink();
|
|
1483
|
+
restartBlink();
|
|
338
1484
|
});
|
|
339
1485
|
addEventListener("keydown", (event) => {
|
|
340
1486
|
if (event.key === "Escape" && inspectionEnabled) setInspectionEnabled(false, true);
|
|
@@ -346,18 +1492,16 @@ function renderTileflowComparisonHtml(options) {
|
|
|
346
1492
|
side.eventSource?.close();
|
|
347
1493
|
side.sidecarAbort?.abort();
|
|
348
1494
|
side.spriteAbort?.abort();
|
|
349
|
-
side
|
|
1495
|
+
cleanupSideBridge(side);
|
|
350
1496
|
}
|
|
351
1497
|
});
|
|
352
1498
|
|
|
353
1499
|
function selectSide(id, writeState = true) {
|
|
354
|
-
if (id
|
|
1500
|
+
if (!states[id]) return;
|
|
355
1501
|
const changed = activeSideId !== id || stage.dataset.driver !== id;
|
|
356
1502
|
const previousSpriteOwner = spriteOwnerId;
|
|
357
1503
|
activeSideId = id;
|
|
358
1504
|
stage.dataset.driver = id;
|
|
359
|
-
driverButton.textContent = "Interact: " + id;
|
|
360
|
-
driverButton.setAttribute("aria-label", "Interact with " + states[id].definition.label);
|
|
361
1505
|
if (changed && previousSpriteOwner && previousSpriteOwner !== id) {
|
|
362
1506
|
states[previousSpriteOwner].spriteAbort?.abort();
|
|
363
1507
|
spriteLoadSequence += 1;
|
|
@@ -370,33 +1514,31 @@ function renderTileflowComparisonHtml(options) {
|
|
|
370
1514
|
|
|
371
1515
|
function setInspectionEnabled(enabled, manageFocus = false) {
|
|
372
1516
|
inspectionEnabled = enabled;
|
|
1517
|
+
if (globalThis.__tileflowDevtoolsUI) {
|
|
1518
|
+
inspectButton.setAttribute("aria-pressed", String(enabled));
|
|
1519
|
+
document.getElementById("wb-inspect-hint").hidden = !enabled;
|
|
1520
|
+
for (const side of Object.values(states)) sendInspectionState(side);
|
|
1521
|
+
globalThis.__tileflowDevtoolsUI.inspectionChanged(enabled);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1524
|
+
if (enabled) toggleMapControls(false);
|
|
373
1525
|
inspectButton.setAttribute("aria-pressed", String(enabled));
|
|
374
1526
|
inspectButton.setAttribute("aria-expanded", String(enabled));
|
|
375
1527
|
inspector.hidden = !enabled;
|
|
376
|
-
|
|
1528
|
+
document.getElementById("wb-inspect-hint").hidden = !enabled;
|
|
1529
|
+
for (const side of Object.values(states)) { sendInspectionState(side); side.map?.resize?.(); }
|
|
377
1530
|
if (manageFocus) {
|
|
378
1531
|
if (enabled) inspector.focus();
|
|
379
1532
|
else inspectButton.focus();
|
|
380
1533
|
}
|
|
381
1534
|
}
|
|
382
1535
|
|
|
383
|
-
function updateBlinkMotionPreference() {
|
|
384
|
-
const motionBlocked = reducedMotion.matches;
|
|
385
|
-
blinkPauseButton.disabled = motionBlocked;
|
|
386
|
-
blinkPauseButton.textContent = motionBlocked
|
|
387
|
-
? "Blink paused (reduced motion)"
|
|
388
|
-
: blinkPaused ? "Resume blink" : "Pause blink";
|
|
389
|
-
}
|
|
390
1536
|
|
|
391
1537
|
function setMode(mode) {
|
|
392
|
-
if (!
|
|
1538
|
+
if (!options.right) mode = "single";
|
|
1539
|
+
if (!["single", "side-by-side", "split", "overlay", "blink"].includes(mode)) return;
|
|
1540
|
+
if (mode === "single" && activeSideId !== "left") selectSide("left", false);
|
|
393
1541
|
stage.dataset.mode = mode;
|
|
394
|
-
for (const button of document.querySelectorAll("[data-mode]")) {
|
|
395
|
-
button.setAttribute("aria-pressed", String(button.dataset.mode === mode));
|
|
396
|
-
}
|
|
397
|
-
document.getElementById("split-control").hidden = mode !== "split";
|
|
398
|
-
document.getElementById("alpha-control").hidden = mode !== "overlay";
|
|
399
|
-
blinkPauseButton.hidden = mode !== "blink";
|
|
400
1542
|
restartBlink();
|
|
401
1543
|
writeUrlState();
|
|
402
1544
|
setTimeout(() => {
|
|
@@ -405,8 +1547,8 @@ function renderTileflowComparisonHtml(options) {
|
|
|
405
1547
|
}
|
|
406
1548
|
|
|
407
1549
|
function applySliders() {
|
|
408
|
-
const split = boundedNumber(
|
|
409
|
-
const alpha = boundedNumber(
|
|
1550
|
+
const split = boundedNumber(initialUi.split, 5, 95, 50);
|
|
1551
|
+
const alpha = boundedNumber(initialUi.alpha, 0, 100, 50);
|
|
410
1552
|
stage.style.setProperty("--split", split + "%");
|
|
411
1553
|
stage.style.setProperty("--overlay-alpha", String(alpha / 100));
|
|
412
1554
|
}
|
|
@@ -423,16 +1565,27 @@ function renderTileflowComparisonHtml(options) {
|
|
|
423
1565
|
}, 700);
|
|
424
1566
|
}
|
|
425
1567
|
|
|
1568
|
+
function cleanupSideBridge(side) {
|
|
1569
|
+
try { side.cleanupBridge?.(); }
|
|
1570
|
+
catch (error) {
|
|
1571
|
+
// A stopped dev server can replace the iframe with a cross-origin browser error page.
|
|
1572
|
+
if (error?.name !== "SecurityError") throw error;
|
|
1573
|
+
} finally { side.cleanupBridge = undefined; }
|
|
1574
|
+
}
|
|
1575
|
+
|
|
426
1576
|
function connectSide(side) {
|
|
427
1577
|
side.loadEpoch += 1;
|
|
1578
|
+
side.appliedSceneStyle = undefined;
|
|
1579
|
+
side.pendingAppearance = undefined;
|
|
428
1580
|
const epoch = side.loadEpoch;
|
|
429
|
-
side
|
|
1581
|
+
cleanupSideBridge(side);
|
|
430
1582
|
side.cleanupBridge = undefined;
|
|
431
1583
|
side.sidecarAbort?.abort();
|
|
432
1584
|
side.spriteAbort?.abort();
|
|
433
1585
|
side.sidecarAbort = undefined;
|
|
434
1586
|
side.spriteAbort = undefined;
|
|
435
1587
|
side.map = undefined;
|
|
1588
|
+
side.bridgeConnected = false;
|
|
436
1589
|
side.bridgeReady = false;
|
|
437
1590
|
side.camera = undefined;
|
|
438
1591
|
side.sidecar = undefined;
|
|
@@ -456,7 +1609,6 @@ function renderTileflowComparisonHtml(options) {
|
|
|
456
1609
|
curveOutput.textContent = "Paint and layout values appear here.";
|
|
457
1610
|
sidecarOutput.textContent = "No layer selected.";
|
|
458
1611
|
}
|
|
459
|
-
renderSideStatus(side);
|
|
460
1612
|
updateDocumentReadiness();
|
|
461
1613
|
const started = performance.now();
|
|
462
1614
|
const findMap = () => {
|
|
@@ -467,7 +1619,13 @@ function renderTileflowComparisonHtml(options) {
|
|
|
467
1619
|
const map = childWindow?.__tileflowPreviewMap;
|
|
468
1620
|
if (map) {
|
|
469
1621
|
side.map = map;
|
|
1622
|
+
// DevTools can edit a connected map while its tiles are still loading.
|
|
1623
|
+
// bridgeReady remains the separate renderer-readiness signal.
|
|
1624
|
+
side.bridgeConnected = true;
|
|
1625
|
+
side.camera = readMapCamera(map);
|
|
470
1626
|
side.cleanupBridge = installChildBridge(side, childWindow, map, epoch);
|
|
1627
|
+
if (options.workbenchUrl) void refreshWorkbench();
|
|
1628
|
+
if (!controlsPanel.hidden) void loadMapControls();
|
|
471
1629
|
return;
|
|
472
1630
|
}
|
|
473
1631
|
} catch (error) {
|
|
@@ -493,6 +1651,68 @@ function renderTileflowComparisonHtml(options) {
|
|
|
493
1651
|
let releaseFrameAfterPaint;
|
|
494
1652
|
let readyTimer;
|
|
495
1653
|
let readyAnnounced = false;
|
|
1654
|
+
let inspectedFeatures = [];
|
|
1655
|
+
let highlightedIndex = -1;
|
|
1656
|
+
let hoveredFeature, hoveredCoordinate, hoverFrame, hoverHint;
|
|
1657
|
+
let inspectedCoordinate;
|
|
1658
|
+
let highlight;
|
|
1659
|
+
const drawHighlight = () => {
|
|
1660
|
+
const geometry = hoveredFeature?.geometry || inspectedFeatures[highlightedIndex]?.geometry;
|
|
1661
|
+
if ((!inspectionActive && !globalThis.__tileflowDevtoolsUI) || !geometry || !map.getContainer || !map.project) { if (highlight) highlight.style.display = "none"; return; }
|
|
1662
|
+
if (!highlight) {
|
|
1663
|
+
highlight = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1664
|
+
highlight.setAttribute("aria-hidden", "true");
|
|
1665
|
+
highlight.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;overflow:hidden";
|
|
1666
|
+
map.getContainer().append(highlight);
|
|
1667
|
+
}
|
|
1668
|
+
highlight.style.display = "";
|
|
1669
|
+
highlight.replaceChildren();
|
|
1670
|
+
let remaining = 3000;
|
|
1671
|
+
const project = (coordinate) => {
|
|
1672
|
+
if (--remaining < 0 || !Array.isArray(coordinate) || !coordinate.slice(0,2).every(Number.isFinite)) return;
|
|
1673
|
+
const point = map.project(coordinate);
|
|
1674
|
+
return Number.isFinite(point.x) && Number.isFinite(point.y) ? point : undefined;
|
|
1675
|
+
};
|
|
1676
|
+
const draw = (coordinates, closed = false) => {
|
|
1677
|
+
if (!Array.isArray(coordinates)) return;
|
|
1678
|
+
const points = coordinates.slice(0, Math.max(0,remaining)).map(project).filter(Boolean);
|
|
1679
|
+
if (!points.length) return;
|
|
1680
|
+
const shape = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1681
|
+
shape.setAttribute("d", points.map((point,index) => (index ? "L" : "M") + point.x + " " + point.y).join(" ") + (closed ? "Z" : ""));
|
|
1682
|
+
shape.setAttribute("fill", closed ? "#1a73e822" : "none");
|
|
1683
|
+
shape.setAttribute("stroke", "#1a73e8"); shape.setAttribute("stroke-width", "3");
|
|
1684
|
+
shape.setAttribute("stroke-linejoin", "round"); highlight.append(shape);
|
|
1685
|
+
};
|
|
1686
|
+
const coordinates = geometry.coordinates;
|
|
1687
|
+
const containsPoint = (ring) => {
|
|
1688
|
+
const coordinate = hoveredCoordinate || inspectedCoordinate;
|
|
1689
|
+
if (!coordinate) return false;
|
|
1690
|
+
const [x,y] = coordinate;
|
|
1691
|
+
let inside = false;
|
|
1692
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
1693
|
+
const [xi,yi] = ring[i], [xj,yj] = ring[j];
|
|
1694
|
+
if ((yi > y) !== (yj > y) && x < (xj-xi)*(y-yi)/(yj-yi)+xi) inside = !inside;
|
|
1695
|
+
}
|
|
1696
|
+
return inside;
|
|
1697
|
+
};
|
|
1698
|
+
if (geometry.type === "Polygon") coordinates.forEach((ring) => draw(ring,true));
|
|
1699
|
+
else if (geometry.type === "MultiPolygon") coordinates.filter((polygon) => containsPoint(polygon[0]) && !polygon.slice(1).some(containsPoint)).forEach((polygon) => polygon.forEach((ring) => draw(ring,true)));
|
|
1700
|
+
else if (geometry.type === "LineString") draw(coordinates);
|
|
1701
|
+
else if (geometry.type === "MultiLineString") coordinates.forEach((line) => draw(line));
|
|
1702
|
+
else for (const coordinate of geometry.type === "MultiPoint" ? coordinates : [coordinates]) {
|
|
1703
|
+
const point = project(coordinate); if (!point) continue;
|
|
1704
|
+
const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
1705
|
+
circle.setAttribute("cx", point.x); circle.setAttribute("cy", point.y); circle.setAttribute("r", "10");
|
|
1706
|
+
circle.setAttribute("fill", "#1a73e822"); circle.setAttribute("stroke", "#1a73e8"); circle.setAttribute("stroke-width", "3"); highlight.append(circle);
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
side.highlightFeature = (index) => { highlightedIndex = index; drawHighlight(); };
|
|
1710
|
+
side.getInspectedFeature = () => inspectedFeatures[highlightedIndex];
|
|
1711
|
+
const handleKey = (event) => {
|
|
1712
|
+
if (event.target.closest?.("input,select,textarea,[contenteditable]")) return;
|
|
1713
|
+
if (event.key === "Escape" && inspectionEnabled) { event.preventDefault(); setInspectionEnabled(false, true); }
|
|
1714
|
+
if (event.key.toLowerCase() === "i" && !event.metaKey && !event.ctrlKey && !event.altKey) { event.preventDefault(); setInspectionEnabled(!inspectionEnabled, true); }
|
|
1715
|
+
};
|
|
496
1716
|
const targetOrigin = childWindow.location.origin;
|
|
497
1717
|
// The bridge is installed by the same-origin parent, so callbacks created here
|
|
498
1718
|
// belong to the parent realm. Dispatch directly with an explicit child source;
|
|
@@ -515,11 +1735,11 @@ function renderTileflowComparisonHtml(options) {
|
|
|
515
1735
|
});
|
|
516
1736
|
};
|
|
517
1737
|
const handleMoveStart = (event) => {
|
|
518
|
-
if (applyingRequestId === undefined && event?.originalEvent) userMovementActive = true;
|
|
1738
|
+
if (applyingRequestId === undefined && (event?.originalEvent || event?.tileflowDevtools === true)) userMovementActive = true;
|
|
519
1739
|
};
|
|
520
1740
|
const handleMove = (event) => {
|
|
521
1741
|
if (applyingRequestId !== undefined) return;
|
|
522
|
-
if (event?.originalEvent) userMovementActive = true;
|
|
1742
|
+
if (event?.originalEvent || event?.tileflowDevtools === true) userMovementActive = true;
|
|
523
1743
|
queuedUserMovement = queuedUserMovement || userMovementActive;
|
|
524
1744
|
if (movementFrame !== undefined) return;
|
|
525
1745
|
movementFrame = childWindow.requestAnimationFrame(() => {
|
|
@@ -535,7 +1755,7 @@ function renderTileflowComparisonHtml(options) {
|
|
|
535
1755
|
scheduleApplyingRelease(applyingRequestId);
|
|
536
1756
|
return;
|
|
537
1757
|
}
|
|
538
|
-
if (event?.originalEvent) userMovementActive = true;
|
|
1758
|
+
if (event?.originalEvent || event?.tileflowDevtools === true) userMovementActive = true;
|
|
539
1759
|
if (movementFrame !== undefined) {
|
|
540
1760
|
childWindow.cancelAnimationFrame(movementFrame);
|
|
541
1761
|
movementFrame = undefined;
|
|
@@ -544,9 +1764,45 @@ function renderTileflowComparisonHtml(options) {
|
|
|
544
1764
|
postCamera("end", undefined, userMovementActive);
|
|
545
1765
|
userMovementActive = false;
|
|
546
1766
|
};
|
|
1767
|
+
const clearHover = () => {
|
|
1768
|
+
if (hoverFrame !== undefined) childWindow.cancelAnimationFrame(hoverFrame);
|
|
1769
|
+
hoverFrame = undefined; hoveredFeature = undefined; hoveredCoordinate = undefined;
|
|
1770
|
+
if (hoverHint) hoverHint.hidden = true;
|
|
1771
|
+
drawHighlight();
|
|
1772
|
+
};
|
|
1773
|
+
side.hoverFeature = (index) => {
|
|
1774
|
+
clearHover();
|
|
1775
|
+
if (!inspectedFeatures[index]) return;
|
|
1776
|
+
hoveredFeature = inspectedFeatures[index];
|
|
1777
|
+
hoveredCoordinate = inspectedCoordinate;
|
|
1778
|
+
drawHighlight();
|
|
1779
|
+
};
|
|
1780
|
+
const handleHover = (event) => {
|
|
1781
|
+
if (!inspectionActive || !globalThis.__tileflowDevtoolsUI || hoverFrame !== undefined) return;
|
|
1782
|
+
hoverFrame = childWindow.requestAnimationFrame(() => {
|
|
1783
|
+
hoverFrame = undefined;
|
|
1784
|
+
hoveredFeature = map.queryRenderedFeatures(event.point).find((feature) => !feature.layer.id.startsWith("__tileflow-devtools-"));
|
|
1785
|
+
hoveredCoordinate = [event.lngLat.lng, event.lngLat.lat];
|
|
1786
|
+
if (!hoverHint) {
|
|
1787
|
+
hoverHint = document.createElement("div");
|
|
1788
|
+
hoverHint.style.cssText = "position:absolute;z-index:6;pointer-events:none;padding:4px 7px;background:#fff;color:#202124;border:1px solid #dadce0;border-radius:3px;box-shadow:0 2px 5px #0003;font:11px/1.5 monospace;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
|
|
1789
|
+
map.getContainer().append(hoverHint);
|
|
1790
|
+
}
|
|
1791
|
+
hoverHint.hidden = !hoveredFeature;
|
|
1792
|
+
if (hoveredFeature) {
|
|
1793
|
+
hoverHint.textContent = hoveredFeature.layer.id + " \xB7 " + hoveredFeature.geometry.type + (hoveredFeature.id === undefined ? "" : " #" + hoveredFeature.id);
|
|
1794
|
+
hoverHint.style.left = Math.max(4,Math.min(event.point.x + 14,map.getContainer().clientWidth - 310)) + "px";
|
|
1795
|
+
hoverHint.style.top = Math.max(4,Math.min(event.point.y + 14,map.getContainer().clientHeight - 32)) + "px";
|
|
1796
|
+
}
|
|
1797
|
+
drawHighlight();
|
|
1798
|
+
});
|
|
1799
|
+
};
|
|
547
1800
|
const handleClick = (event) => {
|
|
548
1801
|
if (!inspectionActive) return;
|
|
549
|
-
|
|
1802
|
+
clearHover();
|
|
1803
|
+
inspectedCoordinate = [event.lngLat.lng, event.lngLat.lat];
|
|
1804
|
+
inspectedFeatures = map.queryRenderedFeatures(event.point).filter((feature) => !feature.layer.id.startsWith("__tileflow-devtools-")).slice(0, 16);
|
|
1805
|
+
const features = inspectedFeatures.map(projectFeature);
|
|
550
1806
|
postToParent({
|
|
551
1807
|
type: bridgeInspectionType,
|
|
552
1808
|
schemaVersion: bridgeSchemaVersion,
|
|
@@ -555,6 +1811,13 @@ function renderTileflowComparisonHtml(options) {
|
|
|
555
1811
|
features
|
|
556
1812
|
});
|
|
557
1813
|
};
|
|
1814
|
+
side.inspectRenderedFeature = (feature, coordinate) => {
|
|
1815
|
+
clearHover();
|
|
1816
|
+
inspectedCoordinate = coordinate;
|
|
1817
|
+
inspectedFeatures = [feature];
|
|
1818
|
+
postToParent({type: bridgeInspectionType, schemaVersion: bridgeSchemaVersion,
|
|
1819
|
+
side: side.id, coordinate, features: [projectFeature(feature)]});
|
|
1820
|
+
};
|
|
558
1821
|
const handleError = (event) => {
|
|
559
1822
|
postToParent({
|
|
560
1823
|
type: bridgeRuntimeErrorType,
|
|
@@ -569,7 +1832,9 @@ function renderTileflowComparisonHtml(options) {
|
|
|
569
1832
|
if (!message || message.schemaVersion !== bridgeSchemaVersion) return;
|
|
570
1833
|
if (message.type === bridgeSetInspectionType && typeof message.enabled === "boolean") {
|
|
571
1834
|
inspectionActive = message.enabled;
|
|
1835
|
+
if (!inspectionActive) clearHover();
|
|
572
1836
|
map.getCanvas().style.cursor = inspectionActive ? "crosshair" : "";
|
|
1837
|
+
drawHighlight();
|
|
573
1838
|
return;
|
|
574
1839
|
}
|
|
575
1840
|
if (message.type !== bridgeSetCameraType || !validCamera(message.camera)) return;
|
|
@@ -622,10 +1887,15 @@ function renderTileflowComparisonHtml(options) {
|
|
|
622
1887
|
});
|
|
623
1888
|
});
|
|
624
1889
|
};
|
|
1890
|
+
map.on("move", drawHighlight);
|
|
1891
|
+
map.on("resize", drawHighlight);
|
|
1892
|
+
childWindow.addEventListener("keydown", handleKey);
|
|
625
1893
|
map.on("movestart", handleMoveStart);
|
|
626
1894
|
map.on("move", handleMove);
|
|
627
1895
|
map.on("moveend", handleMoveEnd);
|
|
628
1896
|
map.on("click", handleClick);
|
|
1897
|
+
map.on("mousemove", handleHover);
|
|
1898
|
+
map.on("mouseout", clearHover);
|
|
629
1899
|
map.on("error", handleError);
|
|
630
1900
|
childWindow.addEventListener("message", handleCommand);
|
|
631
1901
|
if (map.loaded?.()) announceReady();
|
|
@@ -638,10 +1908,17 @@ function renderTileflowComparisonHtml(options) {
|
|
|
638
1908
|
if (movementFrame !== undefined) childWindow.cancelAnimationFrame(movementFrame);
|
|
639
1909
|
cancelApplyingRelease();
|
|
640
1910
|
clearTimeout(readyTimer);
|
|
1911
|
+
clearHover(); hoverHint?.remove();
|
|
1912
|
+
highlight?.remove(); side.highlightFeature = undefined; side.hoverFeature = undefined;
|
|
1913
|
+
side.getInspectedFeature = undefined; side.inspectRenderedFeature = undefined;
|
|
1914
|
+
map.off("move", drawHighlight); map.off("resize", drawHighlight);
|
|
1915
|
+
childWindow.removeEventListener("keydown", handleKey);
|
|
641
1916
|
map.off("movestart", handleMoveStart);
|
|
642
1917
|
map.off("move", handleMove);
|
|
643
1918
|
map.off("moveend", handleMoveEnd);
|
|
644
1919
|
map.off("click", handleClick);
|
|
1920
|
+
map.off("mousemove", handleHover);
|
|
1921
|
+
map.off("mouseout", clearHover);
|
|
645
1922
|
map.off("error", handleError);
|
|
646
1923
|
map.off("load", announceReady);
|
|
647
1924
|
map.off("idle", announceReady);
|
|
@@ -657,10 +1934,11 @@ function renderTileflowComparisonHtml(options) {
|
|
|
657
1934
|
if (message.type === bridgeReadyType && validCamera(message.camera)) {
|
|
658
1935
|
side.bridgeReady = true;
|
|
659
1936
|
side.camera = message.camera;
|
|
660
|
-
renderSideStatus(side);
|
|
661
1937
|
maybeEstablishInitialCamera();
|
|
662
1938
|
sendInspectionState(side);
|
|
663
1939
|
void loadSidecar(side);
|
|
1940
|
+
if (options.workbenchUrl) void refreshWorkbench();
|
|
1941
|
+
if (!controlsPanel.hidden) void loadMapControls();
|
|
664
1942
|
updateDocumentReadiness();
|
|
665
1943
|
return;
|
|
666
1944
|
}
|
|
@@ -709,7 +1987,7 @@ function renderTileflowComparisonHtml(options) {
|
|
|
709
1987
|
}
|
|
710
1988
|
|
|
711
1989
|
function sendCamera(side, camera) {
|
|
712
|
-
if (!side
|
|
1990
|
+
if (!side?.bridgeReady || !camera || !side.frame.contentWindow) return;
|
|
713
1991
|
if (side.camera && camerasEqual(side.camera, camera)) return;
|
|
714
1992
|
const requestId = ++requestSequence;
|
|
715
1993
|
side.lastCameraRequestId = requestId;
|
|
@@ -759,6 +2037,16 @@ function renderTileflowComparisonHtml(options) {
|
|
|
759
2037
|
.catch(() => undefined);
|
|
760
2038
|
const events = new EventSource(side.definition.eventsUrl);
|
|
761
2039
|
side.eventSource = events;
|
|
2040
|
+
let connected = false, disconnected = false;
|
|
2041
|
+
events.addEventListener("open", () => {
|
|
2042
|
+
if (connected && disconnected) {
|
|
2043
|
+
side.artifactGeneration = -1;
|
|
2044
|
+
side.latestArtifactStatus = undefined;
|
|
2045
|
+
side.frame.contentWindow?.location.reload();
|
|
2046
|
+
}
|
|
2047
|
+
connected = true; disconnected = false;
|
|
2048
|
+
});
|
|
2049
|
+
events.addEventListener("error", () => { if (connected) disconnected = true; });
|
|
762
2050
|
for (const name of ["building", "ready", "invalid"]) {
|
|
763
2051
|
events.addEventListener(name, (event) => {
|
|
764
2052
|
try { applyArtifactStatus(side, JSON.parse(event.data)); } catch {}
|
|
@@ -781,7 +2069,8 @@ function renderTileflowComparisonHtml(options) {
|
|
|
781
2069
|
? status.lastGoodGeneration
|
|
782
2070
|
: undefined;
|
|
783
2071
|
side.artifactDiagnostic = status.diagnostics?.[0]?.message;
|
|
784
|
-
|
|
2072
|
+
side.latestArtifactStatus = status;
|
|
2073
|
+
side.previewStatusReceiver?.(status);
|
|
785
2074
|
maybeEstablishInitialCamera();
|
|
786
2075
|
updateDocumentReadiness();
|
|
787
2076
|
}
|
|
@@ -800,31 +2089,26 @@ function renderTileflowComparisonHtml(options) {
|
|
|
800
2089
|
stage.dataset.tileflowState = value;
|
|
801
2090
|
}
|
|
802
2091
|
|
|
803
|
-
function renderSideStatus(side) {
|
|
804
|
-
const diagnostic = side.artifactDiagnostic;
|
|
805
|
-
const message = side.runtimeFailed
|
|
806
|
-
? "Runtime error: " + (side.runtimeErrors.at(-1) || "Preview runtime failed")
|
|
807
|
-
: side.artifactStatus === "invalid"
|
|
808
|
-
? "Invalid generation; showing last valid preview" + (diagnostic ? ": " + diagnostic : "")
|
|
809
|
-
: "Generation " + String(Math.max(0, side.artifactGeneration)) + " \xB7 " + side.artifactStatus;
|
|
810
|
-
side.stateElement.dataset.status = side.runtimeFailed ? "error" : side.artifactStatus;
|
|
811
|
-
side.stateElement.title = message;
|
|
812
|
-
side.stateElement.setAttribute("aria-label", side.definition.label + ": " + message);
|
|
813
|
-
side.statusTextElement.textContent = side.runtimeFailed ? "Runtime error" : side.artifactStatus;
|
|
814
|
-
}
|
|
815
2092
|
|
|
816
2093
|
async function loadSidecar(side) {
|
|
817
2094
|
if (!side.definition.sidecarUrl) return undefined;
|
|
2095
|
+
const key = currentSidecarUrl(side);
|
|
2096
|
+
if (side.sidecarKey !== key) {
|
|
2097
|
+
side.sidecarAbort?.abort();
|
|
2098
|
+
side.sidecar = undefined;
|
|
2099
|
+
side.sidecarPromise = undefined;
|
|
2100
|
+
side.sidecarKey = key;
|
|
2101
|
+
}
|
|
818
2102
|
if (side.sidecar !== undefined) return side.sidecar;
|
|
819
2103
|
if (side.sidecarPromise) return side.sidecarPromise;
|
|
820
2104
|
const epoch = side.loadEpoch;
|
|
821
2105
|
const controller = new AbortController();
|
|
822
2106
|
side.sidecarAbort?.abort();
|
|
823
2107
|
side.sidecarAbort = controller;
|
|
824
|
-
const promise = fetch(
|
|
2108
|
+
const promise = fetch(key, {cache: "no-store", signal: controller.signal})
|
|
825
2109
|
.then((response) => response.ok ? response.json() : Promise.reject(new Error("Compiler sidecar unavailable")))
|
|
826
2110
|
.then((document) => {
|
|
827
|
-
if (side.loadEpoch !== epoch || controller.signal.aborted) return undefined;
|
|
2111
|
+
if (side.loadEpoch !== epoch || side.sidecarKey !== key || controller.signal.aborted) return undefined;
|
|
828
2112
|
side.sidecar = document;
|
|
829
2113
|
if (selectedInspection?.side === side) renderInspection();
|
|
830
2114
|
return document;
|
|
@@ -862,6 +2146,9 @@ function renderTileflowComparisonHtml(options) {
|
|
|
862
2146
|
return configured.href;
|
|
863
2147
|
}
|
|
864
2148
|
configured.pathname = configured.pathname.slice(0, markerIndex) + marker + map + "/" + theme + ".json";
|
|
2149
|
+
const controls = livePreviewUrl(side).searchParams.get("controls");
|
|
2150
|
+
if (controls) configured.searchParams.set("controls", controls);
|
|
2151
|
+
else configured.searchParams.delete("controls");
|
|
865
2152
|
return configured.href;
|
|
866
2153
|
}
|
|
867
2154
|
|
|
@@ -870,6 +2157,7 @@ function renderTileflowComparisonHtml(options) {
|
|
|
870
2157
|
const side = selectedInspection.side;
|
|
871
2158
|
const feature = selectedInspection.features[selectedInspection.featureIndex];
|
|
872
2159
|
inspectionOutput.replaceChildren();
|
|
2160
|
+
for (const entry of Object.values(states)) entry.highlightFeature?.(entry === side ? selectedInspection.featureIndex : -1);
|
|
873
2161
|
if (!feature) {
|
|
874
2162
|
inspectionOutput.className = "empty";
|
|
875
2163
|
inspectionOutput.textContent = side.definition.label + ": no rendered feature at this point.";
|
|
@@ -877,6 +2165,7 @@ function renderTileflowComparisonHtml(options) {
|
|
|
877
2165
|
curveOutput.className = "empty";
|
|
878
2166
|
curveOutput.textContent = "Paint and layout values appear here.";
|
|
879
2167
|
sidecarOutput.textContent = "No layer selected.";
|
|
2168
|
+
globalThis.__tileflowDevtoolsUI?.selectionChanged();
|
|
880
2169
|
return;
|
|
881
2170
|
}
|
|
882
2171
|
inspectionOutput.className = "";
|
|
@@ -885,14 +2174,14 @@ function renderTileflowComparisonHtml(options) {
|
|
|
885
2174
|
title.textContent = side.definition.label + " \xB7 " + feature.layerId;
|
|
886
2175
|
const pickerLabel = document.createElement("label");
|
|
887
2176
|
pickerLabel.className = "feature-picker";
|
|
888
|
-
pickerLabel.append(document.createTextNode("
|
|
2177
|
+
pickerLabel.append(document.createTextNode("Elements at this point (" + selectedInspection.features.length + ")"));
|
|
889
2178
|
const picker = document.createElement("select");
|
|
890
2179
|
picker.setAttribute("aria-label", "Rendered feature at inspected point");
|
|
891
2180
|
selectedInspection.features.forEach((candidate, index) => {
|
|
892
2181
|
const option = document.createElement("option");
|
|
893
2182
|
option.value = String(index);
|
|
894
|
-
option.textContent = String(index + 1) + ". " + candidate.layerId + " \xB7 " + candidate.geometryType +
|
|
895
|
-
|
|
2183
|
+
option.textContent = options.workbenchUrl ? featureDisplayName(candidate) + " \xB7 " + candidate.geometryType : String(index + 1) + ". " + candidate.layerId + " \xB7 " + candidate.geometryType + (candidate.id === null ? "" : " \xB7 " + String(candidate.id));
|
|
2184
|
+
option.title = candidate.layerId;
|
|
896
2185
|
picker.append(option);
|
|
897
2186
|
});
|
|
898
2187
|
picker.value = String(selectedInspection.featureIndex);
|
|
@@ -915,13 +2204,15 @@ function renderTileflowComparisonHtml(options) {
|
|
|
915
2204
|
filter: layer.filter
|
|
916
2205
|
} : undefined
|
|
917
2206
|
});
|
|
918
|
-
|
|
2207
|
+
if (options.workbenchUrl) renderFeatureSummary(side, feature, layer, pickerLabel, summary);
|
|
2208
|
+
else inspectionOutput.append(title, pickerLabel, summary);
|
|
919
2209
|
populateCurveProperties(layer);
|
|
920
2210
|
const sidecarLayer = findSidecarLayer(side.sidecar, feature.layerId);
|
|
921
2211
|
sidecarOutput.textContent = sidecarLayer
|
|
922
2212
|
? boundedJson(sidecarLayerWithSemanticAttribution(sidecarLayer))
|
|
923
2213
|
: "No compiler sidecar entry for this layer.";
|
|
924
2214
|
if (side.sidecar === undefined && !side.sidecarPromise) void loadSidecar(side);
|
|
2215
|
+
globalThis.__tileflowDevtoolsUI?.selectionChanged();
|
|
925
2216
|
}
|
|
926
2217
|
|
|
927
2218
|
function populateCurveProperties(layer) {
|
|
@@ -1199,103 +2490,14 @@ function renderTileflowComparisonHtml(options) {
|
|
|
1199
2490
|
value.x + value.width <= image.naturalWidth && value.y + value.height <= image.naturalHeight;
|
|
1200
2491
|
}
|
|
1201
2492
|
|
|
1202
|
-
async function copyScene(commandOnly) {
|
|
1203
|
-
const name = sceneNameInput.value;
|
|
1204
|
-
if (!validPortableName(name)) {
|
|
1205
|
-
setFeedback("Use a portable kebab-case scene name.");
|
|
1206
|
-
return;
|
|
1207
|
-
}
|
|
1208
|
-
const side = states[activeSideId];
|
|
1209
|
-
const camera = side.camera || canonicalCamera;
|
|
1210
|
-
if (!camera) {
|
|
1211
|
-
setFeedback("Wait for the maps to become ready.");
|
|
1212
|
-
return;
|
|
1213
|
-
}
|
|
1214
|
-
const style = side.map?.getStyle?.();
|
|
1215
|
-
const map = style?.metadata?.["tileflow:map"];
|
|
1216
|
-
const theme = style?.metadata?.["tileflow:theme"];
|
|
1217
|
-
if (!validPortableName(map) || !validConcreteThemeName(theme)) {
|
|
1218
|
-
setFeedback("The active preview does not expose a concrete map and theme.");
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
const dpr = Number(sceneDprInput.value) === 2 ? 2 : 1;
|
|
1222
|
-
const viewport = readMapViewport(side, dpr);
|
|
1223
|
-
const {width, height} = viewport;
|
|
1224
|
-
if (commandOnly) {
|
|
1225
|
-
const command = [
|
|
1226
|
-
"tileflow capture",
|
|
1227
|
-
...(side.definition.captureConfig
|
|
1228
|
-
? ["--config " + quoteCliArgument(side.definition.captureConfig)]
|
|
1229
|
-
: []),
|
|
1230
|
-
"--map " + quoteCliArgument(map),
|
|
1231
|
-
"--theme " + quoteCliArgument(theme),
|
|
1232
|
-
"--center=" + formatNumber(camera.center[0]) + "," + formatNumber(camera.center[1]),
|
|
1233
|
-
"--zoom=" + formatNumber(camera.zoom),
|
|
1234
|
-
"--bearing=" + formatNumber(camera.bearing),
|
|
1235
|
-
"--pitch=" + formatNumber(camera.pitch),
|
|
1236
|
-
"--width=" + width,
|
|
1237
|
-
"--height=" + height,
|
|
1238
|
-
"--dpr=" + dpr,
|
|
1239
|
-
"--out=" + quoteCliArgument(name + ".png")
|
|
1240
|
-
].join(" ");
|
|
1241
|
-
if (!await copyText(command)) {
|
|
1242
|
-
setFeedback("Clipboard access is unavailable.");
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
setFeedback("Exploratory capture command copied.");
|
|
1246
|
-
return;
|
|
1247
|
-
}
|
|
1248
|
-
const definition = {
|
|
1249
|
-
theme,
|
|
1250
|
-
camera: {
|
|
1251
|
-
type: "center",
|
|
1252
|
-
center: [roundNumber(camera.center[0]), roundNumber(camera.center[1])],
|
|
1253
|
-
zoom: roundNumber(camera.zoom),
|
|
1254
|
-
bearing: roundNumber(camera.bearing),
|
|
1255
|
-
pitch: roundNumber(camera.pitch)
|
|
1256
|
-
},
|
|
1257
|
-
viewport: {width, height, dpr},
|
|
1258
|
-
target: {kind: "map"}
|
|
1259
|
-
};
|
|
1260
|
-
const serialized = JSON.stringify(definition, null, 2).replace(/^/gm, " ");
|
|
1261
|
-
if (!await copyText("'" + name + "': " + serialized.trimStart() + ",")) {
|
|
1262
|
-
setFeedback("Clipboard access is unavailable.");
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
setFeedback("Scene entry copied.");
|
|
1266
|
-
}
|
|
1267
2493
|
|
|
1268
|
-
function readMapViewport(side, dpr) {
|
|
1269
|
-
const container = side.map?.getContainer?.();
|
|
1270
|
-
const canvas = side.map?.getCanvas?.();
|
|
1271
|
-
const maximum = dpr === 2 ? 2048 : 4096;
|
|
1272
|
-
const width = boundedInteger(container?.clientWidth || canvas?.clientWidth, 64, maximum, 1280);
|
|
1273
|
-
const height = boundedInteger(container?.clientHeight || canvas?.clientHeight, 64, maximum, 800);
|
|
1274
|
-
return {width, height};
|
|
1275
|
-
}
|
|
1276
2494
|
|
|
1277
|
-
async function copyText(value) {
|
|
1278
|
-
try {
|
|
1279
|
-
await navigator.clipboard.writeText(value);
|
|
1280
|
-
return true;
|
|
1281
|
-
} catch {
|
|
1282
|
-
const textarea = document.createElement("textarea");
|
|
1283
|
-
textarea.value = value;
|
|
1284
|
-
textarea.style.position = "fixed";
|
|
1285
|
-
textarea.style.opacity = "0";
|
|
1286
|
-
document.body.append(textarea);
|
|
1287
|
-
textarea.select();
|
|
1288
|
-
const copied = document.execCommand("copy");
|
|
1289
|
-
textarea.remove();
|
|
1290
|
-
return copied;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
2495
|
|
|
1294
2496
|
function writeUrlState() {
|
|
1295
2497
|
const url = new URL(location.href);
|
|
1296
2498
|
url.searchParams.set("mode", stage.dataset.mode);
|
|
1297
|
-
url.searchParams.set("split", String(Math.round(
|
|
1298
|
-
url.searchParams.set("alpha", String(Math.round(
|
|
2499
|
+
url.searchParams.set("split", String(Math.round(initialUi.split)));
|
|
2500
|
+
url.searchParams.set("alpha", String(Math.round(initialUi.alpha)));
|
|
1299
2501
|
url.searchParams.set("driver", stage.dataset.driver);
|
|
1300
2502
|
if (canonicalCamera) {
|
|
1301
2503
|
const values = {
|
|
@@ -1310,9 +2512,9 @@ function renderTileflowComparisonHtml(options) {
|
|
|
1310
2512
|
function readUiFromUrl() {
|
|
1311
2513
|
const query = new URL(location.href).searchParams;
|
|
1312
2514
|
const modeValue = singleQueryValue(query, "mode");
|
|
1313
|
-
const mode = ["side-by-side", "split", "overlay", "blink"].includes(modeValue) ? modeValue : undefined;
|
|
2515
|
+
const mode = ["single", "side-by-side", "split", "overlay", "blink"].includes(modeValue) ? modeValue : undefined;
|
|
1314
2516
|
const driver = singleQueryValue(query, "driver");
|
|
1315
|
-
selectSide(driver === "right" ? "right" : "left", false);
|
|
2517
|
+
selectSide(driver === "right" && options.right ? "right" : "left", false);
|
|
1316
2518
|
const split = singleQueryValue(query, "split");
|
|
1317
2519
|
const alpha = singleQueryValue(query, "alpha");
|
|
1318
2520
|
return {
|
|
@@ -1414,7 +2616,6 @@ function renderTileflowComparisonHtml(options) {
|
|
|
1414
2616
|
if (!side.runtimeErrors.includes(message)) side.runtimeErrors.push(message);
|
|
1415
2617
|
side.runtimeErrors = side.runtimeErrors.slice(-16);
|
|
1416
2618
|
side.runtimeFailed = true;
|
|
1417
|
-
renderSideStatus(side);
|
|
1418
2619
|
updateDocumentReadiness();
|
|
1419
2620
|
}
|
|
1420
2621
|
|
|
@@ -1482,8 +2683,32 @@ function renderTileflowComparisonHtml(options) {
|
|
|
1482
2683
|
function roundNumber(value) { return Number(value.toFixed(6)); }
|
|
1483
2684
|
function formatNumber(value) { return String(roundNumber(value)); }
|
|
1484
2685
|
function wrapLongitude(value) { return ((value + 180) % 360 + 360) % 360 - 180; }
|
|
1485
|
-
function setFeedback(value) { feedback
|
|
2686
|
+
function setFeedback(value) { globalThis.__tileflowDevtoolsUI?.feedback(value); }
|
|
2687
|
+
${controlsClient}
|
|
2688
|
+
${workbenchClient}
|
|
2689
|
+
globalThis.__tileflowDevtoolsHost = {
|
|
2690
|
+
schemaVersion: 1,
|
|
2691
|
+
subscribePreviewStatus: (child, receive) => {
|
|
2692
|
+
const side = Object.values(states).find((entry) => entry.frame.contentWindow === child);
|
|
2693
|
+
if (!side) return undefined;
|
|
2694
|
+
side.previewStatusReceiver = receive;
|
|
2695
|
+
if (side.latestArtifactStatus) receive(side.latestArtifactStatus);
|
|
2696
|
+
return () => { if (side.previewStatusReceiver === receive) side.previewStatusReceiver = undefined; };
|
|
2697
|
+
},
|
|
2698
|
+
sides: () => Object.values(states), active: () => states[activeSideId],
|
|
2699
|
+
selection: () => selectedInspection,
|
|
2700
|
+
clearFeatureSelection: () => {
|
|
2701
|
+
selectedInspection = undefined;
|
|
2702
|
+
for (const side of Object.values(states)) { side.highlightFeature?.(-1); side.hoverFeature?.(-1); }
|
|
2703
|
+
},
|
|
2704
|
+
loadSidecar: (side) => loadSidecar(side),
|
|
2705
|
+
selectFeature: (index) => { if (selectedInspection && selectedInspection.features[index]) { selectedInspection.featureIndex = index; renderInspection(); } },
|
|
2706
|
+
inspect: (enabled) => setInspectionEnabled(enabled), inspecting: () => inspectionEnabled,
|
|
2707
|
+
controls: () => loadMapControls(),
|
|
2708
|
+
sprites: () => loadSprites(states[activeSideId])
|
|
2709
|
+
};
|
|
1486
2710
|
</script>
|
|
2711
|
+
<script type="module"${nonceAttribute} src="${fontBasePath}/__workbench-runtime/devtools.js"></script>
|
|
1487
2712
|
</body>
|
|
1488
2713
|
</html>`;
|
|
1489
2714
|
}
|
|
@@ -1515,15 +2740,56 @@ var tileflowComparisonSchemaVersion = 1;
|
|
|
1515
2740
|
function createTileflowComparisonRequestHandler(options) {
|
|
1516
2741
|
const comparisonBasePath = normalizeTileflowBasePath(options.basePath);
|
|
1517
2742
|
const left = normalizeSide(options.left, "left", comparisonBasePath);
|
|
1518
|
-
const right = normalizeSide(options.right, "right", comparisonBasePath);
|
|
1519
|
-
assertDisjointSideRoutes(left.basePath, right.basePath);
|
|
1520
|
-
const
|
|
1521
|
-
|
|
2743
|
+
const right = options.right ? normalizeSide(options.right, "right", comparisonBasePath) : void 0;
|
|
2744
|
+
if (right) assertDisjointSideRoutes(left.basePath, right.basePath);
|
|
2745
|
+
const workbenchUrl = options.workbench ? normalizeTileflowBasePath(options.workbench.url) : void 0;
|
|
2746
|
+
if (workbenchUrl !== void 0) {
|
|
2747
|
+
if (!workbenchUrl || comparisonBasePath && !matchesBasePath(workbenchUrl, comparisonBasePath))
|
|
2748
|
+
throw new TypeError(
|
|
2749
|
+
"Map DevTools API must own a non-root route inside the comparison base path."
|
|
2750
|
+
);
|
|
2751
|
+
assertDisjointSideRoutes(workbenchUrl, left.basePath);
|
|
2752
|
+
if (right) assertDisjointSideRoutes(workbenchUrl, right.basePath);
|
|
2753
|
+
assertDisjointSideRoutes(workbenchUrl, `${comparisonBasePath}/__workbench-fonts`);
|
|
2754
|
+
assertDisjointSideRoutes(workbenchUrl, `${comparisonBasePath}/__workbench-runtime`);
|
|
2755
|
+
}
|
|
2756
|
+
const title = normalizeLabel(options.title ?? "Tileflow Map DevTools", "title");
|
|
2757
|
+
const initialMode = right ? options.initialMode ?? "side-by-side" : "single";
|
|
1522
2758
|
return async function handleTileflowComparisonRequest(request) {
|
|
1523
2759
|
const url = new URL(request.url);
|
|
1524
2760
|
const pathname = url.pathname;
|
|
2761
|
+
if (pathname === `${comparisonBasePath}/__workbench-runtime/devtools.js`) {
|
|
2762
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
2763
|
+
return jsonResponse({ error: "Method not allowed" }, 405);
|
|
2764
|
+
const source = await readFile(
|
|
2765
|
+
new URL("../dist/devtools-browser.js", import.meta.url),
|
|
2766
|
+
"utf8"
|
|
2767
|
+
);
|
|
2768
|
+
return new Response(request.method === "HEAD" ? null : source, {
|
|
2769
|
+
headers: {
|
|
2770
|
+
"Content-Type": "text/javascript; charset=utf-8",
|
|
2771
|
+
"Cache-Control": "no-store",
|
|
2772
|
+
"X-Content-Type-Options": "nosniff"
|
|
2773
|
+
}
|
|
2774
|
+
});
|
|
2775
|
+
}
|
|
2776
|
+
const font = pathname.slice(`${comparisonBasePath}/__workbench-fonts/`.length);
|
|
2777
|
+
if (pathname.startsWith(`${comparisonBasePath}/__workbench-fonts/`) && ["mona-sans.woff2", "hubot-sans.woff2", "inter.woff2", "jetbrains-mono.woff2"].includes(font)) {
|
|
2778
|
+
if (request.method !== "GET" && request.method !== "HEAD")
|
|
2779
|
+
return jsonResponse({ error: "Method not allowed" }, 405);
|
|
2780
|
+
const bytes = await readFile(new URL(`../ui-fonts/${font}`, import.meta.url));
|
|
2781
|
+
return new Response(request.method === "HEAD" ? null : bytes, {
|
|
2782
|
+
headers: {
|
|
2783
|
+
"Content-Type": "font/woff2",
|
|
2784
|
+
"Cache-Control": "public, max-age=3600",
|
|
2785
|
+
"X-Content-Type-Options": "nosniff"
|
|
2786
|
+
}
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
if (options.workbench && workbenchUrl && matchesBasePath(pathname, workbenchUrl))
|
|
2790
|
+
return await options.workbench.handle(request);
|
|
1525
2791
|
if (matchesBasePath(pathname, left.basePath)) return await left.handler(request);
|
|
1526
|
-
if (matchesBasePath(pathname, right.basePath)) return await right.handler(request);
|
|
2792
|
+
if (right && matchesBasePath(pathname, right.basePath)) return await right.handler(request);
|
|
1527
2793
|
if (isComparisonRoot(pathname, comparisonBasePath)) {
|
|
1528
2794
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
1529
2795
|
return jsonResponse({ error: "Method not allowed" }, 405);
|
|
@@ -1534,9 +2800,10 @@ function createTileflowComparisonRequestHandler(options) {
|
|
|
1534
2800
|
cspNonce,
|
|
1535
2801
|
initialMode,
|
|
1536
2802
|
left: publicSide(left),
|
|
1537
|
-
right: publicSide(right),
|
|
2803
|
+
...right ? { right: publicSide(right) } : {},
|
|
1538
2804
|
schemaVersion: tileflowComparisonSchemaVersion,
|
|
1539
|
-
title
|
|
2805
|
+
title,
|
|
2806
|
+
...workbenchUrl ? { workbenchUrl } : {}
|
|
1540
2807
|
});
|
|
1541
2808
|
return new Response(request.method === "HEAD" ? null : html, {
|
|
1542
2809
|
headers: {
|
|
@@ -1572,6 +2839,7 @@ function normalizeSide(side, name, comparisonBasePath) {
|
|
|
1572
2839
|
if (!basePath) {
|
|
1573
2840
|
throw new TypeError(`Tileflow comparison ${name} basePath must not own the server root.`);
|
|
1574
2841
|
}
|
|
2842
|
+
assertDisjointSideRoutes(basePath, `${comparisonBasePath}/__workbench-runtime`);
|
|
1575
2843
|
if (comparisonBasePath && !matchesBasePath(basePath, comparisonBasePath)) {
|
|
1576
2844
|
throw new TypeError(
|
|
1577
2845
|
`Tileflow comparison ${name} basePath must be inside ${comparisonBasePath}.`
|
|
@@ -1724,6 +2992,8 @@ function createTileflowDevRequestHandler(options = {}) {
|
|
|
1724
2992
|
const lineBackgroundGlyphs = createTileflowLineBackgroundGlyphResponder({ cwd });
|
|
1725
2993
|
let closed = false;
|
|
1726
2994
|
let lastOrigin;
|
|
2995
|
+
let controlsGeneration;
|
|
2996
|
+
const controlsCache = /* @__PURE__ */ new Map();
|
|
1727
2997
|
const loadOwnedSession = (origin) => {
|
|
1728
2998
|
lastOrigin = origin;
|
|
1729
2999
|
ownedSessionPromise ??= createTileflowArtifactSession({
|
|
@@ -1755,13 +3025,30 @@ function createTileflowDevRequestHandler(options = {}) {
|
|
|
1755
3025
|
return options.session ? eventStreamResponse(options.session) : jsonResponse2({ error: "Live events require a development session." }, 404);
|
|
1756
3026
|
}
|
|
1757
3027
|
if (path.startsWith("/__runtime/")) {
|
|
1758
|
-
const runtimeResponse = (await import("./preview-assets-
|
|
3028
|
+
const runtimeResponse = (await import("./preview-assets-KGUICQHG.js")).getTileflowPreviewRuntimeResponse(path);
|
|
1759
3029
|
if (runtimeResponse) return runtimeResponse;
|
|
1760
3030
|
}
|
|
1761
3031
|
const session = options.session ?? await loadOwnedSession(url.origin);
|
|
1762
3032
|
acquisition = session.acquireArtifacts();
|
|
1763
3033
|
const artifacts = acquisition?.artifacts;
|
|
1764
3034
|
const state = session.getState();
|
|
3035
|
+
const controls = url.searchParams.get("controls");
|
|
3036
|
+
if (controls !== null) parsePreviewControls(controls);
|
|
3037
|
+
const controlled = (map, theme) => {
|
|
3038
|
+
if (!artifacts || controls === null) return void 0;
|
|
3039
|
+
if (controlsGeneration !== acquisition?.generation) {
|
|
3040
|
+
controlsCache.clear();
|
|
3041
|
+
controlsGeneration = acquisition?.generation;
|
|
3042
|
+
}
|
|
3043
|
+
const key = JSON.stringify([map, theme, controls]);
|
|
3044
|
+
let value = controlsCache.get(key);
|
|
3045
|
+
if (!value) {
|
|
3046
|
+
value = compilePreviewControls(artifacts, map, theme, controls, apiBaseUrl);
|
|
3047
|
+
if (controlsCache.size >= 8) controlsCache.delete(controlsCache.keys().next().value);
|
|
3048
|
+
controlsCache.set(key, value);
|
|
3049
|
+
}
|
|
3050
|
+
return value;
|
|
3051
|
+
};
|
|
1765
3052
|
if (path === "/__status") {
|
|
1766
3053
|
return jsonResponse2(createTileflowArtifactStatus(state));
|
|
1767
3054
|
}
|
|
@@ -1769,20 +3056,24 @@ function createTileflowDevRequestHandler(options = {}) {
|
|
|
1769
3056
|
const { renderTileflowPreviewHtml, resolveTileflowPreview } = await import("./preview.js");
|
|
1770
3057
|
const requestedMaps = url.searchParams.getAll("map");
|
|
1771
3058
|
const requestedThemes = url.searchParams.getAll("theme");
|
|
3059
|
+
const requestedScenes = url.searchParams.getAll("scene");
|
|
3060
|
+
if (requestedScenes.length > 1) {
|
|
3061
|
+
return jsonResponse2({ error: "Tileflow scene query must appear at most once." }, 400);
|
|
3062
|
+
}
|
|
1772
3063
|
if (requestedMaps.length > 1) {
|
|
1773
3064
|
return jsonResponse2({ error: "Tileflow map query must appear at most once." }, 400);
|
|
1774
3065
|
}
|
|
1775
3066
|
if (requestedThemes.length > 1) {
|
|
1776
3067
|
return jsonResponse2({ error: "Tileflow theme query must appear at most once." }, 400);
|
|
1777
3068
|
}
|
|
1778
|
-
const requestedMap = requestedMaps.length
|
|
1779
|
-
const requestedTheme = requestedThemes.length
|
|
3069
|
+
const requestedMap = requestedMaps[0] ?? (requestedScenes.length ? void 0 : options.map);
|
|
3070
|
+
const requestedTheme = requestedThemes[0] ?? (requestedScenes.length ? void 0 : options.theme);
|
|
1780
3071
|
const preview = artifacts ? resolveTileflowPreview(artifacts.project, {
|
|
1781
3072
|
map: requestedMap,
|
|
1782
|
-
scene: options.scene,
|
|
3073
|
+
scene: requestedScenes[0] ?? (requestedMaps.length ? void 0 : options.scene),
|
|
1783
3074
|
theme: requestedTheme
|
|
1784
3075
|
}) : void 0;
|
|
1785
|
-
const previewStyle = preview ? artifacts?.styles[preview.mapName]?.[preview.themeName] : void 0;
|
|
3076
|
+
const previewStyle = preview ? controlled(preview.mapName, preview.themeName)?.style ?? artifacts?.styles[preview.mapName]?.[preview.themeName] : void 0;
|
|
1786
3077
|
const isSemanticPreview = preview !== void 0 && previewStyle?.metadata?.["tileflow:compiler"] === "tileflow-semantic";
|
|
1787
3078
|
const fontFaces = previewStyle ? getTileflowStyleFontFaces(previewStyle) : [];
|
|
1788
3079
|
return htmlResponse(
|
|
@@ -1791,11 +3082,23 @@ function createTileflowDevRequestHandler(options = {}) {
|
|
|
1791
3082
|
basePath,
|
|
1792
3083
|
createTileflowArtifactStatus(state),
|
|
1793
3084
|
isSemanticPreview,
|
|
1794
|
-
fontFaces
|
|
3085
|
+
fontFaces,
|
|
3086
|
+
options.fitViewport,
|
|
3087
|
+
controls ?? void 0
|
|
1795
3088
|
)
|
|
1796
3089
|
);
|
|
1797
3090
|
}
|
|
1798
3091
|
if (!artifacts) return unavailableArtifactsResponse(state);
|
|
3092
|
+
if (path === "/__controls") {
|
|
3093
|
+
return compactJsonResponse({
|
|
3094
|
+
...describePreviewControls(
|
|
3095
|
+
artifacts,
|
|
3096
|
+
url.searchParams.get("map") ?? "",
|
|
3097
|
+
url.searchParams.get("theme") ?? ""
|
|
3098
|
+
),
|
|
3099
|
+
generation: acquisition?.generation
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
1799
3102
|
if (path.startsWith(`/${tileflowLineBackgroundGlyphRoute}/`)) {
|
|
1800
3103
|
const glyphRequest = parseTileflowLineBackgroundGlyphPath(path);
|
|
1801
3104
|
const upstream = glyphRequest ? artifacts.lineBackgroundGlyphs?.[glyphRequest.mapName] : void 0;
|
|
@@ -1824,7 +3127,7 @@ function createTileflowDevRequestHandler(options = {}) {
|
|
|
1824
3127
|
}
|
|
1825
3128
|
if (path.startsWith("/__inspection/")) {
|
|
1826
3129
|
const selection = getTileflowStyleInspectionSelection(path);
|
|
1827
|
-
const inspection = selection ? artifacts.styleInspections?.[selection.mapName]?.[selection.themeName] : void 0;
|
|
3130
|
+
const inspection = selection ? controlled(selection.mapName, selection.themeName)?.inspection ?? artifacts.styleInspections?.[selection.mapName]?.[selection.themeName] : void 0;
|
|
1828
3131
|
if (!selection || !inspection) {
|
|
1829
3132
|
return jsonResponse2({ error: "Compiler inspection is unavailable." }, 404);
|
|
1830
3133
|
}
|
|
@@ -1832,7 +3135,7 @@ function createTileflowDevRequestHandler(options = {}) {
|
|
|
1832
3135
|
}
|
|
1833
3136
|
if (path.startsWith("/styles/")) {
|
|
1834
3137
|
const selection = getTileflowStyleSelection(path);
|
|
1835
|
-
const style = selection ? artifacts.styles[selection.mapName]?.[selection.themeName] : void 0;
|
|
3138
|
+
const style = selection ? controlled(selection.mapName, selection.themeName)?.style ?? artifacts.styles[selection.mapName]?.[selection.themeName] : void 0;
|
|
1836
3139
|
if (!selection || !style) {
|
|
1837
3140
|
return jsonResponse2({ error: "Unknown map theme style." }, 404);
|
|
1838
3141
|
}
|
|
@@ -1854,9 +3157,23 @@ function createTileflowDevRequestHandler(options = {}) {
|
|
|
1854
3157
|
}
|
|
1855
3158
|
return assetResponse(asset);
|
|
1856
3159
|
}
|
|
3160
|
+
if (path.startsWith("/models/")) {
|
|
3161
|
+
const assetName = path.replace(/^\/+/, "");
|
|
3162
|
+
const asset = artifacts.assets.find((candidate) => candidate.fileName === assetName);
|
|
3163
|
+
if (!asset) {
|
|
3164
|
+
return jsonResponse2({ error: `Unknown model asset: ${assetName}` }, 404);
|
|
3165
|
+
}
|
|
3166
|
+
return assetResponse(asset);
|
|
3167
|
+
}
|
|
1857
3168
|
return jsonResponse2({ error: "Not found" }, 404);
|
|
1858
3169
|
} catch (error) {
|
|
1859
3170
|
options.onError?.(error);
|
|
3171
|
+
if (url.searchParams.has("controls") || path === "/__controls") {
|
|
3172
|
+
return jsonResponse2(
|
|
3173
|
+
{ error: error instanceof Error ? error.message : "Invalid map controls." },
|
|
3174
|
+
400
|
|
3175
|
+
);
|
|
3176
|
+
}
|
|
1860
3177
|
return tileflowErrorResponse(error);
|
|
1861
3178
|
} finally {
|
|
1862
3179
|
await acquisition?.release();
|
|
@@ -2004,7 +3321,7 @@ function getRequestPathname(url) {
|
|
|
2004
3321
|
}
|
|
2005
3322
|
}
|
|
2006
3323
|
function isOwnedTileflowRequestPath(path) {
|
|
2007
|
-
return path === "/" || path === "/build-manifest.json" || path === "/manifest.json" || path === "/__events" || path === "/__status" || getTileflowStyleInspectionSelection(path) !== void 0 || path.startsWith("/generations/") || path.startsWith("/icons/") || path.startsWith("/tilesets/") || getTileflowStyleSelection(path) !== void 0 || path.startsWith("/fonts/") || path.startsWith(`/${tileflowLineBackgroundGlyphRoute}/`) || path.startsWith("/__runtime/");
|
|
3324
|
+
return path === "/" || path === "/build-manifest.json" || path === "/manifest.json" || path === "/__events" || path === "/__status" || path === "/__controls" || getTileflowStyleInspectionSelection(path) !== void 0 || path.startsWith("/generations/") || path.startsWith("/icons/") || path.startsWith("/tilesets/") || getTileflowStyleSelection(path) !== void 0 || path.startsWith("/fonts/") || path.startsWith("/models/") || path.startsWith(`/${tileflowLineBackgroundGlyphRoute}/`) || path.startsWith("/__runtime/");
|
|
2008
3325
|
}
|
|
2009
3326
|
function tileflowErrorResponse(error) {
|
|
2010
3327
|
if (error instanceof Error && "code" in error && error.code === "PREVIEW_SELECTION_INVALID") {
|