@tileflow/dev 0.1.0-alpha.31 → 0.1.0-alpha.33

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/dist/preview.js CHANGED
@@ -140,11 +140,101 @@ function normalizeTileflowLandmarkManifest(candidate, manifestUrl) {
140
140
  };
141
141
  }
142
142
 
143
+ // src/preview-scene.ts
144
+ var previewSceneClient = String.raw`
145
+ let sceneStyleRevision = 0;
146
+ globalThis.__tileflowApplyScene = async (serialized, isCurrent) => {
147
+ const next = JSON.parse(serialized);
148
+ if (next.style) {
149
+ // getStyle() retains renderer instrumentation on sources. It is not authored
150
+ // style-spec input; MapLibre adds it back from the map's timing option.
151
+ delete next.style.collectResourceTiming;
152
+ for (const source of Object.values(next.style.sources || {})) delete source.collectResourceTiming;
153
+ await loadTileflowStyleFonts(next.style, {fontFaces: previewFontFaces});
154
+ }
155
+ if (!isCurrent()) return false;
156
+ const context = map.getCanvas().getContext("webgl2") || map.getCanvas().getContext("webgl");
157
+ if (context?.isContextLost()) throw new Error("The map's graphics context is recovering. Try the change again.");
158
+ if (next.style) {
159
+ // MapLibre ignores metadata-only diffs. A global-state revision also commits the
160
+ // new 3D design metadata, without replacing sources, tiles or custom layers.
161
+ next.style.state = {...next.style.state, "tileflow:preview-revision": ++sceneStyleRevision};
162
+ const layerOrder = map.getLayersOrder();
163
+ map.setStyle(next.style, {diff:true});
164
+ // Custom layers are omitted from the serialized style. A diff can reinsert the
165
+ // surrounding style layers above them, covering 3D with the flat map. Restore
166
+ // their placement before the nearest surviving successor, without remounting.
167
+ let before;
168
+ for (let index = layerOrder.length - 1; index >= 0; index--) {
169
+ const id = layerOrder[index], layer = map.getLayer(id);
170
+ if (!layer) continue;
171
+ if (layer.type === "custom") map.moveLayer(id, before);
172
+ before = id;
173
+ }
174
+ }
175
+ previewSearchParameters.set("animation", String(next.animation));
176
+ previewSearchParameters.delete("trees3d");
177
+ const visibleTrees = map.getStyle()?.metadata?.["tileflow:vegetation-display"]?.visible ?? true;
178
+ threeDimensionalControl?.setEnabled(next.buildings3d);
179
+ treeControl?.setEnabled(visibleTrees);
180
+ mountCoreScene(map);
181
+ treeControl?.update();
182
+ updateGlobeBackdrop(map);
183
+ const url = new URL(location.href);
184
+ url.searchParams.delete("scene");
185
+ url.searchParams.set("map", next.map);
186
+ url.searchParams.set("theme", next.theme);
187
+ if (Object.keys(next.controls).length) url.searchParams.set("controls", JSON.stringify(next.controls));
188
+ else url.searchParams.delete("controls");
189
+ if (next.animation) url.searchParams.set("animation", String(next.animation));
190
+ else url.searchParams.delete("animation");
191
+ history.replaceState(history.state, "", url.href);
192
+ // Read the current camera here; a slow compile must never undo navigation.
193
+ writeCameraToUrl(map);
194
+ map.triggerRepaint();
195
+ return true;
196
+ };
197
+ `;
198
+
199
+ // src/preview-scene-themes.ts
200
+ function createPreviewSceneThemes(map, readSun) {
201
+ const cache = {
202
+ value: void 0,
203
+ snapshot() {
204
+ if (!cache.value) {
205
+ const metadata = map.getStyle()?.metadata;
206
+ cache.value = { metadata, sun: readSun(metadata) };
207
+ }
208
+ return cache.value;
209
+ },
210
+ invalidate() {
211
+ cache.value = void 0;
212
+ },
213
+ release() {
214
+ map.off("styledata", cache.invalidate);
215
+ map.off("style.load", cache.invalidate);
216
+ map.off("remove", cache.release);
217
+ cache.invalidate();
218
+ }
219
+ };
220
+ map.on("styledata", cache.invalidate);
221
+ map.on("style.load", cache.invalidate);
222
+ map.on("remove", cache.release);
223
+ return {
224
+ getSun() {
225
+ return cache.snapshot().sun;
226
+ },
227
+ getThemeMetadata(key) {
228
+ return { values: [cache.snapshot().metadata?.[key]], position: 0 };
229
+ }
230
+ };
231
+ }
232
+
143
233
  // src/preview-html.ts
144
- function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticPreview, fontFaces = []) {
145
- const styleUrl = preview ? `${basePath}/styles/${preview.mapName}/${preview.themeName}.json` : void 0;
234
+ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticPreview, fontFaces = [], fitViewport = false, controls) {
235
+ const styleUrl = preview ? `${basePath}/styles/${preview.mapName}/${preview.themeName}.json${controls ? `?${new URLSearchParams({ controls })}` : ""}` : void 0;
146
236
  const mapOptions = preview ? previewMapOptions(preview) : void 0;
147
- const viewportCss = preview?.viewport ? `
237
+ const viewportCss = preview?.viewport && !fitViewport ? `
148
238
  html, body { min-height: 100%; margin: 0; }
149
239
  body {
150
240
  display: grid;
@@ -168,7 +258,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
168
258
  ${viewportCss}
169
259
  body { font-family: ui-sans-serif, system-ui, sans-serif; }
170
260
  #map {
171
- --tileflow-globe-radius: 220px;
172
261
  background-color: var(--tileflow-space, #2F5070);
173
262
  background-image:
174
263
  radial-gradient(circle at 18% 24%, rgba(225, 240, 250, 0.34) 0 1px, transparent 1.4px),
@@ -184,22 +273,8 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
184
273
  radial-gradient(circle at 72% 62%, rgba(225, 240, 250, 0.22) 0 1px, transparent 1.3px),
185
274
  radial-gradient(circle at 42% 78%, rgba(225, 240, 250, 0.18) 0 0.8px, transparent 1.2px);
186
275
  }
187
- #map.tileflow-globe::before {
188
- content: "";
189
- position: absolute;
190
- z-index: 0;
191
- left: 50%;
192
- top: 50%;
193
- width: calc(var(--tileflow-globe-radius) * 2);
194
- height: calc(var(--tileflow-globe-radius) * 2);
195
- border: 1px solid var(--tileflow-rim-line, rgba(248, 252, 255, 0.88));
196
- border-radius: 50%;
197
- box-shadow:
198
- 0 0 9px 3px var(--tileflow-rim-core, rgba(248, 252, 255, 0.9)),
199
- 0 0 28px 10px var(--tileflow-rim-glow, rgba(176, 220, 246, 0.52)),
200
- 0 0 58px 20px var(--tileflow-rim-haze, rgba(132, 195, 232, 0.2));
201
- pointer-events: none;
202
- transform: translate(-50%, -50%);
276
+ #map.tileflow-globe .maplibregl-canvas {
277
+ filter: var(--tileflow-globe-rim, none);
203
278
  }
204
279
  #map.tileflow-globe > .maplibregl-canvas-container {
205
280
  position: relative;
@@ -233,6 +308,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
233
308
  padding: 12px;
234
309
  white-space: pre-wrap;
235
310
  }
311
+ html[data-devtools-embedded=true] .status { display:none !important; }
236
312
  .maplibregl-ctrl-group .tileflow-3d-toggle,
237
313
  .maplibregl-ctrl-group .tileflow-tree-toggle,
238
314
  .maplibregl-ctrl-group .tileflow-theme-toggle {
@@ -253,6 +329,12 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
253
329
  .maplibregl-ctrl-group .tileflow-theme-toggle:hover {
254
330
  background-color: #F1F3F4;
255
331
  }
332
+ .maplibregl-ctrl-group { background:#faf6eb; border:1px solid #e0d5c4; border-radius:3px; box-shadow:0 3px 12px #30251f18; }
333
+ .maplibregl-ctrl-group button { color:#30251f; }
334
+ .maplibregl-ctrl-group button + button { border-top-color:#e0d5c4; }
335
+ .maplibregl-ctrl-group .tileflow-3d-toggle,.maplibregl-ctrl-group .tileflow-tree-toggle,.maplibregl-ctrl-group .tileflow-theme-toggle { color:#30251f; font-weight:500; }
336
+ .maplibregl-ctrl-group .tileflow-3d-toggle[aria-pressed="true"],.maplibregl-ctrl-group .tileflow-tree-toggle[aria-pressed="true"] { background:#f0dfe0; color:#8c2e3f; }
337
+ .maplibregl-ctrl-group .tileflow-3d-toggle:hover,.maplibregl-ctrl-group .tileflow-tree-toggle:hover,.maplibregl-ctrl-group .tileflow-theme-toggle:hover { background:#eee4d3; }
256
338
  </style>
257
339
  </head>
258
340
  <body>
@@ -262,19 +344,25 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
262
344
  <script type="importmap">{"imports":{"fflate":"${basePath}/__runtime/fflate.js","three":"${basePath}/__runtime/three.module.js"}}</script>
263
345
  <script type="module">
264
346
  import * as maplibregl from "${basePath}/__runtime/maplibre-gl.mjs";
347
+ import {createPreviewObserver} from "${basePath}/__runtime/devtools-observer.js";
265
348
  import {loadTileflowStyleFonts} from "${basePath}/__runtime/tileflow-browser.js";
266
349
  import {registerTileflowContourProtocol} from "${basePath}/__runtime/tileflow-browser.js";
267
350
  import {registerTileflowPmtilesProtocol} from "${basePath}/__runtime/tileflow-browser.js";
268
- import {createTileflowBuildings3D, createTileflowStreetLights, createTileflowTrees3D} from "${basePath}/__runtime/tileflow-browser.js";
351
+ import {createTileflowBuildings3D, createTileflowStreetLights, createTileflowVegetation} from "${basePath}/__runtime/tileflow-browser.js";
269
352
  import {readTileflowSunPreset, resolveTileflowSun, tileflowDefaultSunPreset} from "${basePath}/__runtime/tileflow-browser.js";
270
353
 
271
354
  maplibregl.setWorkerUrl("${basePath}/__runtime/maplibre-gl-worker.mjs");
272
355
 
356
+ const devtoolsObserver = window.parent !== window ? createPreviewObserver() : undefined;
357
+ globalThis.__tileflowDevtoolsObserver = devtoolsObserver;
358
+ if (window.parent !== window) document.documentElement.dataset.devtoolsEmbedded = "true";
359
+
273
360
  const initialStatus = ${JSON.stringify(initialStatus)};
274
361
  const normalizeTileflowLandmarkManifest = ${normalizeTileflowLandmarkManifest.toString()};
275
362
  const readBoundedTileflowResponse = ${readBoundedTileflowResponse.toString()};
276
363
  const readBoundedTileflowJsonResponse = ${readBoundedTileflowJsonResponse.toString()};
277
364
  const initialGeneration = initialStatus.generation;
365
+ document.documentElement.dataset.tileflowArtifactGeneration = String(initialStatus.lastGoodGeneration ?? initialGeneration);
278
366
  const badge = document.getElementById("badge");
279
367
  const status = document.getElementById("status");
280
368
  const previewLabel = ${JSON.stringify(preview?.label)};
@@ -318,8 +406,8 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
318
406
  if (values[0] === "off") return false;
319
407
  return fallback;
320
408
  }
321
- let threeDimensionalEnabled = readToggleFromUrl("buildings3d", false);
322
- let treesEnabled = readToggleFromUrl("trees3d", false);
409
+ let threeDimensionalEnabled = readToggleFromUrl("buildings3d", true);
410
+ let treesEnabled = readToggleFromUrl("trees3d", true);
323
411
  const landmarkState = {
324
412
  active: [],
325
413
  cacheLimit: undefined,
@@ -940,14 +1028,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
940
1028
  return;
941
1029
  }
942
1030
 
943
- // A Web Mercator world is 512 * 2^zoom pixels wide. Dividing its
944
- // circumference by 2\u03C0 closely tracks MapLibre's on-screen globe radius.
945
- const radius = Math.min(
946
- 4096,
947
- ((512 * Math.pow(2, map.getZoom())) / (2 * Math.PI)) * 0.91
948
- );
949
1031
  container.classList.add("tileflow-globe");
950
- container.style.setProperty("--tileflow-globe-radius", radius.toFixed(2) + "px");
951
1032
  applyAtmosphereBackdrop(map, container);
952
1033
  }
953
1034
 
@@ -961,12 +1042,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
961
1042
  const value = Number.parseInt(match[1], 16);
962
1043
  return "rgba(" + (value >> 16) + ", " + ((value >> 8) & 255) + ", " + (value & 255) + ", " + alpha + ")";
963
1044
  };
1045
+ // Use the rendered canvas alpha, as the shared browser atmosphere does.
1046
+ // A zoom-only circle cannot track the globe's latitude, viewport or pitch.
1047
+ const opacity = Math.max(0, Math.min(1, (6 - map.getZoom()) / 3));
1048
+ const core = rgba(atmosphere?.horizonColor ?? "#F8FCFF", opacity * 0.65);
1049
+ const glow = rgba(atmosphere?.horizonColor ?? "#B0DCF6", opacity * 0.4);
964
1050
  const properties = {
965
1051
  "--tileflow-space": rgba(atmosphere?.spaceColor, 1),
966
- "--tileflow-rim-line": rgba(atmosphere?.horizonColor, 0.88),
967
- "--tileflow-rim-core": rgba(atmosphere?.horizonColor, 0.9),
968
- "--tileflow-rim-glow": rgba(atmosphere?.horizonColor, 0.52),
969
- "--tileflow-rim-haze": rgba(atmosphere?.skyColor, 0.35),
1052
+ "--tileflow-globe-rim": opacity > 0
1053
+ ? "drop-shadow(0 0 2px " + core + ") drop-shadow(0 0 12px " + glow + ")"
1054
+ : "none",
970
1055
  };
971
1056
  for (const [name, value] of Object.entries(properties)) {
972
1057
  if (value) container.style.setProperty(name, value);
@@ -1921,6 +2006,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
1921
2006
  (layer) =>
1922
2007
  (layer.id.startsWith("tileflow-label-") &&
1923
2008
  !layer.id.startsWith("tileflow-label-road-")) ||
2009
+ layer.id === "tileflow-poi" ||
1924
2010
  layer.id.startsWith("tileflow-poi-")
1925
2011
  )?.id;
1926
2012
  const treeLayer = map.getLayer("tileflow-vegetation-trees")
@@ -1930,48 +2016,82 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
1930
2016
  }
1931
2017
 
1932
2018
  // Core's 3D layers, as an application mounts them: the map's style describes how its
1933
- // buildings, trees and street lights look, and the controls only show and hide them. With
1934
- // both controls off the preview draws what Capture draws. The preview shows one theme at a
2019
+ // buildings, trees and street lights look, and the controls only show and hide them. The
2020
+ // vegetation control hides all trees when off. The preview shows one theme at a
1935
2021
  // time, so the style on screen holds the sun and the design.
1936
- const coreScene = {buildings: undefined, trees: undefined, lights: undefined, buildingsShown: false, treesShown: false};
2022
+ const coreScene = {buildings: undefined, trees: undefined, lights: undefined, buildingsShown: undefined, animation: "0"};
2023
+ const runtimeLayerVisibility = {};
1937
2024
  globalThis.__tileflowPreviewScene = coreScene;
2025
+ globalThis.__tileflowRuntimeVisibility = (id, visible) => {
2026
+ if (!['tileflow-buildings-3d','tileflow-trees-3d','tileflow-street-lights'].includes(id)) return undefined;
2027
+ if (typeof visible === 'boolean') { runtimeLayerVisibility[id]=visible; mountCoreScene(map); map.triggerRepaint(); }
2028
+ return runtimeLayerVisibility[id] !== false;
2029
+ };
2030
+ // Explicit observation capability: controllers stay private to the preview and expose
2031
+ // only their public state/statistics to the parent inspector.
2032
+ globalThis.__tileflowInspectModels = () => [...(coreScene.buildings?.inspectModels?.() || []), ...(coreScene.trees?.inspectModels?.() || [])];
2033
+ globalThis.__tileflowDescribeRuntimeLayers = () => ({
2034
+ ...(coreScene.buildings ? {"tileflow-buildings-3d": {controller:"Buildings 3D", state:{visible:runtimeLayerVisibility["tileflow-buildings-3d"] !== false, shown:coreScene.buildingsShown, scale:coreScene.buildings.scale}, stats:coreScene.buildings.stats}} : {}),
2035
+ ...(coreScene.trees ? {"tileflow-trees-3d": {controller:"Vegetation", state:{visible:runtimeLayerVisibility["tileflow-trees-3d"] !== false, representation:coreScene.trees.representation}, stats:coreScene.trees.stats}} : {}),
2036
+ ...(coreScene.lights ? {"tileflow-street-lights": {controller:"Street lights", state:{visible:runtimeLayerVisibility["tileflow-street-lights"] !== false, mounted:true}, stats:coreScene.lights.stats}} : {})
2037
+ });
2038
+ const createPreviewSceneThemes = ${createPreviewSceneThemes.toString()};
2039
+ const coreSceneThemeControllers = new WeakMap();
1938
2040
  function coreSceneThemes(map) {
1939
- return {
1940
- getSun() {
1941
- const preset = readTileflowSunPreset(map.getStyle()?.metadata);
1942
- return resolveTileflowSun(preset ?? tileflowDefaultSunPreset);
1943
- },
1944
- getThemeMetadata(key) {
1945
- return {values: [map.getStyle()?.metadata?.[key]], position: 0};
1946
- }
1947
- };
2041
+ let controller = coreSceneThemeControllers.get(map);
2042
+ if (!controller) {
2043
+ controller = createPreviewSceneThemes(map, metadata =>
2044
+ resolveTileflowSun(readTileflowSunPreset(metadata) ?? tileflowDefaultSunPreset));
2045
+ coreSceneThemeControllers.set(map, controller);
2046
+ }
2047
+ return controller;
1948
2048
  }
2049
+ let mountingCoreScene = false;
1949
2050
  function mountCoreScene(map) {
1950
- const metadata = map.getStyle()?.metadata ?? {};
1951
- const controller = coreSceneThemes(map);
1952
- if (!coreScene.buildings && metadata["tileflow:buildings"]) {
1953
- coreScene.buildings = createTileflowBuildings3D({map, controller});
1954
- }
1955
- if (!coreScene.trees && metadata["tileflow:vegetation"]) {
1956
- coreScene.trees = createTileflowTrees3D({map, controller});
1957
- }
1958
- // Street lights have no capture fallback, so they shine only with the 3D control on.
1959
- if (!coreScene.lights && threeDimensionalEnabled && metadata["tileflow:street-lights"]) {
1960
- coreScene.lights = createTileflowStreetLights({map, controller});
1961
- } else if (coreScene.lights && !threeDimensionalEnabled) {
1962
- coreScene.lights.dispose();
1963
- coreScene.lights = undefined;
1964
- }
1965
- const instant = {transition: {duration: 0}};
1966
- // Only a change of the controls moves the layers, so a style event does not restart them.
1967
- if (coreScene.buildings && coreScene.buildingsShown !== threeDimensionalEnabled) {
1968
- coreScene.buildingsShown = threeDimensionalEnabled;
1969
- void (threeDimensionalEnabled ? coreScene.buildings.show() : coreScene.buildings.hide(instant));
1970
- }
1971
- if (coreScene.trees && coreScene.treesShown !== treesEnabled) {
1972
- coreScene.treesShown = treesEnabled;
1973
- void (treesEnabled ? coreScene.trees.show() : coreScene.trees.hide(instant));
1974
- }
2051
+ if (mountingCoreScene) return;
2052
+ mountingCoreScene = true;
2053
+ try {
2054
+ const metadata = map.getStyle()?.metadata ?? {};
2055
+ const controller = coreSceneThemes(map);
2056
+ if (!coreScene.buildings && metadata["tileflow:buildings"]) {
2057
+ coreScene.buildings = createTileflowBuildings3D({map, controller});
2058
+ }
2059
+ if (!coreScene.trees) {
2060
+ if (!previewSearchParameters.has("trees3d")) {
2061
+ treesEnabled = metadata["tileflow:vegetation-display"]?.visible ?? true;
2062
+ }
2063
+ coreScene.trees = createTileflowVegetation({map, controller, visible: treesEnabled,
2064
+ viewMode: threeDimensionalEnabled ? "3d" : "2d", transformRequest: previewTransformRequest});
2065
+ }
2066
+ coreScene.trees.setVisible(treesEnabled && runtimeLayerVisibility["tileflow-trees-3d"] !== false);
2067
+ coreScene.trees.setViewMode(threeDimensionalEnabled ? "3d" : "2d");
2068
+ // Street lights require an explicit map opt-in and the 3D control; defaults stay off.
2069
+ if (!coreScene.lights && threeDimensionalEnabled && metadata["tileflow:street-lights"]) {
2070
+ coreScene.lights = createTileflowStreetLights({map, controller});
2071
+ } else if (coreScene.lights && (!threeDimensionalEnabled || !metadata["tileflow:street-lights"])) {
2072
+ coreScene.lights.dispose();
2073
+ coreScene.lights = undefined;
2074
+ }
2075
+ const lightLayer = map.getLayer("tileflow-street-lights");
2076
+ const lightRenderer = lightLayer?.implementation || lightLayer;
2077
+ if (lightRenderer?.render && !lightRenderer.__previewVisibility) {
2078
+ const render = lightRenderer.render;
2079
+ lightRenderer.render = function(...args) {
2080
+ if (runtimeLayerVisibility["tileflow-street-lights"] !== false) return render.apply(this,args);
2081
+ };
2082
+ lightRenderer.__previewVisibility = true;
2083
+ }
2084
+ const showBuildings = threeDimensionalEnabled && runtimeLayerVisibility["tileflow-buildings-3d"] !== false;
2085
+ const instant = {transition: {duration: 0}};
2086
+ const animation = previewSearchParameters.get('animation') || '0';
2087
+ const duration = animation === 'fade' ? 800 : Math.min(3000, Math.max(0, Number(animation) || 0));
2088
+ // Only a change of the controls moves the layers, so a style event does not restart them.
2089
+ if (coreScene.buildings && (coreScene.buildingsShown !== showBuildings || coreScene.animation !== animation)) {
2090
+ coreScene.buildingsShown = showBuildings;
2091
+ coreScene.animation = animation;
2092
+ void (showBuildings ? coreScene.buildings.show({transition: {duration, mode: animation === "fade" ? "fade" : "grow"}}) : coreScene.buildings.hide(instant));
2093
+ }
2094
+ } finally {mountingCoreScene = false;}
1975
2095
  }
1976
2096
 
1977
2097
  class ThreeDimensionalControl {
@@ -2076,18 +2196,11 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2076
2196
  }
2077
2197
 
2078
2198
  update() {
2079
- const treeLayer = this.map.getStyle()?.layers?.find(
2080
- (layer) => layer.id === "tileflow-vegetation-trees"
2081
- );
2082
- // Core's 3D trees hide the style's flat trees while they stand (mountCoreScene).
2083
- if (treeLayer && !this.map.getStyle()?.metadata?.["tileflow:vegetation"]) {
2084
- if (this.map.getLayoutProperty(treeLayer.id, "visibility") !== "visible") {
2085
- this.map.setLayoutProperty(treeLayer.id, "visibility", "visible");
2086
- }
2087
- }
2199
+ if (this.map.isStyleLoaded()) mountCoreScene(this.map);
2200
+ this.enabled = treesEnabled;
2088
2201
  this.button.textContent = this.enabled ? "TREES ON" : "TREES OFF";
2089
2202
  this.button.setAttribute("aria-pressed", String(this.enabled));
2090
- this.button.title = this.enabled ? "Show flat trees" : "Show 3D trees";
2203
+ this.button.title = this.enabled ? "Hide vegetation" : "Show vegetation";
2091
2204
  }
2092
2205
 
2093
2206
  onRemove() {
@@ -2183,24 +2296,21 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2183
2296
  ...visibleLayerGroups,
2184
2297
  ].join(",");
2185
2298
  }
2299
+ const vegetationLayers = map.getStyle()?.metadata?.["tileflow:vegetation-display"]?.flatLayers;
2186
2300
  for (const layer of map.getStyle()?.layers || []) {
2187
- if (!defaultLayerVisibility.has(layer.id)) {
2188
- defaultLayerVisibility.set(
2189
- layer.id,
2190
- map.getLayoutProperty(layer.id, "visibility") || "visible"
2191
- );
2192
- }
2193
2301
  if (layer.type === "symbol" && layer.layout?.["text-field"] !== undefined) {
2194
- if (!defaultLayerTextField.has(layer.id)) {
2302
+ const hideLabels = !visibleLayerGroups.has("labels");
2303
+ if (hideLabels && !defaultLayerTextField.has(layer.id)) {
2195
2304
  defaultLayerTextField.set(
2196
2305
  layer.id,
2197
2306
  map.getLayoutProperty(layer.id, "text-field")
2198
2307
  );
2199
2308
  }
2200
- const textField = visibleLayerGroups.has("labels")
2201
- ? defaultLayerTextField.get(layer.id)
2202
- : "";
2309
+ const restoreText = !hideLabels && defaultLayerTextField.has(layer.id);
2310
+ const textField = hideLabels ? "" : defaultLayerTextField.get(layer.id);
2311
+ if (restoreText) defaultLayerTextField.delete(layer.id);
2203
2312
  if (
2313
+ (hideLabels || restoreText) &&
2204
2314
  !previewLayoutValuesEqual(
2205
2315
  map.getLayoutProperty(layer.id, "text-field"),
2206
2316
  textField
@@ -2209,21 +2319,29 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2209
2319
  map.setLayoutProperty(layer.id, "text-field", textField);
2210
2320
  }
2211
2321
  }
2322
+ // The shared vegetation controller owns every flat representation.
2323
+ if (vegetationLayers?.some((entry) => entry.id === layer.id.split("::")[0])) continue;
2212
2324
  const groups = previewLayerGroups(layer);
2325
+ const hideLayer = groups.some((group) => !visibleLayerGroups.has(group));
2326
+ // Group controls only own the properties they mask. Ordinary style events must
2327
+ // preserve temporary DevTools edits, including visibility and label text.
2328
+ if (hideLayer && !defaultLayerVisibility.has(layer.id)) {
2329
+ defaultLayerVisibility.set(layer.id, map.getLayoutProperty(layer.id, "visibility") || "visible");
2330
+ }
2331
+ if (!hideLayer && !defaultLayerVisibility.has(layer.id)) continue;
2213
2332
  const threeDimensionalToggle = layer.metadata?.["tileflow:3d-toggle"];
2214
2333
  const authoredVisibility = defaultLayerVisibility.get(layer.id);
2334
+ if (!hideLayer) defaultLayerVisibility.delete(layer.id);
2215
2335
  const styleMetadata = map.getStyle()?.metadata ?? {};
2216
2336
  // Core's 3D buildings and trees stand in for compiled extrusions and flat trees.
2217
- const flatTrees = styleMetadata["tileflow:vegetation"]?.flatLayers ?? [];
2218
2337
  const replacedByCore =
2219
- (threeDimensionalToggle === "building" && styleMetadata["tileflow:buildings"]) ||
2220
- (treesEnabled && flatTrees.includes(layer.id.split("::")[0]));
2338
+ threeDimensionalToggle === "building" && styleMetadata["tileflow:buildings"];
2221
2339
  const toggleVisibility = replacedByCore
2222
2340
  ? "none"
2223
2341
  : threeDimensionalToggle === "building" || threeDimensionalToggle === "landmark"
2224
2342
  ? (threeDimensionalEnabled ? "visible" : "none")
2225
2343
  : authoredVisibility;
2226
- const visibility = groups.some((group) => !visibleLayerGroups.has(group))
2344
+ const visibility = hideLayer
2227
2345
  ? "none"
2228
2346
  : toggleVisibility;
2229
2347
  if (map.getLayoutProperty(layer.id, "visibility") !== visibility) {
@@ -2238,6 +2356,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2238
2356
  map.fire("tileflow:visible-layer-groups", {groups: [...visibleLayerGroups]});
2239
2357
  }
2240
2358
 
2359
+ function previewTransformRequest(url) {
2360
+ try {
2361
+ return new URL(url, location.href).hostname === "dev-tiles.tileflow.dev"
2362
+ ? {credentials: "include", url}
2363
+ : {url};
2364
+ } catch {
2365
+ return {url};
2366
+ }
2367
+ }
2368
+
2241
2369
  if (styleUrl) {
2242
2370
  // Dense vector tiles finish in bursts with multiple workers, causing several
2243
2371
  // bucket uploads in one frame. One worker keeps navigation smooth; benchmarks
@@ -2248,23 +2376,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2248
2376
  registerTileflowContourProtocol({addProtocol: maplibregl.addProtocol});
2249
2377
  registerTileflowPmtilesProtocol({addProtocol: maplibregl.addProtocol});
2250
2378
  const map = new maplibregl.Map({
2251
- collectResourceTiming: mapBenchmarkEnabled,
2379
+ collectResourceTiming: mapBenchmarkEnabled || Boolean(devtoolsObserver),
2252
2380
  container: "map",
2253
2381
  style: styleUrl,
2254
- transformRequest: (url) => {
2255
- try {
2256
- return new URL(url, location.href).hostname === "dev-tiles.tileflow.dev"
2257
- ? {credentials: "include", url}
2258
- : {url};
2259
- } catch {
2260
- return {url};
2261
- }
2262
- },
2382
+ transformRequest: previewTransformRequest,
2263
2383
  ...resolveInitialMapOptions(previewMapOptions)
2264
2384
  });
2265
2385
  globalThis.__tileflowPreviewMap = map;
2386
+ devtoolsObserver?.attach(map);
2266
2387
  installMapBenchmark(map);
2267
- map.addControl(new maplibregl.NavigationControl(), "top-right");
2388
+ if (window.parent === window) map.addControl(new maplibregl.NavigationControl(), "top-right");
2268
2389
  let threeDimensionalControl;
2269
2390
  let treeControl;
2270
2391
  if (isSemanticPreview) {
@@ -2272,8 +2393,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2272
2393
  treeControl = new TreeControl();
2273
2394
  map.addControl(threeDimensionalControl, "top-right");
2274
2395
  map.addControl(treeControl, "top-right");
2396
+ // Embedded Map DevTools owns the common Controls sidebar. Keep the controls mounted
2397
+ // for their renderer lifecycle and message bridge, but remove duplicate buttons.
2398
+ if (window.parent !== window) {
2399
+ threeDimensionalControl.container.hidden = true;
2400
+ treeControl.container.hidden = true;
2401
+ }
2275
2402
  }
2276
- if (previewThemes.length > 1) map.addControl(new ThemeControl(), "top-right");
2403
+ // Map DevTools has a shared theme selector in its Controls panel; keep the map-level
2404
+ // cycling button for standalone previews only.
2405
+ if (previewThemes.length > 1 && window.parent === window) map.addControl(new ThemeControl(), "top-right");
2277
2406
  let ensuringThreeDimensionalLayers;
2278
2407
  const ensureThreeDimensionalLayers = () => {
2279
2408
  if (ensuringThreeDimensionalLayers) return ensuringThreeDimensionalLayers;
@@ -2360,13 +2489,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2360
2489
  map.on("tileflow:3d-toggle", ensureThreeDimensionalLayers);
2361
2490
  map.on("tileflow:trees-toggle", ensureThreeDimensionalLayers);
2362
2491
  map.on("load", () => {
2492
+ mountCoreScene(map);
2493
+ treeControl?.update();
2363
2494
  writeCameraToUrl(map);
2364
2495
  updateGlobeBackdrop(map);
2365
2496
  ensureThreeDimensionalLayers();
2366
- mountCoreScene(map);
2367
2497
  });
2368
2498
  map.on("move", () => updateGlobeBackdrop(map));
2499
+ map.on("style.load", () => updateGlobeBackdrop(map));
2369
2500
  map.on("moveend", () => writeCameraToUrl(map));
2501
+ ${previewSceneClient}
2370
2502
  addEventListener("message", (event) => {
2371
2503
  if (
2372
2504
  window.parent === window ||
@@ -2437,6 +2569,15 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2437
2569
  }
2438
2570
 
2439
2571
  applyStatus(initialStatus);
2572
+ // Embedded previews share their host's stream. Two streams per tab can exhaust Chrome's
2573
+ // six HTTP/1 connections and leave even local module/font requests waiting indefinitely.
2574
+ let unsubscribePreviewStatus;
2575
+ try {
2576
+ if (window.parent !== window) unsubscribePreviewStatus = window.parent.__tileflowDevtoolsHost?.subscribePreviewStatus?.(window, applyStatus);
2577
+ } catch { /* Other embedders keep the standalone stream. */ }
2578
+ if (unsubscribePreviewStatus) {
2579
+ addEventListener("beforeunload", unsubscribePreviewStatus);
2580
+ } else {
2440
2581
  const events = new EventSource(${JSON.stringify(`${basePath}/__events`)});
2441
2582
  // Generations are monotonic only within one dev-server process. A source-code watcher can
2442
2583
  // restart that process at generation 1, so reconnecting after a live connection was lost
@@ -2457,6 +2598,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2457
2598
  for (const eventName of ["building", "ready", "invalid"]) {
2458
2599
  events.addEventListener(eventName, (event) => applyStatus(JSON.parse(event.data)));
2459
2600
  }
2601
+ }
2460
2602
  </script>
2461
2603
  </body>
2462
2604
  </html>`;
package/dist/server.d.ts CHANGED
@@ -5,7 +5,7 @@ import '@tileflow/core';
5
5
  import '@tileflow/core/build';
6
6
  import '@tileflow/core/manifest';
7
7
  import '@tileflow/core/native-profile';
8
- import './icons.js';
8
+ import './icons-BdOehQmB.js';
9
9
  import './config.js';
10
10
 
11
11
  declare const tileflowComparisonSchemaVersion: 1;
@@ -23,16 +23,21 @@ type TileflowComparisonSide = {
23
23
  /** Same-origin preview URL below basePath, including its map/theme query. */
24
24
  previewUrl: string;
25
25
  };
26
- type TileflowComparisonMode = 'blink' | 'overlay' | 'side-by-side' | 'split';
26
+ type TileflowComparisonMode = 'single' | 'blink' | 'overlay' | 'side-by-side' | 'split';
27
27
  type TileflowComparisonOptions = {
28
28
  basePath?: string;
29
29
  initialMode?: TileflowComparisonMode;
30
30
  left: TileflowComparisonSide;
31
- right: TileflowComparisonSide;
31
+ right?: TileflowComparisonSide;
32
32
  title?: string;
33
+ /** Optional local workflow adapter supplied by the CLI or an embedding server. */
34
+ workbench?: {
35
+ url: string;
36
+ handle: TileflowComparisonRequestHandler;
37
+ };
33
38
  };
34
39
  /**
35
- * Compose two existing Tileflow dev handlers behind one same-origin comparison shell.
40
+ * Compose one or two existing Tileflow dev handlers behind the same-origin Map DevTools.
36
41
  *
37
42
  * The side handlers retain ownership of styles, sprites, fonts, generations, live
38
43
  * events, and last-known-good behavior. This router owns only the shell document and
@@ -46,6 +51,8 @@ type TileflowDevRequestHandlerOptions = {
46
51
  config?: string;
47
52
  cwd?: string;
48
53
  inspection?: boolean;
54
+ /** Fit scene previews to their pane; capture retains the declared scene frame. */
55
+ fitViewport?: boolean;
49
56
  map?: string;
50
57
  onError?: (error: unknown) => void;
51
58
  scene?: string;