@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/dist/preview.js CHANGED
@@ -140,11 +140,67 @@ 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
+
143
199
  // 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;
200
+ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticPreview, fontFaces = [], fitViewport = false, controls) {
201
+ const styleUrl = preview ? `${basePath}/styles/${preview.mapName}/${preview.themeName}.json${controls ? `?${new URLSearchParams({ controls })}` : ""}` : void 0;
146
202
  const mapOptions = preview ? previewMapOptions(preview) : void 0;
147
- const viewportCss = preview?.viewport ? `
203
+ const viewportCss = preview?.viewport && !fitViewport ? `
148
204
  html, body { min-height: 100%; margin: 0; }
149
205
  body {
150
206
  display: grid;
@@ -168,7 +224,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
168
224
  ${viewportCss}
169
225
  body { font-family: ui-sans-serif, system-ui, sans-serif; }
170
226
  #map {
171
- --tileflow-globe-radius: 220px;
172
227
  background-color: var(--tileflow-space, #2F5070);
173
228
  background-image:
174
229
  radial-gradient(circle at 18% 24%, rgba(225, 240, 250, 0.34) 0 1px, transparent 1.4px),
@@ -184,22 +239,8 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
184
239
  radial-gradient(circle at 72% 62%, rgba(225, 240, 250, 0.22) 0 1px, transparent 1.3px),
185
240
  radial-gradient(circle at 42% 78%, rgba(225, 240, 250, 0.18) 0 0.8px, transparent 1.2px);
186
241
  }
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%);
242
+ #map.tileflow-globe .maplibregl-canvas {
243
+ filter: var(--tileflow-globe-rim, none);
203
244
  }
204
245
  #map.tileflow-globe > .maplibregl-canvas-container {
205
246
  position: relative;
@@ -233,6 +274,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
233
274
  padding: 12px;
234
275
  white-space: pre-wrap;
235
276
  }
277
+ html[data-devtools-embedded=true] .status { display:none !important; }
236
278
  .maplibregl-ctrl-group .tileflow-3d-toggle,
237
279
  .maplibregl-ctrl-group .tileflow-tree-toggle,
238
280
  .maplibregl-ctrl-group .tileflow-theme-toggle {
@@ -253,6 +295,12 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
253
295
  .maplibregl-ctrl-group .tileflow-theme-toggle:hover {
254
296
  background-color: #F1F3F4;
255
297
  }
298
+ .maplibregl-ctrl-group { background:#faf6eb; border:1px solid #e0d5c4; border-radius:3px; box-shadow:0 3px 12px #30251f18; }
299
+ .maplibregl-ctrl-group button { color:#30251f; }
300
+ .maplibregl-ctrl-group button + button { border-top-color:#e0d5c4; }
301
+ .maplibregl-ctrl-group .tileflow-3d-toggle,.maplibregl-ctrl-group .tileflow-tree-toggle,.maplibregl-ctrl-group .tileflow-theme-toggle { color:#30251f; font-weight:500; }
302
+ .maplibregl-ctrl-group .tileflow-3d-toggle[aria-pressed="true"],.maplibregl-ctrl-group .tileflow-tree-toggle[aria-pressed="true"] { background:#f0dfe0; color:#8c2e3f; }
303
+ .maplibregl-ctrl-group .tileflow-3d-toggle:hover,.maplibregl-ctrl-group .tileflow-tree-toggle:hover,.maplibregl-ctrl-group .tileflow-theme-toggle:hover { background:#eee4d3; }
256
304
  </style>
257
305
  </head>
258
306
  <body>
@@ -262,19 +310,25 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
262
310
  <script type="importmap">{"imports":{"fflate":"${basePath}/__runtime/fflate.js","three":"${basePath}/__runtime/three.module.js"}}</script>
263
311
  <script type="module">
264
312
  import * as maplibregl from "${basePath}/__runtime/maplibre-gl.mjs";
313
+ import {createPreviewObserver} from "${basePath}/__runtime/devtools-observer.js";
265
314
  import {loadTileflowStyleFonts} from "${basePath}/__runtime/tileflow-browser.js";
266
315
  import {registerTileflowContourProtocol} from "${basePath}/__runtime/tileflow-browser.js";
267
316
  import {registerTileflowPmtilesProtocol} from "${basePath}/__runtime/tileflow-browser.js";
268
- import {createTileflowBuildings3D, createTileflowStreetLights, createTileflowTrees3D} from "${basePath}/__runtime/tileflow-browser.js";
317
+ import {createTileflowBuildings3D, createTileflowStreetLights, createTileflowVegetation} from "${basePath}/__runtime/tileflow-browser.js";
269
318
  import {readTileflowSunPreset, resolveTileflowSun, tileflowDefaultSunPreset} from "${basePath}/__runtime/tileflow-browser.js";
270
319
 
271
320
  maplibregl.setWorkerUrl("${basePath}/__runtime/maplibre-gl-worker.mjs");
272
321
 
322
+ const devtoolsObserver = window.parent !== window ? createPreviewObserver() : undefined;
323
+ globalThis.__tileflowDevtoolsObserver = devtoolsObserver;
324
+ if (window.parent !== window) document.documentElement.dataset.devtoolsEmbedded = "true";
325
+
273
326
  const initialStatus = ${JSON.stringify(initialStatus)};
274
327
  const normalizeTileflowLandmarkManifest = ${normalizeTileflowLandmarkManifest.toString()};
275
328
  const readBoundedTileflowResponse = ${readBoundedTileflowResponse.toString()};
276
329
  const readBoundedTileflowJsonResponse = ${readBoundedTileflowJsonResponse.toString()};
277
330
  const initialGeneration = initialStatus.generation;
331
+ document.documentElement.dataset.tileflowArtifactGeneration = String(initialStatus.lastGoodGeneration ?? initialGeneration);
278
332
  const badge = document.getElementById("badge");
279
333
  const status = document.getElementById("status");
280
334
  const previewLabel = ${JSON.stringify(preview?.label)};
@@ -319,7 +373,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
319
373
  return fallback;
320
374
  }
321
375
  let threeDimensionalEnabled = readToggleFromUrl("buildings3d", false);
322
- let treesEnabled = readToggleFromUrl("trees3d", false);
376
+ let treesEnabled = readToggleFromUrl("trees3d", true);
323
377
  const landmarkState = {
324
378
  active: [],
325
379
  cacheLimit: undefined,
@@ -940,14 +994,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
940
994
  return;
941
995
  }
942
996
 
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
997
  container.classList.add("tileflow-globe");
950
- container.style.setProperty("--tileflow-globe-radius", radius.toFixed(2) + "px");
951
998
  applyAtmosphereBackdrop(map, container);
952
999
  }
953
1000
 
@@ -961,12 +1008,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
961
1008
  const value = Number.parseInt(match[1], 16);
962
1009
  return "rgba(" + (value >> 16) + ", " + ((value >> 8) & 255) + ", " + (value & 255) + ", " + alpha + ")";
963
1010
  };
1011
+ // Use the rendered canvas alpha, as the shared browser atmosphere does.
1012
+ // A zoom-only circle cannot track the globe's latitude, viewport or pitch.
1013
+ const opacity = Math.max(0, Math.min(1, (6 - map.getZoom()) / 3));
1014
+ const core = rgba(atmosphere?.horizonColor ?? "#F8FCFF", opacity * 0.65);
1015
+ const glow = rgba(atmosphere?.horizonColor ?? "#B0DCF6", opacity * 0.4);
964
1016
  const properties = {
965
1017
  "--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),
1018
+ "--tileflow-globe-rim": opacity > 0
1019
+ ? "drop-shadow(0 0 2px " + core + ") drop-shadow(0 0 12px " + glow + ")"
1020
+ : "none",
970
1021
  };
971
1022
  for (const [name, value] of Object.entries(properties)) {
972
1023
  if (value) container.style.setProperty(name, value);
@@ -1921,6 +1972,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
1921
1972
  (layer) =>
1922
1973
  (layer.id.startsWith("tileflow-label-") &&
1923
1974
  !layer.id.startsWith("tileflow-label-road-")) ||
1975
+ layer.id === "tileflow-poi" ||
1924
1976
  layer.id.startsWith("tileflow-poi-")
1925
1977
  )?.id;
1926
1978
  const treeLayer = map.getLayer("tileflow-vegetation-trees")
@@ -1930,11 +1982,25 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
1930
1982
  }
1931
1983
 
1932
1984
  // 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
1985
+ // buildings, trees and street lights look, and the controls only show and hide them. The
1986
+ // vegetation control hides all trees when off. The preview shows one theme at a
1935
1987
  // 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};
1988
+ const coreScene = {buildings: undefined, trees: undefined, lights: undefined, buildingsShown: false, animation: "0"};
1989
+ const runtimeLayerVisibility = {};
1937
1990
  globalThis.__tileflowPreviewScene = coreScene;
1991
+ globalThis.__tileflowRuntimeVisibility = (id, visible) => {
1992
+ if (!['tileflow-buildings-3d','tileflow-trees-3d','tileflow-street-lights'].includes(id)) return undefined;
1993
+ if (typeof visible === 'boolean') { runtimeLayerVisibility[id]=visible; mountCoreScene(map); map.triggerRepaint(); }
1994
+ return runtimeLayerVisibility[id] !== false;
1995
+ };
1996
+ // Explicit observation capability: controllers stay private to the preview and expose
1997
+ // only their public state/statistics to the parent inspector.
1998
+ globalThis.__tileflowInspectModels = () => [...(coreScene.buildings?.inspectModels?.() || []), ...(coreScene.trees?.inspectModels?.() || [])];
1999
+ globalThis.__tileflowDescribeRuntimeLayers = () => ({
2000
+ ...(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}} : {}),
2001
+ ...(coreScene.trees ? {"tileflow-trees-3d": {controller:"Vegetation", state:{visible:runtimeLayerVisibility["tileflow-trees-3d"] !== false, representation:coreScene.trees.representation}, stats:coreScene.trees.stats}} : {}),
2002
+ ...(coreScene.lights ? {"tileflow-street-lights": {controller:"Street lights", state:{visible:runtimeLayerVisibility["tileflow-street-lights"] !== false, mounted:true}, stats:coreScene.lights.stats}} : {})
2003
+ });
1938
2004
  function coreSceneThemes(map) {
1939
2005
  return {
1940
2006
  getSun() {
@@ -1946,32 +2012,52 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
1946
2012
  }
1947
2013
  };
1948
2014
  }
2015
+ let mountingCoreScene = false;
1949
2016
  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
- }
2017
+ if (mountingCoreScene) return;
2018
+ mountingCoreScene = true;
2019
+ try {
2020
+ const metadata = map.getStyle()?.metadata ?? {};
2021
+ const controller = coreSceneThemes(map);
2022
+ if (!coreScene.buildings && metadata["tileflow:buildings"]) {
2023
+ coreScene.buildings = createTileflowBuildings3D({map, controller});
2024
+ }
2025
+ if (!coreScene.trees) {
2026
+ if (!previewSearchParameters.has("trees3d")) {
2027
+ treesEnabled = metadata["tileflow:vegetation-display"]?.visible ?? true;
2028
+ }
2029
+ coreScene.trees = createTileflowVegetation({map, controller, visible: treesEnabled,
2030
+ viewMode: threeDimensionalEnabled ? "3d" : "2d", transformRequest: previewTransformRequest});
2031
+ }
2032
+ coreScene.trees.setVisible(treesEnabled && runtimeLayerVisibility["tileflow-trees-3d"] !== false);
2033
+ coreScene.trees.setViewMode(threeDimensionalEnabled ? "3d" : "2d");
2034
+ // Street lights have no capture fallback, so they shine only with the 3D control on.
2035
+ if (!coreScene.lights && threeDimensionalEnabled && metadata["tileflow:street-lights"]) {
2036
+ coreScene.lights = createTileflowStreetLights({map, controller});
2037
+ } else if (coreScene.lights && !threeDimensionalEnabled) {
2038
+ coreScene.lights.dispose();
2039
+ coreScene.lights = undefined;
2040
+ }
2041
+ const lightLayer = map.getLayer("tileflow-street-lights");
2042
+ const lightRenderer = lightLayer?.implementation || lightLayer;
2043
+ if (lightRenderer?.render && !lightRenderer.__previewVisibility) {
2044
+ const render = lightRenderer.render;
2045
+ lightRenderer.render = function(...args) {
2046
+ if (runtimeLayerVisibility["tileflow-street-lights"] !== false) return render.apply(this,args);
2047
+ };
2048
+ lightRenderer.__previewVisibility = true;
2049
+ }
2050
+ const showBuildings = threeDimensionalEnabled && runtimeLayerVisibility["tileflow-buildings-3d"] !== false;
2051
+ const instant = {transition: {duration: 0}};
2052
+ const animation = previewSearchParameters.get('animation') || '0';
2053
+ const duration = animation === 'fade' ? 800 : Math.min(3000, Math.max(0, Number(animation) || 0));
2054
+ // Only a change of the controls moves the layers, so a style event does not restart them.
2055
+ if (coreScene.buildings && (coreScene.buildingsShown !== showBuildings || coreScene.animation !== animation)) {
2056
+ coreScene.buildingsShown = showBuildings;
2057
+ coreScene.animation = animation;
2058
+ void (showBuildings ? coreScene.buildings.show({transition: {duration, mode: animation === "fade" ? "fade" : "grow"}}) : coreScene.buildings.hide(instant));
2059
+ }
2060
+ } finally {mountingCoreScene = false;}
1975
2061
  }
1976
2062
 
1977
2063
  class ThreeDimensionalControl {
@@ -2076,18 +2162,11 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2076
2162
  }
2077
2163
 
2078
2164
  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
- }
2165
+ if (this.map.isStyleLoaded()) mountCoreScene(this.map);
2166
+ this.enabled = treesEnabled;
2088
2167
  this.button.textContent = this.enabled ? "TREES ON" : "TREES OFF";
2089
2168
  this.button.setAttribute("aria-pressed", String(this.enabled));
2090
- this.button.title = this.enabled ? "Show flat trees" : "Show 3D trees";
2169
+ this.button.title = this.enabled ? "Hide vegetation" : "Show vegetation";
2091
2170
  }
2092
2171
 
2093
2172
  onRemove() {
@@ -2183,24 +2262,21 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2183
2262
  ...visibleLayerGroups,
2184
2263
  ].join(",");
2185
2264
  }
2265
+ const vegetationLayers = map.getStyle()?.metadata?.["tileflow:vegetation-display"]?.flatLayers;
2186
2266
  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
2267
  if (layer.type === "symbol" && layer.layout?.["text-field"] !== undefined) {
2194
- if (!defaultLayerTextField.has(layer.id)) {
2268
+ const hideLabels = !visibleLayerGroups.has("labels");
2269
+ if (hideLabels && !defaultLayerTextField.has(layer.id)) {
2195
2270
  defaultLayerTextField.set(
2196
2271
  layer.id,
2197
2272
  map.getLayoutProperty(layer.id, "text-field")
2198
2273
  );
2199
2274
  }
2200
- const textField = visibleLayerGroups.has("labels")
2201
- ? defaultLayerTextField.get(layer.id)
2202
- : "";
2275
+ const restoreText = !hideLabels && defaultLayerTextField.has(layer.id);
2276
+ const textField = hideLabels ? "" : defaultLayerTextField.get(layer.id);
2277
+ if (restoreText) defaultLayerTextField.delete(layer.id);
2203
2278
  if (
2279
+ (hideLabels || restoreText) &&
2204
2280
  !previewLayoutValuesEqual(
2205
2281
  map.getLayoutProperty(layer.id, "text-field"),
2206
2282
  textField
@@ -2209,21 +2285,29 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2209
2285
  map.setLayoutProperty(layer.id, "text-field", textField);
2210
2286
  }
2211
2287
  }
2288
+ // The shared vegetation controller owns every flat representation.
2289
+ if (vegetationLayers?.some((entry) => entry.id === layer.id.split("::")[0])) continue;
2212
2290
  const groups = previewLayerGroups(layer);
2291
+ const hideLayer = groups.some((group) => !visibleLayerGroups.has(group));
2292
+ // Group controls only own the properties they mask. Ordinary style events must
2293
+ // preserve temporary DevTools edits, including visibility and label text.
2294
+ if (hideLayer && !defaultLayerVisibility.has(layer.id)) {
2295
+ defaultLayerVisibility.set(layer.id, map.getLayoutProperty(layer.id, "visibility") || "visible");
2296
+ }
2297
+ if (!hideLayer && !defaultLayerVisibility.has(layer.id)) continue;
2213
2298
  const threeDimensionalToggle = layer.metadata?.["tileflow:3d-toggle"];
2214
2299
  const authoredVisibility = defaultLayerVisibility.get(layer.id);
2300
+ if (!hideLayer) defaultLayerVisibility.delete(layer.id);
2215
2301
  const styleMetadata = map.getStyle()?.metadata ?? {};
2216
2302
  // Core's 3D buildings and trees stand in for compiled extrusions and flat trees.
2217
- const flatTrees = styleMetadata["tileflow:vegetation"]?.flatLayers ?? [];
2218
2303
  const replacedByCore =
2219
- (threeDimensionalToggle === "building" && styleMetadata["tileflow:buildings"]) ||
2220
- (treesEnabled && flatTrees.includes(layer.id.split("::")[0]));
2304
+ threeDimensionalToggle === "building" && styleMetadata["tileflow:buildings"];
2221
2305
  const toggleVisibility = replacedByCore
2222
2306
  ? "none"
2223
2307
  : threeDimensionalToggle === "building" || threeDimensionalToggle === "landmark"
2224
2308
  ? (threeDimensionalEnabled ? "visible" : "none")
2225
2309
  : authoredVisibility;
2226
- const visibility = groups.some((group) => !visibleLayerGroups.has(group))
2310
+ const visibility = hideLayer
2227
2311
  ? "none"
2228
2312
  : toggleVisibility;
2229
2313
  if (map.getLayoutProperty(layer.id, "visibility") !== visibility) {
@@ -2238,6 +2322,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2238
2322
  map.fire("tileflow:visible-layer-groups", {groups: [...visibleLayerGroups]});
2239
2323
  }
2240
2324
 
2325
+ function previewTransformRequest(url) {
2326
+ try {
2327
+ return new URL(url, location.href).hostname === "dev-tiles.tileflow.dev"
2328
+ ? {credentials: "include", url}
2329
+ : {url};
2330
+ } catch {
2331
+ return {url};
2332
+ }
2333
+ }
2334
+
2241
2335
  if (styleUrl) {
2242
2336
  // Dense vector tiles finish in bursts with multiple workers, causing several
2243
2337
  // bucket uploads in one frame. One worker keeps navigation smooth; benchmarks
@@ -2248,23 +2342,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2248
2342
  registerTileflowContourProtocol({addProtocol: maplibregl.addProtocol});
2249
2343
  registerTileflowPmtilesProtocol({addProtocol: maplibregl.addProtocol});
2250
2344
  const map = new maplibregl.Map({
2251
- collectResourceTiming: mapBenchmarkEnabled,
2345
+ collectResourceTiming: mapBenchmarkEnabled || Boolean(devtoolsObserver),
2252
2346
  container: "map",
2253
2347
  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
- },
2348
+ transformRequest: previewTransformRequest,
2263
2349
  ...resolveInitialMapOptions(previewMapOptions)
2264
2350
  });
2265
2351
  globalThis.__tileflowPreviewMap = map;
2352
+ devtoolsObserver?.attach(map);
2266
2353
  installMapBenchmark(map);
2267
- map.addControl(new maplibregl.NavigationControl(), "top-right");
2354
+ if (window.parent === window) map.addControl(new maplibregl.NavigationControl(), "top-right");
2268
2355
  let threeDimensionalControl;
2269
2356
  let treeControl;
2270
2357
  if (isSemanticPreview) {
@@ -2272,8 +2359,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2272
2359
  treeControl = new TreeControl();
2273
2360
  map.addControl(threeDimensionalControl, "top-right");
2274
2361
  map.addControl(treeControl, "top-right");
2362
+ // Embedded Map DevTools owns the common Controls sidebar. Keep the controls mounted
2363
+ // for their renderer lifecycle and message bridge, but remove duplicate buttons.
2364
+ if (window.parent !== window) {
2365
+ threeDimensionalControl.container.hidden = true;
2366
+ treeControl.container.hidden = true;
2367
+ }
2275
2368
  }
2276
- if (previewThemes.length > 1) map.addControl(new ThemeControl(), "top-right");
2369
+ // Map DevTools has a shared theme selector in its Controls panel; keep the map-level
2370
+ // cycling button for standalone previews only.
2371
+ if (previewThemes.length > 1 && window.parent === window) map.addControl(new ThemeControl(), "top-right");
2277
2372
  let ensuringThreeDimensionalLayers;
2278
2373
  const ensureThreeDimensionalLayers = () => {
2279
2374
  if (ensuringThreeDimensionalLayers) return ensuringThreeDimensionalLayers;
@@ -2360,13 +2455,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2360
2455
  map.on("tileflow:3d-toggle", ensureThreeDimensionalLayers);
2361
2456
  map.on("tileflow:trees-toggle", ensureThreeDimensionalLayers);
2362
2457
  map.on("load", () => {
2458
+ mountCoreScene(map);
2459
+ treeControl?.update();
2363
2460
  writeCameraToUrl(map);
2364
2461
  updateGlobeBackdrop(map);
2365
2462
  ensureThreeDimensionalLayers();
2366
- mountCoreScene(map);
2367
2463
  });
2368
2464
  map.on("move", () => updateGlobeBackdrop(map));
2465
+ map.on("style.load", () => updateGlobeBackdrop(map));
2369
2466
  map.on("moveend", () => writeCameraToUrl(map));
2467
+ ${previewSceneClient}
2370
2468
  addEventListener("message", (event) => {
2371
2469
  if (
2372
2470
  window.parent === window ||
@@ -2437,6 +2535,15 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2437
2535
  }
2438
2536
 
2439
2537
  applyStatus(initialStatus);
2538
+ // Embedded previews share their host's stream. Two streams per tab can exhaust Chrome's
2539
+ // six HTTP/1 connections and leave even local module/font requests waiting indefinitely.
2540
+ let unsubscribePreviewStatus;
2541
+ try {
2542
+ if (window.parent !== window) unsubscribePreviewStatus = window.parent.__tileflowDevtoolsHost?.subscribePreviewStatus?.(window, applyStatus);
2543
+ } catch { /* Other embedders keep the standalone stream. */ }
2544
+ if (unsubscribePreviewStatus) {
2545
+ addEventListener("beforeunload", unsubscribePreviewStatus);
2546
+ } else {
2440
2547
  const events = new EventSource(${JSON.stringify(`${basePath}/__events`)});
2441
2548
  // Generations are monotonic only within one dev-server process. A source-code watcher can
2442
2549
  // restart that process at generation 1, so reconnecting after a live connection was lost
@@ -2457,6 +2564,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
2457
2564
  for (const eventName of ["building", "ready", "invalid"]) {
2458
2565
  events.addEventListener(eventName, (event) => applyStatus(JSON.parse(event.data)));
2459
2566
  }
2567
+ }
2460
2568
  </script>
2461
2569
  </body>
2462
2570
  </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-B84H8Ymu.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;