@tileflow/dev 0.1.0-alpha.30 → 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
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  getFirstTileflowMapName
3
- } from "./chunk-6MGFMKG3.js";
3
+ } from "./chunk-AQUGMUZW.js";
4
4
  import "./chunk-D4OE4ITL.js";
5
5
 
6
6
  // src/preview.ts
@@ -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,17 +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";
317
+ import {createTileflowBuildings3D, createTileflowStreetLights, createTileflowVegetation} from "${basePath}/__runtime/tileflow-browser.js";
318
+ import {readTileflowSunPreset, resolveTileflowSun, tileflowDefaultSunPreset} from "${basePath}/__runtime/tileflow-browser.js";
268
319
 
269
320
  maplibregl.setWorkerUrl("${basePath}/__runtime/maplibre-gl-worker.mjs");
270
321
 
322
+ const devtoolsObserver = window.parent !== window ? createPreviewObserver() : undefined;
323
+ globalThis.__tileflowDevtoolsObserver = devtoolsObserver;
324
+ if (window.parent !== window) document.documentElement.dataset.devtoolsEmbedded = "true";
325
+
271
326
  const initialStatus = ${JSON.stringify(initialStatus)};
272
327
  const normalizeTileflowLandmarkManifest = ${normalizeTileflowLandmarkManifest.toString()};
273
328
  const readBoundedTileflowResponse = ${readBoundedTileflowResponse.toString()};
274
329
  const readBoundedTileflowJsonResponse = ${readBoundedTileflowJsonResponse.toString()};
275
330
  const initialGeneration = initialStatus.generation;
331
+ document.documentElement.dataset.tileflowArtifactGeneration = String(initialStatus.lastGoodGeneration ?? initialGeneration);
276
332
  const badge = document.getElementById("badge");
277
333
  const status = document.getElementById("status");
278
334
  const previewLabel = ${JSON.stringify(preview?.label)};
@@ -282,64 +338,40 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
282
338
  const previewThemes = ${serializeInlineJson(preview?.themeNames ?? [])};
283
339
  const isSemanticPreview = ${JSON.stringify(isSemanticPreview)};
284
340
  const previewFontFaces = ${serializeInlineJson(fontFaces)};
285
- const treeSearchParameters = new URL(location.href).searchParams;
286
- const requestedTreeRenderer = treeSearchParameters.get("treeRenderer");
287
- const treeRendererMode = ["circle", "simple", "complex"].includes(
288
- requestedTreeRenderer
289
- ) ? requestedTreeRenderer : "circle";
290
- const treeBackendMode = treeSearchParameters.get("treeBackend") === "three"
291
- ? "three"
292
- : "webgl2";
293
- const treeBenchmarkEnabled = treeSearchParameters.get("treeBenchmark") === "1";
294
- const mapBenchmarkEnabled = treeSearchParameters.get("mapBenchmark") === "1";
295
- const mapSweepEnabled = treeSearchParameters.get("mapSweep") === "1";
296
- const mapCompareEnabled = treeSearchParameters.get("mapCompare") === "1";
297
- const requestedMapCompareLimit = Number(treeSearchParameters.get("mapCompareLimit"));
341
+ const previewSearchParameters = new URL(location.href).searchParams;
342
+ const mapBenchmarkEnabled = previewSearchParameters.get("mapBenchmark") === "1";
343
+ const mapSweepEnabled = previewSearchParameters.get("mapSweep") === "1";
344
+ const mapCompareEnabled = previewSearchParameters.get("mapCompare") === "1";
345
+ const requestedMapCompareLimit = Number(previewSearchParameters.get("mapCompareLimit"));
298
346
  const mapCompareLimit =
299
347
  Number.isInteger(requestedMapCompareLimit) &&
300
348
  requestedMapCompareLimit >= 1 &&
301
349
  requestedMapCompareLimit <= 64
302
350
  ? requestedMapCompareLimit
303
351
  : 16;
304
- const requestedMapWorkerCount = Number(treeSearchParameters.get("mapWorkers"));
352
+ const requestedMapWorkerCount = Number(previewSearchParameters.get("mapWorkers"));
305
353
  const mapWorkerCountOverride =
306
354
  Number.isInteger(requestedMapWorkerCount) &&
307
355
  requestedMapWorkerCount >= 1 &&
308
356
  requestedMapWorkerCount <= 3
309
357
  ? requestedMapWorkerCount
310
358
  : undefined;
311
- const mapBenchmarkLongitude = Number(treeSearchParameters.get("lng"));
312
- const mapBenchmarkLatitude = Number(treeSearchParameters.get("lat"));
359
+ const mapBenchmarkLongitude = Number(previewSearchParameters.get("lng"));
360
+ const mapBenchmarkLatitude = Number(previewSearchParameters.get("lat"));
313
361
  const mapBenchmarkCenter =
314
- treeSearchParameters.has("lng") &&
315
- treeSearchParameters.has("lat") &&
362
+ previewSearchParameters.has("lng") &&
363
+ previewSearchParameters.has("lat") &&
316
364
  Number.isFinite(mapBenchmarkLongitude) &&
317
365
  Number.isFinite(mapBenchmarkLatitude)
318
366
  ? [mapBenchmarkLongitude, mapBenchmarkLatitude]
319
367
  : undefined;
320
368
  function readToggleFromUrl(name, fallback) {
321
- const values = treeSearchParameters.getAll(name);
369
+ const values = previewSearchParameters.getAll(name);
322
370
  if (values.length !== 1) return fallback;
323
371
  if (values[0] === "on") return true;
324
372
  if (values[0] === "off") return false;
325
373
  return fallback;
326
374
  }
327
- const treeFrameSamples = [];
328
- const treeMetrics = {
329
- buildMilliseconds: undefined,
330
- backend: treeBackendMode,
331
- frameP95Milliseconds: undefined,
332
- mode: treeRendererMode,
333
- queryMilliseconds: undefined,
334
- refreshMilliseconds: undefined,
335
- renderCalls: 0,
336
- renderMilliseconds: undefined,
337
- renderedTriangles: 0,
338
- selectionMilliseconds: undefined,
339
- terrainMilliseconds: undefined,
340
- treeCount: 0
341
- };
342
- globalThis.__tileflowTreeMetrics = treeMetrics;
343
375
  let threeDimensionalEnabled = readToggleFromUrl("buildings3d", false);
344
376
  let treesEnabled = readToggleFromUrl("trees3d", true);
345
377
  const landmarkState = {
@@ -493,26 +525,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
493
525
  return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * percentileValue) - 1)];
494
526
  }
495
527
 
496
- function updateTreeStatus(message, details = {}) {
497
- globalThis.__tileflowTreeState = {message, ...details};
498
- }
499
-
500
- if (treeBenchmarkEnabled) {
501
- let previousFrameTime;
502
- function sampleTreeFrame(frameTime) {
503
- if (previousFrameTime !== undefined) {
504
- treeFrameSamples.push(frameTime - previousFrameTime);
505
- }
506
- previousFrameTime = frameTime;
507
- if (treeFrameSamples.length < 360) {
508
- requestAnimationFrame(sampleTreeFrame);
509
- return;
510
- }
511
- treeMetrics.frameP95Milliseconds = percentile(treeFrameSamples, 0.95);
512
- }
513
- requestAnimationFrame(sampleTreeFrame);
514
- }
515
-
516
528
  function installMapBenchmark(map) {
517
529
  if (!mapBenchmarkEnabled) return;
518
530
  let activeRun;
@@ -982,14 +994,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
982
994
  return;
983
995
  }
984
996
 
985
- // A Web Mercator world is 512 * 2^zoom pixels wide. Dividing its
986
- // circumference by 2\u03C0 closely tracks MapLibre's on-screen globe radius.
987
- const radius = Math.min(
988
- 4096,
989
- ((512 * Math.pow(2, map.getZoom())) / (2 * Math.PI)) * 0.91
990
- );
991
997
  container.classList.add("tileflow-globe");
992
- container.style.setProperty("--tileflow-globe-radius", radius.toFixed(2) + "px");
993
998
  applyAtmosphereBackdrop(map, container);
994
999
  }
995
1000
 
@@ -1003,12 +1008,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
1003
1008
  const value = Number.parseInt(match[1], 16);
1004
1009
  return "rgba(" + (value >> 16) + ", " + ((value >> 8) & 255) + ", " + (value & 255) + ", " + alpha + ")";
1005
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);
1006
1016
  const properties = {
1007
1017
  "--tileflow-space": rgba(atmosphere?.spaceColor, 1),
1008
- "--tileflow-rim-line": rgba(atmosphere?.horizonColor, 0.88),
1009
- "--tileflow-rim-core": rgba(atmosphere?.horizonColor, 0.9),
1010
- "--tileflow-rim-glow": rgba(atmosphere?.horizonColor, 0.52),
1011
- "--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",
1012
1021
  };
1013
1022
  for (const [name, value] of Object.entries(properties)) {
1014
1023
  if (value) container.style.setProperty(name, value);
@@ -1456,1719 +1465,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
1456
1465
  map.addLayer(createBuildingWireframeLayer(map, styleLayer), layerAboveBuildings);
1457
1466
  }
1458
1467
 
1459
- function createTreeLayer(map, styleLayer) {
1460
- const initialTreeInstanceCapacity = 64;
1461
- const maximumCachedTrees = 12000;
1462
- const sourceRefreshIntervalMilliseconds = 120;
1463
- const viewportBufferPixels = 96;
1464
- const treeLods = [
1465
- {
1466
- densityCellPixels: 12,
1467
- name: "low",
1468
- untilZoom: 16.5
1469
- },
1470
- {
1471
- densityCellPixels: 8,
1472
- name: "medium",
1473
- untilZoom: 17.5
1474
- },
1475
- {
1476
- densityCellPixels: 0,
1477
- name: "full",
1478
- untilZoom: Infinity
1479
- }
1480
- ];
1481
- const sourceId = styleLayer.source;
1482
- const heightField = styleLayer.metadata?.["tileflow:tree-height-field"] ?? "height";
1483
- const crownField = styleLayer.metadata?.["tileflow:tree-crown-field"] ?? "diameter_crown";
1484
- const genusField = styleLayer.metadata?.["tileflow:tree-genus-field"] ?? "genus";
1485
- const leafTypeField = styleLayer.metadata?.["tileflow:tree-leaf-type-field"] ?? "leaf_type";
1486
- const speciesField = styleLayer.metadata?.["tileflow:tree-species-field"] ?? "species";
1487
- const heightScaleValue = Number(
1488
- styleLayer.metadata?.["tileflow:tree-height-scale"] ?? 1
1489
- );
1490
- const crownScaleValue = Number(
1491
- styleLayer.metadata?.["tileflow:tree-crown-scale"] ?? 1
1492
- );
1493
- const heightScale = Number.isFinite(heightScaleValue) && heightScaleValue > 0
1494
- ? heightScaleValue
1495
- : 1;
1496
- const crownScale = Number.isFinite(crownScaleValue) && crownScaleValue > 0
1497
- ? crownScaleValue
1498
- : 1;
1499
- let scene;
1500
- let sceneElevation;
1501
- let sceneOriginMercator;
1502
- let camera;
1503
- let renderer;
1504
- let treeGroup;
1505
- let treeMaterial;
1506
- let treeGeometries = [];
1507
- let treeMeshes = [];
1508
- let treeMeshCapacities = [];
1509
- let treeShadowGeometry;
1510
- let treeShadowMesh;
1511
- let treeShadowCapacity = 0;
1512
- let treeShadowMaterial;
1513
- let activeTreeBackend = treeBackendMode;
1514
- let rawGl;
1515
- let rawProgram;
1516
- let rawMatrixUniform;
1517
- let rawTreeResources = [];
1518
- let rawShadowProgram;
1519
- let rawShadowMatrixUniform;
1520
- let rawShadowOpacityUniform;
1521
- let rawShadowResource;
1522
- let rawTreesVisible = false;
1523
- const fallbackCircleOpacity = styleLayer.paint?.["circle-opacity"] ?? 0.82;
1524
- const fallbackCircleStrokeOpacity =
1525
- styleLayer.paint?.["circle-stroke-opacity"] ?? 0.55;
1526
- let fallbackVisible = true;
1527
- let emptyRefreshAttempts = 0;
1528
- let emptyRefreshTimer;
1529
- let lastRefreshTime = 0;
1530
- let lastTreeSelectionSignature;
1531
- let refreshQueued = false;
1532
- let refreshTimer;
1533
- let renderedTreeCount = 0;
1534
- let sourceSettled = false;
1535
- const treeStateCache = new Map();
1536
- const matrix = new THREE.Matrix4();
1537
- const mapMatrix = new THREE.Matrix4();
1538
- const sceneMatrix = new THREE.Matrix4();
1539
- const combinedMatrix = new THREE.Matrix4();
1540
- const position = new THREE.Vector3();
1541
- const scale = new THREE.Vector3();
1542
- const crownRotation = new THREE.Quaternion();
1543
- const yAxis = new THREE.Vector3(0, 1, 0);
1544
- const instanceTint = new THREE.Color();
1545
- const shadowMatrix = new THREE.Matrix4();
1546
- const shadowPosition = new THREE.Vector3();
1547
- const shadowScale = new THREE.Vector3();
1548
- const shadowRotation = new THREE.Quaternion();
1549
- const treeShadowLiftMeters = 0.04;
1550
- const treeShadowOpacity = 0.17;
1551
- const barkColor = new THREE.Color(
1552
- styleLayer.metadata?.["tileflow:tree-bark-color"] ?? "#8F8E79"
1553
- );
1554
- const broadleafColorValues =
1555
- styleLayer.metadata?.["tileflow:tree-broadleaf-colors"];
1556
- const broadleafPalette = (
1557
- Array.isArray(broadleafColorValues) && broadleafColorValues.length > 0
1558
- ? broadleafColorValues
1559
- : ["#87BA8C", "#98C89A", "#AAD4A7", "#B8DDB1"]
1560
- ).map((color) => new THREE.Color(color));
1561
- const coniferColorValues = styleLayer.metadata?.["tileflow:tree-conifer-colors"];
1562
- const coniferPalette = (
1563
- Array.isArray(coniferColorValues) && coniferColorValues.length > 0
1564
- ? coniferColorValues
1565
- : ["#77A77E", "#87B58A", "#98C399"]
1566
- ).map((color) => new THREE.Color(color));
1567
- const palmPalette = ["#82B77C", "#91C487", "#A0D093"].map(
1568
- (color) => new THREE.Color(color)
1569
- );
1570
-
1571
- function addGeometryPart(parts, geometry, partPosition, partScale, rotation, color) {
1572
- const partMatrix = new THREE.Matrix4().compose(
1573
- new THREE.Vector3(...partPosition),
1574
- rotation,
1575
- new THREE.Vector3(...partScale)
1576
- );
1577
- geometry.applyMatrix4(partMatrix);
1578
- if (!geometry.getAttribute("normal")) geometry.computeVertexNormals();
1579
- const renderGeometry = geometry.index ? geometry.toNonIndexed() : geometry;
1580
- const positions = renderGeometry.getAttribute("position").array;
1581
- const normals = renderGeometry.getAttribute("normal").array;
1582
- const colors = new Float32Array((positions.length / 3) * 3);
1583
- const indices = Uint32Array.from(
1584
- {length: positions.length / 3},
1585
- (_value, index) => index
1586
- );
1587
- for (let index = 0; index < colors.length; index += 3) {
1588
- colors[index] = color.r;
1589
- colors[index + 1] = color.g;
1590
- colors[index + 2] = color.b;
1591
- }
1592
- parts.push({
1593
- colors,
1594
- indices,
1595
- normals: new Float32Array(normals),
1596
- positions: new Float32Array(positions)
1597
- });
1598
- if (renderGeometry !== geometry) renderGeometry.dispose();
1599
- geometry.dispose();
1600
- }
1601
-
1602
- function addTaperedBranchPart(
1603
- parts,
1604
- start,
1605
- end,
1606
- baseRadius,
1607
- tipRadius,
1608
- radialSegments = 6
1609
- ) {
1610
- const startPoint = new THREE.Vector3(...start);
1611
- const endPoint = new THREE.Vector3(...end);
1612
- const direction = new THREE.Vector3().subVectors(endPoint, startPoint);
1613
- const branchRotation = new THREE.Quaternion().setFromUnitVectors(
1614
- yAxis,
1615
- direction.clone().normalize()
1616
- );
1617
- const center = startPoint.clone().add(endPoint).multiplyScalar(0.5);
1618
- addGeometryPart(
1619
- parts,
1620
- new THREE.CylinderGeometry(tipRadius, baseRadius, 1, radialSegments, 1, true),
1621
- [center.x, center.y, center.z],
1622
- [1, direction.length(), 1],
1623
- branchRotation,
1624
- barkColor
1625
- );
1626
- }
1627
-
1628
- function createOrganicCrownGeometry(seed, simple) {
1629
- const geometry = new THREE.SphereGeometry(
1630
- 0.5,
1631
- simple ? 8 : 12,
1632
- simple ? 5 : 7
1633
- );
1634
- const positions = geometry.getAttribute("position");
1635
- for (let index = 0; index < positions.count; index += 1) {
1636
- const x = positions.getX(index);
1637
- const y = positions.getY(index);
1638
- const z = positions.getZ(index);
1639
- const angle = Math.atan2(z, x);
1640
- const normalizedHeight = Math.max(-1, Math.min(1, y * 2));
1641
- const latitudeWeight = 1 - normalizedHeight * normalizedHeight;
1642
- const ripple =
1643
- 1 +
1644
- Math.sin(angle * 3 + seed * 1.73) * 0.065 * latitudeWeight +
1645
- Math.sin(angle * 5 - seed * 0.91) * 0.035 * latitudeWeight +
1646
- Math.sin(y * Math.PI * 4 + seed * 0.63) * 0.025;
1647
- positions.setXYZ(
1648
- index,
1649
- x * ripple,
1650
- y * (1 + Math.cos(angle * 2 + seed) * 0.035),
1651
- z * ripple
1652
- );
1653
- }
1654
- positions.needsUpdate = true;
1655
- geometry.deleteAttribute("normal");
1656
- geometry.computeVertexNormals();
1657
- return geometry;
1658
- }
1659
-
1660
- function createScallopedConeGeometry(seed, simple) {
1661
- const geometry = new THREE.ConeGeometry(
1662
- 0.5,
1663
- 1,
1664
- simple ? 8 : 12,
1665
- simple ? 2 : 3
1666
- );
1667
- const positions = geometry.getAttribute("position");
1668
- for (let index = 0; index < positions.count; index += 1) {
1669
- const x = positions.getX(index);
1670
- const y = positions.getY(index);
1671
- const z = positions.getZ(index);
1672
- const angle = Math.atan2(z, x);
1673
- const progress = Math.max(0, Math.min(1, y + 0.5));
1674
- const ripple =
1675
- 1 +
1676
- Math.sin(angle * 3 + seed * 1.31) * 0.08 +
1677
- Math.sin(angle * 5 - seed * 0.77) * 0.045 +
1678
- Math.sin(progress * Math.PI * 3 + seed) * 0.035;
1679
- positions.setXYZ(
1680
- index,
1681
- x * ripple,
1682
- y + Math.sin(angle * 2 + seed) * (1 - progress) * 0.018,
1683
- z * ripple
1684
- );
1685
- }
1686
- positions.needsUpdate = true;
1687
- geometry.deleteAttribute("normal");
1688
- geometry.computeVertexNormals();
1689
- return geometry;
1690
- }
1691
-
1692
- function createCurvedPalmFrondGeometry(seed, simple, fan) {
1693
- const segments = simple ? 5 : 8;
1694
- const length = fan ? 0.49 + (seed % 3) * 0.025 : 0.58 + (seed % 3) * 0.03;
1695
- const droop = fan ? 0.16 + (seed % 2) * 0.025 : 0.25 + (seed % 3) * 0.025;
1696
- const sway = (((seed * 37) % 7) - 3) * 0.008;
1697
- const widthScale = fan ? 0.17 : 0.024;
1698
- const centerAt = (progress) => [
1699
- length * progress,
1700
- Math.sin(Math.PI * progress) * (fan ? 0.08 : 0.11) -
1701
- droop * Math.pow(progress, 1.7),
1702
- sway * Math.sin(Math.PI * progress)
1703
- ];
1704
- const points = [];
1705
- for (let step = 0; step <= segments; step += 1) {
1706
- const progress = step / segments;
1707
- const envelope = Math.pow(Math.sin(Math.PI * progress), 0.7) * (1 - 0.25 * progress);
1708
- const width = Math.max(0.008, widthScale * envelope);
1709
- const [centerX, centerY, centerZ] = centerAt(progress);
1710
- points.push(
1711
- [centerX, centerY, centerZ - width],
1712
- [centerX, centerY, centerZ + width]
1713
- );
1714
- }
1715
- const trianglePositions = [];
1716
- const appendTriangle = (first, second, third) => {
1717
- trianglePositions.push(...first, ...second, ...third);
1718
- };
1719
- const appendDoubleSidedQuad = (leftStart, rightStart, leftEnd, rightEnd) => {
1720
- appendTriangle(leftStart, rightStart, leftEnd);
1721
- appendTriangle(rightStart, rightEnd, leftEnd);
1722
- appendTriangle(leftEnd, rightStart, leftStart);
1723
- appendTriangle(leftEnd, rightEnd, rightStart);
1724
- };
1725
- for (let segment = 0; segment < segments; segment += 1) {
1726
- const leftStart = points[segment * 2];
1727
- const rightStart = points[segment * 2 + 1];
1728
- const leftEnd = points[(segment + 1) * 2];
1729
- const rightEnd = points[(segment + 1) * 2 + 1];
1730
- appendDoubleSidedQuad(leftStart, rightStart, leftEnd, rightEnd);
1731
- }
1732
- if (!fan) {
1733
- const leafletCount = simple ? 4 : 7;
1734
- for (let leaflet = 0; leaflet < leafletCount; leaflet += 1) {
1735
- const progress = 0.17 + (leaflet / (leafletCount - 1)) * 0.66;
1736
- const [centerX, centerY, centerZ] = centerAt(progress);
1737
- const leafletLength =
1738
- (0.075 + Math.sin(Math.PI * progress) * 0.095) *
1739
- (0.94 + ((leaflet + seed) % 3) * 0.035);
1740
- const leafletWidth = 0.018 + Math.sin(Math.PI * progress) * 0.012;
1741
- const leafletBack = 0.018 + ((leaflet + seed) % 2) * 0.012;
1742
- const leafletDrop = 0.022 + progress * 0.045;
1743
- for (const side of [-1, 1]) {
1744
- const base = [centerX - 0.014, centerY, centerZ + side * 0.007];
1745
- const midCenter = [
1746
- centerX + 0.012,
1747
- centerY - leafletDrop * 0.35,
1748
- centerZ + side * leafletLength * 0.58
1749
- ];
1750
- const midLeft = [
1751
- midCenter[0] - leafletWidth,
1752
- midCenter[1],
1753
- midCenter[2] - side * leafletWidth * 0.14
1754
- ];
1755
- const midRight = [
1756
- midCenter[0] + leafletWidth,
1757
- midCenter[1],
1758
- midCenter[2] + side * leafletWidth * 0.14
1759
- ];
1760
- const tip = [
1761
- centerX - leafletBack,
1762
- centerY - leafletDrop,
1763
- centerZ + side * leafletLength
1764
- ];
1765
- appendTriangle(base, midRight, midLeft);
1766
- appendTriangle(midLeft, midRight, tip);
1767
- appendTriangle(midLeft, midRight, base);
1768
- appendTriangle(tip, midRight, midLeft);
1769
- }
1770
- }
1771
- }
1772
- const geometry = new THREE.BufferGeometry();
1773
- geometry.setAttribute(
1774
- "position",
1775
- new THREE.Float32BufferAttribute(trianglePositions, 3)
1776
- );
1777
- geometry.computeVertexNormals();
1778
- return geometry;
1779
- }
1780
-
1781
- function mergeGeometryParts(parts) {
1782
- const positionLength = parts.reduce((total, part) => total + part.positions.length, 0);
1783
- const positions = new Float32Array(positionLength);
1784
- const normals = new Float32Array(positionLength);
1785
- const colors = new Float32Array(positionLength);
1786
- const indexLength = parts.reduce((total, part) => total + part.indices.length, 0);
1787
- const indices = new Uint32Array(indexLength);
1788
- let offset = 0;
1789
- let indexOffset = 0;
1790
- let vertexOffset = 0;
1791
- for (const part of parts) {
1792
- positions.set(part.positions, offset);
1793
- normals.set(part.normals, offset);
1794
- colors.set(part.colors, offset);
1795
- for (let index = 0; index < part.indices.length; index += 1) {
1796
- indices[indexOffset + index] = part.indices[index] + vertexOffset;
1797
- }
1798
- indexOffset += part.indices.length;
1799
- vertexOffset += part.positions.length / 3;
1800
- offset += part.positions.length;
1801
- }
1802
- const geometry = new THREE.BufferGeometry();
1803
- geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
1804
- geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
1805
- geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
1806
- geometry.setIndex(new THREE.BufferAttribute(indices, 1));
1807
- geometry.computeBoundingBox();
1808
- geometry.computeBoundingSphere();
1809
- return geometry;
1810
- }
1811
-
1812
- function createBroadleafTreeGeometry(variant, geometryVariant, simple) {
1813
- const parts = [];
1814
- const trunkTops = [
1815
- [0.008, 0.56, -0.004],
1816
- [-0.012, 0.51, 0.008],
1817
- [0.006, 0.62, 0.004]
1818
- ];
1819
- const trunkTopBase = trunkTops[variant] ?? trunkTops[0];
1820
- const trunkTop = [
1821
- trunkTopBase[0] + (geometryVariant === 0 ? -0.008 : 0.012),
1822
- trunkTopBase[1] + (geometryVariant === 0 ? -0.012 : 0.016),
1823
- trunkTopBase[2] + (geometryVariant === 0 ? 0.006 : -0.01)
1824
- ];
1825
- addTaperedBranchPart(
1826
- parts,
1827
- [0, 0, 0],
1828
- trunkTop,
1829
- 0.04 + variant * 0.001,
1830
- 0.022,
1831
- simple ? 6 : 8
1832
- );
1833
-
1834
- const branchTargetLayouts = [
1835
- [
1836
- [-0.2, 0.72, 0.03],
1837
- [0.01, 0.79, -0.04],
1838
- [0.21, 0.72, -0.02],
1839
- [-0.03, 0.7, 0.18],
1840
- [0.06, 0.73, -0.17]
1841
- ],
1842
- [
1843
- [-0.27, 0.68, 0.02],
1844
- [-0.09, 0.76, -0.08],
1845
- [0.25, 0.69, -0.03],
1846
- [0.06, 0.72, 0.2],
1847
- [0.13, 0.79, -0.13]
1848
- ],
1849
- [
1850
- [-0.15, 0.75, 0.02],
1851
- [0.04, 0.86, -0.04],
1852
- [0.16, 0.77, 0.01],
1853
- [0.02, 0.8, 0.15]
1854
- ]
1855
- ];
1856
- const branchTargets = branchTargetLayouts[variant] ?? branchTargetLayouts[0];
1857
- const branchCount = simple ? Math.min(4, branchTargets.length) : branchTargets.length;
1858
- for (let branch = 0; branch < branchCount; branch += 1) {
1859
- const branchTarget = branchTargets[branch];
1860
- const shelteredBranchTarget = [
1861
- branchTarget[0] * 0.86,
1862
- branchTarget[1] - 0.07,
1863
- branchTarget[2] * 0.86
1864
- ];
1865
- addTaperedBranchPart(
1866
- parts,
1867
- [trunkTop[0] * 0.55, 0.38 + branch * 0.018, trunkTop[2] * 0.55],
1868
- shelteredBranchTarget,
1869
- 0.022 - branch * 0.0015,
1870
- 0.0095 - branch * 0.00065,
1871
- simple ? 5 : 7
1872
- );
1873
- }
1874
-
1875
- const roundCrownLayout = [
1876
- {position: [0, 0.8, 0.01], scale: [0.72, 0.39, 0.66]},
1877
- {position: [-0.27, 0.73, 0.05], scale: [0.45, 0.26, 0.42]},
1878
- {position: [0.27, 0.75, -0.06], scale: [0.44, 0.25, 0.41]},
1879
- {position: [-0.08, 0.7, 0.24], scale: [0.4, 0.23, 0.37]},
1880
- {position: [0.1, 0.72, -0.24], scale: [0.41, 0.24, 0.38]}
1881
- ];
1882
- const openCrownLayout = [
1883
- {position: [-0.29, 0.72, 0.03], scale: [0.47, 0.22, 0.39]},
1884
- {position: [-0.08, 0.83, -0.1], scale: [0.44, 0.24, 0.36]},
1885
- {position: [0.22, 0.79, -0.04], scale: [0.49, 0.22, 0.4]},
1886
- {position: [0.31, 0.67, 0.09], scale: [0.4, 0.19, 0.34]},
1887
- {position: [0.04, 0.68, 0.22], scale: [0.45, 0.21, 0.38]},
1888
- {position: [-0.19, 0.65, 0.18], scale: [0.38, 0.18, 0.32]}
1889
- ];
1890
- const tallCrownLayout = [
1891
- {position: [0, 0.79, 0.01], scale: [0.53, 0.58, 0.48]},
1892
- {position: [-0.19, 0.76, 0.05], scale: [0.34, 0.4, 0.32]},
1893
- {position: [0.18, 0.8, -0.05], scale: [0.34, 0.42, 0.31]},
1894
- {position: [-0.05, 0.94, -0.08], scale: [0.32, 0.34, 0.3]},
1895
- {position: [0.06, 0.69, 0.15], scale: [0.32, 0.35, 0.3]}
1896
- ];
1897
- const crownLayouts = [roundCrownLayout, openCrownLayout, tallCrownLayout];
1898
- const crownLayout = crownLayouts[variant] ?? crownLayouts[0];
1899
- const lobeCount = simple ? Math.max(4, crownLayout.length - 1) : crownLayout.length;
1900
- const crownColorPatterns = [
1901
- [2, 1, 3, 0, 1],
1902
- [1, 2, 0, 3, 1, 2],
1903
- [2, 1, 3, 1, 2]
1904
- ];
1905
- const crownColorPattern = crownColorPatterns[variant] ?? crownColorPatterns[0];
1906
- for (let lobe = 0; lobe < lobeCount; lobe += 1) {
1907
- const lobeShape = crownLayout[lobe];
1908
- const lobeSeed = variant * 23 + geometryVariant * 11 + lobe * 3;
1909
- const lobePosition = [
1910
- lobeShape.position[0] + Math.sin(lobeSeed * 1.7) * 0.018,
1911
- lobeShape.position[1] + Math.cos(lobeSeed * 1.1) * 0.012,
1912
- lobeShape.position[2] + Math.cos(lobeSeed * 1.9) * 0.018
1913
- ];
1914
- addGeometryPart(
1915
- parts,
1916
- createOrganicCrownGeometry(lobeSeed, simple),
1917
- lobePosition,
1918
- lobeShape.scale,
1919
- new THREE.Quaternion().setFromEuler(
1920
- new THREE.Euler(
1921
- ((lobe % 3) - 1) * 0.09,
1922
- variant * 0.41 + geometryVariant * 0.27 + lobe * 0.37,
1923
- (((lobe + variant + geometryVariant) % 3) - 1) * 0.11
1924
- )
1925
- ),
1926
- broadleafPalette[
1927
- (crownColorPattern[lobe] + geometryVariant * 2) % broadleafPalette.length
1928
- ]
1929
- );
1930
- }
1931
- const crownCapShapes = [
1932
- {position: [0.01, 0.9, 0], scale: [0.3, 0.18, 0.28]},
1933
- {position: [0, 0.84, 0.01], scale: [0.29, 0.17, 0.27]},
1934
- {position: [0, 0.96, 0], scale: [0.24, 0.22, 0.23]}
1935
- ];
1936
- const crownCapShape = crownCapShapes[variant] ?? crownCapShapes[0];
1937
- addGeometryPart(
1938
- parts,
1939
- createOrganicCrownGeometry(149 + variant * 17 + geometryVariant * 7, simple),
1940
- crownCapShape.position,
1941
- crownCapShape.scale,
1942
- new THREE.Quaternion().setFromAxisAngle(
1943
- yAxis,
1944
- variant * 0.31 + geometryVariant * 0.43
1945
- ),
1946
- broadleafPalette[(1 + variant + geometryVariant * 2) % broadleafPalette.length]
1947
- );
1948
- return mergeGeometryParts(parts);
1949
- }
1950
-
1951
- function createConiferTreeGeometry(variant, geometryVariant, simple) {
1952
- const parts = [];
1953
- const columnar = variant === 1;
1954
- addTaperedBranchPart(
1955
- parts,
1956
- [0, 0, 0],
1957
- [variant === 0 ? 0.006 : -0.006, columnar ? 0.54 : 0.43, 0],
1958
- 0.035,
1959
- 0.018,
1960
- simple ? 6 : 8
1961
- );
1962
- if (columnar) {
1963
- const columnarCrownLayout = [
1964
- {position: [-0.01, 0.5, 0.01], scale: [0.38, 0.48, 0.36]},
1965
- {position: [0.01, 0.66, -0.01], scale: [0.42, 0.5, 0.39]},
1966
- {position: [-0.015, 0.8, 0.01], scale: [0.34, 0.42, 0.32]},
1967
- {position: [0.008, 0.91, 0], scale: [0.22, 0.28, 0.21]}
1968
- ];
1969
- const massCount = simple ? 3 : columnarCrownLayout.length;
1970
- for (let mass = 0; mass < massCount; mass += 1) {
1971
- const crownMass = columnarCrownLayout[mass];
1972
- addGeometryPart(
1973
- parts,
1974
- createOrganicCrownGeometry(71 + geometryVariant * 13 + mass * 5, simple),
1975
- crownMass.position,
1976
- crownMass.scale,
1977
- new THREE.Quaternion().setFromAxisAngle(yAxis, mass * 0.43),
1978
- coniferPalette[(mass + 1) % coniferPalette.length]
1979
- );
1980
- }
1981
- return mergeGeometryParts(parts);
1982
- }
1983
- const tierCount = simple ? 3 : 5;
1984
- for (let tier = 0; tier < tierCount; tier += 1) {
1985
- const progress = tier / (tierCount - 1);
1986
- const tierDiameter = 0.94 - progress * 0.5;
1987
- const tierHeight = 0.35 - progress * 0.075;
1988
- addGeometryPart(
1989
- parts,
1990
- createScallopedConeGeometry(geometryVariant * 19 + tier * 7, simple),
1991
- [
1992
- Math.sin(tier * 2.1 + geometryVariant) * 0.012,
1993
- 0.45 + progress * 0.44,
1994
- Math.cos(tier * 1.7 + geometryVariant) * 0.012
1995
- ],
1996
- [tierDiameter, tierHeight, tierDiameter],
1997
- new THREE.Quaternion().setFromAxisAngle(
1998
- yAxis,
1999
- variant * 0.6 + geometryVariant * 0.29 + tier * 0.31
2000
- ),
2001
- coniferPalette[(variant + tier) % coniferPalette.length]
2002
- );
2003
- }
2004
- return mergeGeometryParts(parts);
2005
- }
2006
-
2007
- function createPalmTreeGeometry(geometryVariant, simple) {
2008
- const parts = [];
2009
- const fan = geometryVariant === 1;
2010
- const lowerTrunkTop = [fan ? -0.014 : 0.012, 0.42, fan ? 0.006 : -0.004];
2011
- const upperTrunkTop = [fan ? 0.014 : -0.012, fan ? 0.73 : 0.78, fan ? -0.008 : 0.012];
2012
- addTaperedBranchPart(parts, [0, 0, 0], lowerTrunkTop, 0.032, 0.026, simple ? 7 : 9);
2013
- addTaperedBranchPart(
2014
- parts,
2015
- lowerTrunkTop,
2016
- upperTrunkTop,
2017
- 0.026,
2018
- 0.016,
2019
- simple ? 7 : 9
2020
- );
2021
- addGeometryPart(
2022
- parts,
2023
- createOrganicCrownGeometry(113 + geometryVariant * 17, simple),
2024
- [upperTrunkTop[0], upperTrunkTop[1] + 0.018, upperTrunkTop[2]],
2025
- [fan ? 0.19 : 0.16, 0.1, fan ? 0.19 : 0.16],
2026
- new THREE.Quaternion(),
2027
- palmPalette[1]
2028
- );
2029
- const frondCount = simple ? (fan ? 8 : 7) : (fan ? 12 : 11);
2030
- for (let frond = 0; frond < frondCount; frond += 1) {
2031
- const angle =
2032
- (frond / frondCount) * Math.PI * 2 +
2033
- (((frond * 5 + geometryVariant) % 3) - 1) * 0.045;
2034
- addGeometryPart(
2035
- parts,
2036
- createCurvedPalmFrondGeometry(frond + geometryVariant * 17, simple, fan),
2037
- [upperTrunkTop[0], upperTrunkTop[1] + 0.018, upperTrunkTop[2]],
2038
- [0.94 + (frond % 3) * 0.035, 0.96 + (frond % 2) * 0.035, 1],
2039
- new THREE.Quaternion().setFromEuler(
2040
- new THREE.Euler(
2041
- (((frond * 7 + geometryVariant) % 3) - 1) * 0.045,
2042
- -angle,
2043
- (((frond * 5 + geometryVariant) % 5) - 2) * 0.022
2044
- )
2045
- ),
2046
- palmPalette[(frond + geometryVariant) % palmPalette.length]
2047
- );
2048
- }
2049
- return mergeGeometryParts(parts);
2050
- }
2051
-
2052
- function createTreeGeometries() {
2053
- const simple = treeRendererMode === "simple";
2054
- return [
2055
- createBroadleafTreeGeometry(0, 0, simple),
2056
- createBroadleafTreeGeometry(0, 1, simple),
2057
- createBroadleafTreeGeometry(1, 0, simple),
2058
- createBroadleafTreeGeometry(1, 1, simple),
2059
- createBroadleafTreeGeometry(2, 0, simple),
2060
- createBroadleafTreeGeometry(2, 1, simple),
2061
- createConiferTreeGeometry(0, 0, simple),
2062
- createConiferTreeGeometry(1, 0, simple),
2063
- createPalmTreeGeometry(0, simple),
2064
- createPalmTreeGeometry(1, simple)
2065
- ];
2066
- }
2067
-
2068
- function compileRawShader(gl, type, source) {
2069
- const shader = gl.createShader(type);
2070
- if (!shader) throw new Error("Unable to create tree shader");
2071
- gl.shaderSource(shader, source);
2072
- gl.compileShader(shader);
2073
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
2074
- const message = gl.getShaderInfoLog(shader) || "unknown shader error";
2075
- gl.deleteShader(shader);
2076
- throw new Error(message);
2077
- }
2078
- return shader;
2079
- }
2080
-
2081
- function createRawTreeProgram(gl) {
2082
- const vertexShader = compileRawShader(
2083
- gl,
2084
- gl.VERTEX_SHADER,
2085
- "#version 300 es\\n" +
2086
- "precision highp float;\\n" +
2087
- "layout(location=0) in vec3 a_position;\\n" +
2088
- "layout(location=1) in vec3 a_normal;\\n" +
2089
- "layout(location=2) in vec3 a_color;\\n" +
2090
- "layout(location=3) in vec3 a_instance_position;\\n" +
2091
- "layout(location=4) in vec3 a_instance_scale;\\n" +
2092
- "layout(location=5) in vec2 a_instance_rotation;\\n" +
2093
- "layout(location=6) in float a_instance_tint;\\n" +
2094
- "uniform mat4 u_matrix;\\n" +
2095
- "out vec3 v_color;\\n" +
2096
- "void main() {\\n" +
2097
- " vec2 horizontal = a_position.xz * a_instance_scale.xz;\\n" +
2098
- " vec2 rotated = vec2(\\n" +
2099
- " horizontal.x * a_instance_rotation.x - horizontal.y * a_instance_rotation.y,\\n" +
2100
- " horizontal.x * a_instance_rotation.y + horizontal.y * a_instance_rotation.x\\n" +
2101
- " );\\n" +
2102
- " vec3 scaledNormal = normalize(a_normal / max(a_instance_scale, vec3(0.001)));\\n" +
2103
- " vec2 normalHorizontal = vec2(\\n" +
2104
- " scaledNormal.x * a_instance_rotation.x - scaledNormal.z * a_instance_rotation.y,\\n" +
2105
- " scaledNormal.x * a_instance_rotation.y + scaledNormal.z * a_instance_rotation.x\\n" +
2106
- " );\\n" +
2107
- " vec3 mapNormal = normalize(vec3(normalHorizontal.x, normalHorizontal.y, scaledNormal.y));\\n" +
2108
- " float sky = mapNormal.z * 0.5 + 0.5;\\n" +
2109
- " float key = max(dot(mapNormal, normalize(vec3(-0.35, -0.5, 0.78))), 0.0);\\n" +
2110
- " float heightOcclusion = mix(0.84, 1.0, smoothstep(0.16, 0.88, a_position.y));\\n" +
2111
- " float light = (0.67 + sky * 0.18 + key * 0.23) * heightOcclusion;\\n" +
2112
- " v_color = a_color * light * a_instance_tint;\\n" +
2113
- " vec3 mapPosition = vec3(\\n" +
2114
- " a_instance_position.x + rotated.x,\\n" +
2115
- " a_instance_position.z + rotated.y,\\n" +
2116
- " a_instance_position.y + a_position.y * a_instance_scale.y\\n" +
2117
- " );\\n" +
2118
- " gl_Position = u_matrix * vec4(mapPosition, 1.0);\\n" +
2119
- "}\\n"
2120
- );
2121
- const fragmentShader = compileRawShader(
2122
- gl,
2123
- gl.FRAGMENT_SHADER,
2124
- "#version 300 es\\n" +
2125
- "precision highp float;\\n" +
2126
- "in vec3 v_color;\\n" +
2127
- "out vec4 out_color;\\n" +
2128
- "vec3 linearToSrgb(vec3 color) {\\n" +
2129
- " vec3 lower = color * 12.92;\\n" +
2130
- " vec3 upper = 1.055 * pow(color, vec3(1.0 / 2.4)) - 0.055;\\n" +
2131
- " return mix(lower, upper, step(vec3(0.0031308), color));\\n" +
2132
- "}\\n" +
2133
- "void main() { out_color = vec4(linearToSrgb(v_color), 1.0); }\\n"
2134
- );
2135
- const program = gl.createProgram();
2136
- if (!program) throw new Error("Unable to create tree program");
2137
- gl.attachShader(program, vertexShader);
2138
- gl.attachShader(program, fragmentShader);
2139
- gl.linkProgram(program);
2140
- gl.deleteShader(vertexShader);
2141
- gl.deleteShader(fragmentShader);
2142
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
2143
- const message = gl.getProgramInfoLog(program) || "unknown program error";
2144
- gl.deleteProgram(program);
2145
- throw new Error(message);
2146
- }
2147
- return program;
2148
- }
2149
-
2150
- function createRawTreeShadowProgram(gl) {
2151
- const vertexShader = compileRawShader(
2152
- gl,
2153
- gl.VERTEX_SHADER,
2154
- "#version 300 es\\n" +
2155
- "precision highp float;\\n" +
2156
- "layout(location=0) in vec2 a_corner;\\n" +
2157
- "layout(location=1) in vec4 a_instance;\\n" +
2158
- "uniform mat4 u_matrix;\\n" +
2159
- "out vec2 v_shadow_coordinate;\\n" +
2160
- "void main() {\\n" +
2161
- " v_shadow_coordinate = a_corner;\\n" +
2162
- " vec2 shadowOffset = vec2(-0.28, 0.34) * a_instance.w;\\n" +
2163
- " vec2 shadowPosition = a_corner * vec2(1.0, 0.76) * a_instance.w + shadowOffset;\\n" +
2164
- " vec3 mapPosition = vec3(\\n" +
2165
- " a_instance.x + shadowPosition.x,\\n" +
2166
- " a_instance.z + shadowPosition.y,\\n" +
2167
- " a_instance.y + 0.04\\n" +
2168
- " );\\n" +
2169
- " gl_Position = u_matrix * vec4(mapPosition, 1.0);\\n" +
2170
- "}\\n"
2171
- );
2172
- const fragmentShader = compileRawShader(
2173
- gl,
2174
- gl.FRAGMENT_SHADER,
2175
- "#version 300 es\\n" +
2176
- "precision mediump float;\\n" +
2177
- "in vec2 v_shadow_coordinate;\\n" +
2178
- "uniform float u_opacity;\\n" +
2179
- "out vec4 out_color;\\n" +
2180
- "void main() {\\n" +
2181
- " float radiusSquared = dot(v_shadow_coordinate, v_shadow_coordinate);\\n" +
2182
- " if (radiusSquared >= 1.0) discard;\\n" +
2183
- " float coverage = 1.0 - smoothstep(0.90, 1.0, radiusSquared);\\n" +
2184
- " float alpha = u_opacity * coverage;\\n" +
2185
- " out_color = vec4(0.18, 0.21, 0.17, alpha);\\n" +
2186
- "}\\n"
2187
- );
2188
- const program = gl.createProgram();
2189
- if (!program) throw new Error("Unable to create tree shadow program");
2190
- gl.attachShader(program, vertexShader);
2191
- gl.attachShader(program, fragmentShader);
2192
- gl.linkProgram(program);
2193
- gl.deleteShader(vertexShader);
2194
- gl.deleteShader(fragmentShader);
2195
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
2196
- const message = gl.getProgramInfoLog(program) || "unknown tree shadow program error";
2197
- gl.deleteProgram(program);
2198
- throw new Error(message);
2199
- }
2200
- return program;
2201
- }
2202
-
2203
- function createRawTreeShadowResource(gl) {
2204
- const vertexArray = gl.createVertexArray();
2205
- const cornerBuffer = gl.createBuffer();
2206
- const instanceBuffer = gl.createBuffer();
2207
- if (!vertexArray || !cornerBuffer || !instanceBuffer) {
2208
- throw new Error("Unable to allocate tree shadow buffers");
2209
- }
2210
- gl.bindVertexArray(vertexArray);
2211
- gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuffer);
2212
- gl.bufferData(
2213
- gl.ARRAY_BUFFER,
2214
- new Float32Array([-1, -1, 1, -1, 1, 1, -1, 1]),
2215
- gl.STATIC_DRAW
2216
- );
2217
- gl.enableVertexAttribArray(0);
2218
- gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
2219
- gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
2220
- gl.bufferData(gl.ARRAY_BUFFER, 0, gl.DYNAMIC_DRAW);
2221
- gl.enableVertexAttribArray(1);
2222
- gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 16, 0);
2223
- gl.vertexAttribDivisor(1, 1);
2224
- gl.bindVertexArray(null);
2225
- return {cornerBuffer, count: 0, instanceBuffer, vertexArray};
2226
- }
2227
-
2228
- function createRawTreeResource(gl, geometry) {
2229
- const vertexArray = gl.createVertexArray();
2230
- const positionBuffer = gl.createBuffer();
2231
- const normalBuffer = gl.createBuffer();
2232
- const colorBuffer = gl.createBuffer();
2233
- const indexBuffer = gl.createBuffer();
2234
- const instanceBuffer = gl.createBuffer();
2235
- if (
2236
- !vertexArray ||
2237
- !positionBuffer ||
2238
- !normalBuffer ||
2239
- !colorBuffer ||
2240
- !indexBuffer ||
2241
- !instanceBuffer
2242
- ) {
2243
- throw new Error("Unable to allocate tree buffers");
2244
- }
2245
- gl.bindVertexArray(vertexArray);
2246
- const attributes = [
2247
- [0, positionBuffer, geometry.getAttribute("position").array],
2248
- [1, normalBuffer, geometry.getAttribute("normal").array],
2249
- [2, colorBuffer, geometry.getAttribute("color").array]
2250
- ];
2251
- for (const [location, buffer, values] of attributes) {
2252
- gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
2253
- gl.bufferData(gl.ARRAY_BUFFER, values, gl.STATIC_DRAW);
2254
- gl.enableVertexAttribArray(location);
2255
- gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0);
2256
- }
2257
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
2258
- gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, geometry.index.array, gl.STATIC_DRAW);
2259
- gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
2260
- gl.bufferData(gl.ARRAY_BUFFER, 0, gl.DYNAMIC_DRAW);
2261
- gl.enableVertexAttribArray(3);
2262
- gl.vertexAttribPointer(3, 3, gl.FLOAT, false, 36, 0);
2263
- gl.vertexAttribDivisor(3, 1);
2264
- gl.enableVertexAttribArray(4);
2265
- gl.vertexAttribPointer(4, 3, gl.FLOAT, false, 36, 12);
2266
- gl.vertexAttribDivisor(4, 1);
2267
- gl.enableVertexAttribArray(5);
2268
- gl.vertexAttribPointer(5, 2, gl.FLOAT, false, 36, 24);
2269
- gl.vertexAttribDivisor(5, 1);
2270
- gl.enableVertexAttribArray(6);
2271
- gl.vertexAttribPointer(6, 1, gl.FLOAT, false, 36, 32);
2272
- gl.vertexAttribDivisor(6, 1);
2273
- gl.bindVertexArray(null);
2274
- return {
2275
- colorBuffer,
2276
- count: 0,
2277
- indexBuffer,
2278
- indexCount: geometry.index.count,
2279
- instanceBuffer,
2280
- normalBuffer,
2281
- positionBuffer,
2282
- vertexArray
2283
- };
2284
- }
2285
-
2286
- function disposeRawTreeResources() {
2287
- if (!rawGl) return;
2288
- for (const resource of rawTreeResources) {
2289
- rawGl.deleteBuffer(resource.positionBuffer);
2290
- rawGl.deleteBuffer(resource.normalBuffer);
2291
- rawGl.deleteBuffer(resource.colorBuffer);
2292
- rawGl.deleteBuffer(resource.indexBuffer);
2293
- rawGl.deleteBuffer(resource.instanceBuffer);
2294
- rawGl.deleteVertexArray(resource.vertexArray);
2295
- }
2296
- rawTreeResources = [];
2297
- if (rawProgram) rawGl.deleteProgram(rawProgram);
2298
- if (rawShadowResource) {
2299
- rawGl.deleteBuffer(rawShadowResource.cornerBuffer);
2300
- rawGl.deleteBuffer(rawShadowResource.instanceBuffer);
2301
- rawGl.deleteVertexArray(rawShadowResource.vertexArray);
2302
- }
2303
- if (rawShadowProgram) rawGl.deleteProgram(rawShadowProgram);
2304
- rawProgram = undefined;
2305
- rawMatrixUniform = undefined;
2306
- rawShadowProgram = undefined;
2307
- rawShadowMatrixUniform = undefined;
2308
- rawShadowOpacityUniform = undefined;
2309
- rawShadowResource = undefined;
2310
- }
2311
-
2312
- function numberProperty(properties, name) {
2313
- const value = Number(properties?.[name]);
2314
- return Number.isFinite(value) && value > 0 ? value : undefined;
2315
- }
2316
-
2317
- function stableTreeKey(feature, lng, lat) {
2318
- const properties = feature.properties ?? {};
2319
- const sourceFeatureId =
2320
- properties.osm_id ??
2321
- properties.osmId ??
2322
- properties.id ??
2323
- properties["@id"] ??
2324
- feature.id;
2325
- return sourceFeatureId === undefined || sourceFeatureId === null
2326
- ? lng.toFixed(6) + ":" + lat.toFixed(6)
2327
- : String(sourceFeatureId);
2328
- }
2329
-
2330
- function stableUnit(key, salt = 0) {
2331
- let hash = (2166136261 ^ salt) >>> 0;
2332
- for (let index = 0; index < key.length; index += 1) {
2333
- hash ^= key.charCodeAt(index);
2334
- hash = Math.imul(hash, 16777619);
2335
- }
2336
- hash += hash << 13;
2337
- hash ^= hash >>> 7;
2338
- hash += hash << 3;
2339
- hash ^= hash >>> 17;
2340
- hash += hash << 5;
2341
- return (hash >>> 0) / 4294967296;
2342
- }
2343
-
2344
- function treeLodForZoom(zoom) {
2345
- return treeLods.find((lod) => zoom < lod.untilZoom) ?? treeLods.at(-1);
2346
- }
2347
-
2348
- function setFallbackVisible(visible) {
2349
- if (fallbackVisible === visible || !map.getLayer(styleLayer.id)) return;
2350
- map.setPaintProperty(
2351
- styleLayer.id,
2352
- "circle-opacity",
2353
- visible ? fallbackCircleOpacity : 0
2354
- );
2355
- map.setPaintProperty(
2356
- styleLayer.id,
2357
- "circle-stroke-opacity",
2358
- visible ? fallbackCircleStrokeOpacity : 0
2359
- );
2360
- fallbackVisible = visible;
2361
- }
2362
-
2363
- function updateFallbackVisibility() {
2364
- // A non-empty query can still represent only one loaded tile. Keep
2365
- // the vector circles underneath until MapLibre reports the complete
2366
- // source settled, otherwise a partial 3D batch erases neighbouring
2367
- // tiles at source boundaries.
2368
- setFallbackVisible(!sourceSettled || renderedTreeCount === 0);
2369
- }
2370
-
2371
- function treeSelectionSignature(trees, lodName) {
2372
- let hash = 2166136261;
2373
- for (const tree of trees) {
2374
- for (let index = 0; index < tree.key.length; index += 1) {
2375
- hash ^= tree.key.charCodeAt(index);
2376
- hash = Math.imul(hash, 16777619);
2377
- }
2378
- hash ^= 0xff;
2379
- hash = Math.imul(hash, 16777619);
2380
- }
2381
- return lodName + ":" + trees.length + ":" + (hash >>> 0);
2382
- }
2383
-
2384
- function selectVisibleTrees(features, lod) {
2385
- const worldSize = 512 * Math.pow(2, map.getZoom());
2386
- const seen = new Set();
2387
- const candidates = [];
2388
- for (const feature of features) {
2389
- if (feature.geometry?.type !== "Point") continue;
2390
- const [lng, lat] = feature.geometry.coordinates;
2391
- const key = stableTreeKey(feature, lng, lat);
2392
- if (seen.has(key)) continue;
2393
- seen.add(key);
2394
- const latitudeSine = Math.sin(
2395
- (Math.max(-85.051129, Math.min(85.051129, lat)) * Math.PI) / 180
2396
- );
2397
- const worldX = ((lng + 180) / 360) * worldSize;
2398
- const worldY =
2399
- (0.5 - Math.log((1 + latitudeSine) / (1 - latitudeSine)) / (4 * Math.PI)) *
2400
- worldSize;
2401
- candidates.push({feature, key, lat, lng, worldX, worldY});
2402
- }
2403
- candidates.sort((left, right) =>
2404
- left.key < right.key ? -1 : left.key > right.key ? 1 : 0
2405
- );
2406
- if (lod.densityCellPixels === 0) return candidates;
2407
- const occupiedCells = new Set();
2408
- const selected = [];
2409
- for (const candidate of candidates) {
2410
- const cellKey =
2411
- Math.floor(candidate.worldX / lod.densityCellPixels) +
2412
- ":" +
2413
- Math.floor(candidate.worldY / lod.densityCellPixels);
2414
- if (occupiedCells.has(cellKey)) continue;
2415
- occupiedCells.add(cellKey);
2416
- selected.push(candidate);
2417
- }
2418
- return selected;
2419
- }
2420
-
2421
- function nextTreeInstanceCapacity(requiredCapacity) {
2422
- let capacity = initialTreeInstanceCapacity;
2423
- while (capacity < requiredCapacity) capacity *= 2;
2424
- return capacity;
2425
- }
2426
-
2427
- function createThreeTreeMesh(geometry, capacity) {
2428
- const treeMesh = new THREE.InstancedMesh(geometry, treeMaterial, capacity);
2429
- treeMesh.count = 0;
2430
- treeMesh.frustumCulled = false;
2431
- return treeMesh;
2432
- }
2433
-
2434
- function ensureThreeTreeMeshCapacity(variantIndex, requiredCapacity) {
2435
- if (treeMeshCapacities[variantIndex] >= requiredCapacity) return;
2436
- const currentTreeMesh = treeMeshes[variantIndex];
2437
- const treeMesh = createThreeTreeMesh(
2438
- treeGeometries[variantIndex],
2439
- nextTreeInstanceCapacity(requiredCapacity)
2440
- );
2441
- treeGroup.remove(currentTreeMesh);
2442
- currentTreeMesh.dispose();
2443
- treeMeshes[variantIndex] = treeMesh;
2444
- treeMeshCapacities[variantIndex] = treeMesh.instanceMatrix.count;
2445
- treeGroup.add(treeMesh);
2446
- }
2447
-
2448
- function ensureThreeTreeShadowCapacity(requiredCapacity) {
2449
- if (treeShadowCapacity >= requiredCapacity) return;
2450
- const shadowMesh = new THREE.InstancedMesh(
2451
- treeShadowGeometry,
2452
- treeShadowMaterial,
2453
- nextTreeInstanceCapacity(requiredCapacity)
2454
- );
2455
- shadowMesh.count = 0;
2456
- shadowMesh.frustumCulled = false;
2457
- shadowMesh.renderOrder = 1;
2458
- treeGroup.remove(treeShadowMesh);
2459
- treeShadowMesh.dispose();
2460
- treeShadowMesh = shadowMesh;
2461
- treeShadowCapacity = shadowMesh.instanceMatrix.count;
2462
- treeGroup.add(shadowMesh);
2463
- }
2464
-
2465
- function rememberTreeState(key, state) {
2466
- if (treeStateCache.size >= maximumCachedTrees) {
2467
- treeStateCache.delete(treeStateCache.keys().next().value);
2468
- }
2469
- treeStateCache.set(key, state);
2470
- return state;
2471
- }
2472
-
2473
- const treeFormDimensions = {
2474
- round: {crownMaximum: 0.9, crownMinimum: 0.66, heightMaximum: 12, heightMinimum: 6},
2475
- open: {crownMaximum: 1.05, crownMinimum: 0.8, heightMaximum: 10, heightMinimum: 5},
2476
- tall: {crownMaximum: 0.68, crownMinimum: 0.48, heightMaximum: 17, heightMinimum: 9},
2477
- conifer: {crownMaximum: 0.66, crownMinimum: 0.46, heightMaximum: 16, heightMinimum: 7},
2478
- columnar: {crownMaximum: 0.36, crownMinimum: 0.22, heightMaximum: 18, heightMinimum: 9},
2479
- palm: {crownMaximum: 0.5, crownMinimum: 0.34, heightMaximum: 16, heightMinimum: 8}
2480
- };
2481
-
2482
- function resolveTreeState(feature, key, lng, lat, treeElevation) {
2483
- const cached = treeStateCache.get(key);
2484
- if (cached) return cached;
2485
- const variation = stableUnit(key, 1);
2486
- const form = resolveTreeForm(feature.properties, key);
2487
- const dimensions = treeFormDimensions[form] ?? treeFormDimensions.round;
2488
- const baseHeight = Math.min(
2489
- 32,
2490
- numberProperty(feature.properties, heightField) ??
2491
- dimensions.heightMinimum +
2492
- variation * (dimensions.heightMaximum - dimensions.heightMinimum)
2493
- );
2494
- const baseCrownDiameter = Math.min(
2495
- 20,
2496
- numberProperty(feature.properties, crownField) ??
2497
- baseHeight *
2498
- (dimensions.crownMinimum +
2499
- stableUnit(key, 2) * (dimensions.crownMaximum - dimensions.crownMinimum))
2500
- );
2501
- return rememberTreeState(key, {
2502
- colorGain: 0.94 + stableUnit(key, 13) * 0.12,
2503
- crownAspect: 0.92 + stableUnit(key, 11) * 0.16,
2504
- crownDiameter:
2505
- baseCrownDiameter * crownScale * (0.94 + stableUnit(key, 5) * 0.12),
2506
- form,
2507
- height: baseHeight * heightScale,
2508
- lat,
2509
- lng,
2510
- treeElevation,
2511
- variantIndex: treeVariantIndex(form, key)
2512
- });
2513
- }
2514
-
2515
- function ensureSceneOrigin(elevation) {
2516
- if (sceneOriginMercator) return;
2517
- const center = map.getCenter();
2518
- sceneElevation = elevation;
2519
- sceneOriginMercator = maplibregl.MercatorCoordinate.fromLngLat(
2520
- center,
2521
- sceneElevation
2522
- );
2523
- }
2524
-
2525
- function localTreePosition(lng, lat, elevation) {
2526
- const mercator = maplibregl.MercatorCoordinate.fromLngLat([lng, lat]);
2527
- const mercatorPerMeter = sceneOriginMercator.meterInMercatorCoordinateUnits();
2528
- return new THREE.Vector3(
2529
- (mercator.x - sceneOriginMercator.x) / mercatorPerMeter,
2530
- elevation - sceneElevation,
2531
- (sceneOriginMercator.y - mercator.y) / mercatorPerMeter
2532
- );
2533
- }
2534
-
2535
- function createTerrainSampler(trees, fallbackElevation) {
2536
- const gridSize = 5;
2537
- const longitudes = trees.map((tree) => tree.lng);
2538
- const latitudes = trees.map((tree) => tree.lat);
2539
- const minimumLongitude = Math.min(...longitudes);
2540
- const maximumLongitude = Math.max(...longitudes);
2541
- const minimumLatitude = Math.min(...latitudes);
2542
- const maximumLatitude = Math.max(...latitudes);
2543
- const longitudeSpan = Math.max(1e-9, maximumLongitude - minimumLongitude);
2544
- const latitudeSpan = Math.max(1e-9, maximumLatitude - minimumLatitude);
2545
- const elevations = new Float32Array(gridSize * gridSize);
2546
- for (let y = 0; y < gridSize; y += 1) {
2547
- for (let x = 0; x < gridSize; x += 1) {
2548
- const longitude = minimumLongitude + (x / (gridSize - 1)) * longitudeSpan;
2549
- const latitude = minimumLatitude + (y / (gridSize - 1)) * latitudeSpan;
2550
- elevations[y * gridSize + x] =
2551
- map.queryTerrainElevation([longitude, latitude]) ?? fallbackElevation;
2552
- }
2553
- }
2554
- return (longitude, latitude) => {
2555
- const gridX = Math.max(
2556
- 0,
2557
- Math.min(gridSize - 1, ((longitude - minimumLongitude) / longitudeSpan) * (gridSize - 1))
2558
- );
2559
- const gridY = Math.max(
2560
- 0,
2561
- Math.min(gridSize - 1, ((latitude - minimumLatitude) / latitudeSpan) * (gridSize - 1))
2562
- );
2563
- const x0 = Math.floor(gridX);
2564
- const y0 = Math.floor(gridY);
2565
- const x1 = Math.min(gridSize - 1, x0 + 1);
2566
- const y1 = Math.min(gridSize - 1, y0 + 1);
2567
- const xMix = gridX - x0;
2568
- const yMix = gridY - y0;
2569
- const bottom = elevations[y0 * gridSize + x0] * (1 - xMix) +
2570
- elevations[y0 * gridSize + x1] * xMix;
2571
- const top = elevations[y1 * gridSize + x0] * (1 - xMix) +
2572
- elevations[y1 * gridSize + x1] * xMix;
2573
- return bottom * (1 - yMix) + top * yMix;
2574
- };
2575
- }
2576
-
2577
- function botanicalName(properties) {
2578
- return [properties?.[genusField], properties?.[speciesField]]
2579
- .filter(Boolean)
2580
- .join(" ")
2581
- .toLowerCase();
2582
- }
2583
-
2584
- function resolveTreeForm(properties, key) {
2585
- const leafType = String(properties?.[leafTypeField] ?? "").toLowerCase();
2586
- const botanical = botanicalName(properties);
2587
- if (
2588
- leafType.includes("palm") ||
2589
- /(?:^| )(?:archontophoenix|arecaceae|butia|chamaerops|cocos|howea|livistona|phoenix|sabal|syagrus|trachycarpus|washingtonia)(?: |$)/.test(
2590
- botanical
2591
- )
2592
- ) return "palm";
2593
- if (/(?:^| )(?:cupressus|cypress)(?: |$)/.test(botanical)) return "columnar";
2594
- if (
2595
- leafType.includes("needle") ||
2596
- /(?:^| )(?:abies|cedrus|juniperus|picea|pinus|taxodium|taxus)(?: |$)/.test(botanical)
2597
- ) return "conifer";
2598
- if (
2599
- /(?:^| )(?:arbutus|olea|quercus|tamarix)(?: |$)/.test(botanical)
2600
- ) return "open";
2601
- if (
2602
- /(?:^| )(?:celtis|ginkgo|platanus|populus|ulmus)(?: |$)/.test(botanical)
2603
- ) return "tall";
2604
- return ["round", "open", "tall"][Math.floor(stableUnit(key, 67) * 3)];
2605
- }
2606
-
2607
- function treeVariantIndex(form, key) {
2608
- const alternate = Math.floor(stableUnit(key, 101) * 2);
2609
- const base = {
2610
- round: 0,
2611
- open: 2,
2612
- tall: 4,
2613
- conifer: 6,
2614
- columnar: 7,
2615
- palm: 8
2616
- }[form] ?? 0;
2617
- return base + (["round", "open", "tall", "palm"].includes(form) ? alternate : 0);
2618
- }
2619
-
2620
- function refresh() {
2621
- const refreshStartedAt = performance.now();
2622
- refreshQueued = false;
2623
- lastRefreshTime = performance.now();
2624
- if (
2625
- activeTreeBackend === "three"
2626
- ? !treeGroup || treeMeshes.length === 0
2627
- : rawTreeResources.length === 0
2628
- ) return;
2629
- if (!treesEnabled) {
2630
- if (treeGroup) treeGroup.visible = false;
2631
- rawTreesVisible = false;
2632
- setFallbackVisible(false);
2633
- return;
2634
- }
2635
- if (map.getZoom() < 15.5) {
2636
- for (const treeMesh of treeMeshes) treeMesh.count = 0;
2637
- for (const resource of rawTreeResources) resource.count = 0;
2638
- if (treeShadowMesh) treeShadowMesh.count = 0;
2639
- if (rawShadowResource) rawShadowResource.count = 0;
2640
- if (treeGroup) treeGroup.visible = false;
2641
- rawTreesVisible = false;
2642
- renderedTreeCount = 0;
2643
- treeMetrics.treeCount = 0;
2644
- treeMetrics.refreshMilliseconds = performance.now() - refreshStartedAt;
2645
- lastTreeSelectionSignature = undefined;
2646
- setFallbackVisible(true);
2647
- updateTreeStatus("zoom too low", {phase: "refresh", treeCount: 0});
2648
- map.triggerRepaint();
2649
- return;
2650
- }
2651
- let features;
2652
- const queryStartedAt = performance.now();
2653
- try {
2654
- const canvas = map.getCanvas();
2655
- const viewportWidth = canvas.clientWidth || canvas.width;
2656
- const viewportHeight = canvas.clientHeight || canvas.height;
2657
- features = map.queryRenderedFeatures(
2658
- [
2659
- [-viewportBufferPixels, -viewportBufferPixels],
2660
- [
2661
- viewportWidth + viewportBufferPixels,
2662
- viewportHeight + viewportBufferPixels
2663
- ]
2664
- ],
2665
- {layers: [styleLayer.id]}
2666
- );
2667
- } catch (error) {
2668
- updateTreeStatus("error reading source", {
2669
- error: String(error),
2670
- phase: "refresh",
2671
- treeCount: 0
2672
- });
2673
- return;
2674
- }
2675
- treeMetrics.queryMilliseconds = performance.now() - queryStartedAt;
2676
- sourceSettled = map.isSourceLoaded(sourceId);
2677
- const lod = treeLodForZoom(map.getZoom());
2678
- const selectionStartedAt = performance.now();
2679
- const visibleTrees = selectVisibleTrees(features, lod);
2680
- treeMetrics.selectionMilliseconds = performance.now() - selectionStartedAt;
2681
- if (visibleTrees.length === 0) {
2682
- updateFallbackVisibility();
2683
- if (emptyRefreshAttempts < 12) {
2684
- emptyRefreshAttempts += 1;
2685
- clearTimeout(emptyRefreshTimer);
2686
- emptyRefreshTimer = setTimeout(queueRefresh, 150);
2687
- }
2688
- updateTreeStatus("waiting for tiles", {
2689
- lod: lod.name,
2690
- phase: "refresh",
2691
- sourceFeatureCount: features.length,
2692
- sourceLoaded: sourceSettled,
2693
- treeCount: renderedTreeCount
2694
- });
2695
- map.triggerRepaint();
2696
- return;
2697
- }
2698
- const selectionSignature = treeSelectionSignature(visibleTrees, lod.name);
2699
- if (sourceSettled && selectionSignature === lastTreeSelectionSignature) {
2700
- if (treeGroup) treeGroup.visible = true;
2701
- rawTreesVisible = true;
2702
- treeMetrics.refreshMilliseconds = performance.now() - refreshStartedAt;
2703
- updateFallbackVisibility();
2704
- map.triggerRepaint();
2705
- return;
2706
- }
2707
- // The viewport elevation is only a fallback. Each tree uses its own
2708
- // terrain sample so sloped ground cannot clip different parts of the
2709
- // crown as the camera center moves.
2710
- const viewportElevation = map.queryTerrainElevation(map.getCenter()) ?? 0;
2711
- ensureSceneOrigin(viewportElevation);
2712
- const terrainStartedAt = performance.now();
2713
- const terrainElevationAt = createTerrainSampler(visibleTrees, viewportElevation);
2714
- treeMetrics.terrainMilliseconds = performance.now() - terrainStartedAt;
2715
- const buildStartedAt = performance.now();
2716
- let count = 0;
2717
- const variantCount = activeTreeBackend === "three"
2718
- ? treeMeshes.length
2719
- : rawTreeResources.length;
2720
- const variantCounts = Array.from({length: variantCount}, () => 0);
2721
- const rawInstanceValues = Array.from({length: variantCount}, () => []);
2722
- const rawShadowInstanceValues = [];
2723
- for (const {feature, key, lat, lng} of visibleTrees) {
2724
- const treeState = resolveTreeState(
2725
- feature,
2726
- key,
2727
- lng,
2728
- lat,
2729
- terrainElevationAt(lng, lat)
2730
- );
2731
- const {
2732
- colorGain,
2733
- crownAspect,
2734
- crownDiameter,
2735
- height,
2736
- lat: treeLat,
2737
- lng: treeLng,
2738
- treeElevation,
2739
- variantIndex
2740
- } = treeState;
2741
- const treePosition = localTreePosition(treeLng, treeLat, treeElevation);
2742
- const rotation = stableUnit(key, 3) * Math.PI * 2;
2743
- const crownWidth = crownDiameter * crownAspect;
2744
- const crownDepth = crownDiameter / crownAspect;
2745
- const shadowRadius = Math.max(0.75, Math.min(5.5, crownDiameter * 0.42));
2746
- if (activeTreeBackend === "three") {
2747
- ensureThreeTreeMeshCapacity(
2748
- variantIndex,
2749
- variantCounts[variantIndex] + 1
2750
- );
2751
- ensureThreeTreeShadowCapacity(count + 1);
2752
- crownRotation.setFromAxisAngle(yAxis, rotation);
2753
- position.copy(treePosition);
2754
- scale.set(crownWidth, height, crownDepth);
2755
- matrix.compose(position, crownRotation, scale);
2756
- const treeMesh = treeMeshes[variantIndex];
2757
- treeMesh.setMatrixAt(variantCounts[variantIndex], matrix);
2758
- instanceTint.setRGB(colorGain, colorGain, colorGain);
2759
- treeMesh.setColorAt(variantCounts[variantIndex], instanceTint);
2760
- shadowPosition.copy(treePosition);
2761
- shadowPosition.x -= shadowRadius * 0.28;
2762
- shadowPosition.y += treeShadowLiftMeters;
2763
- shadowPosition.z += shadowRadius * 0.34;
2764
- shadowScale.set(shadowRadius, 1, shadowRadius * 0.76);
2765
- shadowMatrix.compose(shadowPosition, shadowRotation, shadowScale);
2766
- treeShadowMesh.setMatrixAt(count, shadowMatrix);
2767
- } else {
2768
- rawInstanceValues[variantIndex].push(
2769
- treePosition.x,
2770
- treePosition.y,
2771
- treePosition.z,
2772
- crownWidth,
2773
- height,
2774
- crownDepth,
2775
- Math.cos(rotation),
2776
- Math.sin(rotation),
2777
- colorGain
2778
- );
2779
- rawShadowInstanceValues.push(
2780
- treePosition.x,
2781
- treePosition.y,
2782
- treePosition.z,
2783
- shadowRadius
2784
- );
2785
- }
2786
- variantCounts[variantIndex] += 1;
2787
- count += 1;
2788
- }
2789
- if (activeTreeBackend === "three") {
2790
- for (let variantIndex = 0; variantIndex < treeMeshes.length; variantIndex += 1) {
2791
- const treeMesh = treeMeshes[variantIndex];
2792
- treeMesh.count = variantCounts[variantIndex];
2793
- treeMesh.instanceMatrix.needsUpdate = true;
2794
- if (treeMesh.instanceColor) treeMesh.instanceColor.needsUpdate = true;
2795
- }
2796
- treeShadowMesh.count = count;
2797
- treeShadowMesh.instanceMatrix.needsUpdate = true;
2798
- } else {
2799
- for (let variantIndex = 0; variantIndex < rawTreeResources.length; variantIndex += 1) {
2800
- const resource = rawTreeResources[variantIndex];
2801
- resource.count = variantCounts[variantIndex];
2802
- rawGl.bindBuffer(rawGl.ARRAY_BUFFER, resource.instanceBuffer);
2803
- rawGl.bufferData(
2804
- rawGl.ARRAY_BUFFER,
2805
- new Float32Array(rawInstanceValues[variantIndex]),
2806
- rawGl.DYNAMIC_DRAW
2807
- );
2808
- }
2809
- rawShadowResource.count = count;
2810
- rawGl.bindBuffer(rawGl.ARRAY_BUFFER, rawShadowResource.instanceBuffer);
2811
- rawGl.bufferData(
2812
- rawGl.ARRAY_BUFFER,
2813
- new Float32Array(rawShadowInstanceValues),
2814
- rawGl.DYNAMIC_DRAW
2815
- );
2816
- rawGl.bindBuffer(rawGl.ARRAY_BUFFER, null);
2817
- }
2818
- treeMetrics.buildMilliseconds = performance.now() - buildStartedAt;
2819
- if (treeGroup) treeGroup.visible = true;
2820
- rawTreesVisible = true;
2821
- renderedTreeCount = count;
2822
- treeMetrics.treeCount = count;
2823
- treeMetrics.refreshMilliseconds = performance.now() - refreshStartedAt;
2824
- lastTreeSelectionSignature = selectionSignature;
2825
- emptyRefreshAttempts = 0;
2826
- clearTimeout(emptyRefreshTimer);
2827
- emptyRefreshTimer = undefined;
2828
- updateTreeStatus(count + " 3D trees", {
2829
- lod: lod.name,
2830
- phase: "refresh",
2831
- renderer: treeRendererMode,
2832
- sourceFeatureCount: features.length,
2833
- sourceLoaded: sourceSettled,
2834
- treeCount: count
2835
- });
2836
- updateFallbackVisibility();
2837
- if (!sourceSettled) {
2838
- clearTimeout(emptyRefreshTimer);
2839
- emptyRefreshTimer = setTimeout(() => queueRefresh(true), 150);
2840
- }
2841
- map.triggerRepaint();
2842
- }
2843
-
2844
- function queueRefresh(immediate = false) {
2845
- if (refreshQueued) return;
2846
- if (immediate && refreshTimer !== undefined) {
2847
- clearTimeout(refreshTimer);
2848
- refreshTimer = undefined;
2849
- }
2850
- if (!immediate) {
2851
- if (refreshTimer !== undefined) return;
2852
- const elapsed = performance.now() - lastRefreshTime;
2853
- const delay = Math.max(0, sourceRefreshIntervalMilliseconds - elapsed);
2854
- if (delay > 0) {
2855
- refreshTimer = setTimeout(() => {
2856
- refreshTimer = undefined;
2857
- queueRefresh(true);
2858
- }, delay);
2859
- return;
2860
- }
2861
- }
2862
- refreshQueued = true;
2863
- requestAnimationFrame(refresh);
2864
- }
2865
-
2866
- function handleMoveStart() {
2867
- if (!treesEnabled) return;
2868
- sourceSettled = false;
2869
- updateFallbackVisibility();
2870
- }
2871
-
2872
- function handleMoveEnd() {
2873
- if (!treesEnabled) return;
2874
- emptyRefreshAttempts = 0;
2875
- sourceSettled = map.isSourceLoaded(sourceId);
2876
- updateFallbackVisibility();
2877
- queueRefresh(true);
2878
- }
2879
-
2880
- function handleSourceLoading(event) {
2881
- if (!treesEnabled || event.sourceId !== sourceId) return;
2882
- sourceSettled = false;
2883
- updateFallbackVisibility();
2884
- }
2885
-
2886
- function handleSourceData(event) {
2887
- if (!treesEnabled || event.sourceId !== sourceId) return;
2888
- sourceSettled = map.isSourceLoaded(sourceId);
2889
- if (map.isMoving()) return;
2890
- queueRefresh();
2891
- }
2892
-
2893
- function handleTreesToggle() {
2894
- clearTimeout(emptyRefreshTimer);
2895
- clearTimeout(refreshTimer);
2896
- emptyRefreshTimer = undefined;
2897
- refreshTimer = undefined;
2898
- if (!treesEnabled) {
2899
- if (treeGroup) treeGroup.visible = false;
2900
- rawTreesVisible = false;
2901
- setFallbackVisible(false);
2902
- updateTreeStatus("disabled", {phase: "toggle", treeCount: renderedTreeCount});
2903
- map.triggerRepaint();
2904
- return;
2905
- }
2906
- if (treeGroup) treeGroup.visible = renderedTreeCount > 0;
2907
- rawTreesVisible = renderedTreeCount > 0;
2908
- updateFallbackVisibility();
2909
- updateTreeStatus(
2910
- renderedTreeCount > 0 ? renderedTreeCount + " 3D trees" : "enabling",
2911
- {phase: "toggle", treeCount: renderedTreeCount}
2912
- );
2913
- queueRefresh(true);
2914
- map.triggerRepaint();
2915
- }
2916
-
2917
- return {
2918
- id: "tileflow-vegetation-trees-3d",
2919
- type: "custom",
2920
- renderingMode: "3d",
2921
- onAdd(_map, gl) {
2922
- updateTreeStatus("layer mounted", {phase: "onAdd", treeCount: 0});
2923
- const geometries = createTreeGeometries();
2924
- if (
2925
- activeTreeBackend === "webgl2" &&
2926
- typeof gl.createVertexArray === "function" &&
2927
- typeof gl.drawArraysInstanced === "function"
2928
- ) {
2929
- rawGl = gl;
2930
- rawProgram = createRawTreeProgram(gl);
2931
- rawMatrixUniform = gl.getUniformLocation(rawProgram, "u_matrix");
2932
- rawShadowProgram = createRawTreeShadowProgram(gl);
2933
- rawShadowMatrixUniform = gl.getUniformLocation(rawShadowProgram, "u_matrix");
2934
- rawShadowOpacityUniform = gl.getUniformLocation(rawShadowProgram, "u_opacity");
2935
- rawShadowResource = createRawTreeShadowResource(gl);
2936
- rawTreeResources = geometries.map((geometry) =>
2937
- createRawTreeResource(gl, geometry)
2938
- );
2939
- for (const geometry of geometries) geometry.dispose();
2940
- } else {
2941
- activeTreeBackend = "three";
2942
- treeMetrics.backend = activeTreeBackend;
2943
- treeGeometries = geometries;
2944
- camera = new THREE.Camera();
2945
- scene = new THREE.Scene();
2946
- scene.rotateX(Math.PI / 2);
2947
- scene.scale.multiply(new THREE.Vector3(1, 1, -1));
2948
- scene.add(new THREE.HemisphereLight(0xf4fff1, 0x808878, 1.35));
2949
- const sunlight = new THREE.DirectionalLight(0xfff3d8, 0.88);
2950
- sunlight.position.set(-2, -3, 6);
2951
- scene.add(sunlight);
2952
- treeGroup = new THREE.Group();
2953
- treeMaterial = new THREE.MeshLambertMaterial({
2954
- flatShading: false,
2955
- vertexColors: true
2956
- });
2957
- treeMeshes = geometries.map((geometry) =>
2958
- createThreeTreeMesh(geometry, initialTreeInstanceCapacity)
2959
- );
2960
- treeMeshCapacities = geometries.map(() => initialTreeInstanceCapacity);
2961
- treeShadowGeometry = new THREE.PlaneGeometry(2, 2);
2962
- treeShadowGeometry.rotateX(-Math.PI / 2);
2963
- treeShadowMaterial = new THREE.ShaderMaterial({
2964
- depthTest: true,
2965
- depthWrite: false,
2966
- fragmentShader:
2967
- "varying vec2 v_shadow_coordinate;\\n" +
2968
- "uniform float u_opacity;\\n" +
2969
- "void main() {\\n" +
2970
- " float radiusSquared = dot(v_shadow_coordinate, v_shadow_coordinate);\\n" +
2971
- " if (radiusSquared >= 1.0) discard;\\n" +
2972
- " float coverage = 1.0 - smoothstep(0.90, 1.0, radiusSquared);\\n" +
2973
- " gl_FragColor = vec4(0.18, 0.21, 0.17, u_opacity * coverage);\\n" +
2974
- "}\\n",
2975
- side: THREE.DoubleSide,
2976
- toneMapped: false,
2977
- transparent: true,
2978
- uniforms: {u_opacity: {value: treeShadowOpacity}},
2979
- vertexShader:
2980
- "varying vec2 v_shadow_coordinate;\\n" +
2981
- "void main() {\\n" +
2982
- " v_shadow_coordinate = uv * 2.0 - 1.0;\\n" +
2983
- " vec4 instancePosition = instanceMatrix * vec4(position, 1.0);\\n" +
2984
- " gl_Position = projectionMatrix * modelViewMatrix * instancePosition;\\n" +
2985
- "}\\n"
2986
- });
2987
- treeShadowMesh = new THREE.InstancedMesh(
2988
- treeShadowGeometry,
2989
- treeShadowMaterial,
2990
- initialTreeInstanceCapacity
2991
- );
2992
- treeShadowCapacity = initialTreeInstanceCapacity;
2993
- treeShadowMesh.count = 0;
2994
- treeShadowMesh.frustumCulled = false;
2995
- treeShadowMesh.renderOrder = 1;
2996
- treeGroup.visible = false;
2997
- treeGroup.add(...treeMeshes, treeShadowMesh);
2998
- scene.add(treeGroup);
2999
- renderer = new THREE.WebGLRenderer({
3000
- canvas: map.getCanvas(),
3001
- context: gl
3002
- });
3003
- renderer.autoClear = false;
3004
- }
3005
- sourceSettled = map.isSourceLoaded(sourceId);
3006
- map.on("movestart", handleMoveStart);
3007
- map.on("moveend", handleMoveEnd);
3008
- map.on("sourcedataloading", handleSourceLoading);
3009
- map.on("sourcedata", handleSourceData);
3010
- map.on("tileflow:trees-toggle", handleTreesToggle);
3011
- if (treesEnabled) queueRefresh(true);
3012
- else handleTreesToggle();
3013
- },
3014
- render(_gl, options) {
3015
- if (
3016
- !treesEnabled ||
3017
- !sceneOriginMercator ||
3018
- (activeTreeBackend === "three" ? !treeGroup?.visible : !rawTreesVisible)
3019
- ) return;
3020
- const renderStartedAt = performance.now();
3021
- const meter = sceneOriginMercator.meterInMercatorCoordinateUnits();
3022
- mapMatrix.fromArray(options.defaultProjectionData.mainMatrix);
3023
- sceneMatrix
3024
- .makeTranslation(
3025
- sceneOriginMercator.x,
3026
- sceneOriginMercator.y,
3027
- sceneOriginMercator.z
3028
- )
3029
- .scale(new THREE.Vector3(meter, -meter, meter));
3030
- combinedMatrix.copy(mapMatrix).multiply(sceneMatrix);
3031
- if (activeTreeBackend === "three") {
3032
- camera.projectionMatrix.copy(combinedMatrix);
3033
- renderer.resetState();
3034
- renderer.render(scene, camera);
3035
- treeMetrics.renderCalls = renderer.info.render.calls;
3036
- treeMetrics.renderedTriangles = renderer.info.render.triangles;
3037
- } else {
3038
- rawGl.useProgram(rawProgram);
3039
- rawGl.uniformMatrix4fv(rawMatrixUniform, false, combinedMatrix.elements);
3040
- rawGl.enable(rawGl.DEPTH_TEST);
3041
- rawGl.depthFunc(rawGl.LEQUAL);
3042
- rawGl.depthMask(true);
3043
- rawGl.enable(rawGl.CULL_FACE);
3044
- rawGl.cullFace(rawGl.BACK);
3045
- rawGl.frontFace(rawGl.CCW);
3046
- rawGl.disable(rawGl.BLEND);
3047
- let renderCalls = 0;
3048
- let renderedTriangles = 0;
3049
- for (const resource of rawTreeResources) {
3050
- if (resource.count === 0) continue;
3051
- rawGl.bindVertexArray(resource.vertexArray);
3052
- rawGl.drawElementsInstanced(
3053
- rawGl.TRIANGLES,
3054
- resource.indexCount,
3055
- rawGl.UNSIGNED_INT,
3056
- 0,
3057
- resource.count
3058
- );
3059
- renderCalls += 1;
3060
- renderedTriangles += (resource.indexCount / 3) * resource.count;
3061
- }
3062
- rawGl.bindVertexArray(null);
3063
- if (rawShadowResource.count > 0) {
3064
- rawGl.useProgram(rawShadowProgram);
3065
- rawGl.uniformMatrix4fv(
3066
- rawShadowMatrixUniform,
3067
- false,
3068
- combinedMatrix.elements
3069
- );
3070
- rawGl.uniform1f(rawShadowOpacityUniform, treeShadowOpacity);
3071
- rawGl.depthMask(false);
3072
- rawGl.disable(rawGl.CULL_FACE);
3073
- rawGl.enable(rawGl.BLEND);
3074
- rawGl.blendEquation(rawGl.FUNC_ADD);
3075
- rawGl.blendFuncSeparate(
3076
- rawGl.SRC_ALPHA,
3077
- rawGl.ONE_MINUS_SRC_ALPHA,
3078
- rawGl.ONE,
3079
- rawGl.ONE_MINUS_SRC_ALPHA
3080
- );
3081
- rawGl.bindVertexArray(rawShadowResource.vertexArray);
3082
- rawGl.drawArraysInstanced(
3083
- rawGl.TRIANGLE_FAN,
3084
- 0,
3085
- 4,
3086
- rawShadowResource.count
3087
- );
3088
- rawGl.bindVertexArray(null);
3089
- rawGl.depthMask(true);
3090
- rawGl.disable(rawGl.BLEND);
3091
- rawGl.enable(rawGl.CULL_FACE);
3092
- renderCalls += 1;
3093
- renderedTriangles += rawShadowResource.count * 2;
3094
- }
3095
- treeMetrics.renderCalls = renderCalls;
3096
- treeMetrics.renderedTriangles = renderedTriangles;
3097
- }
3098
- treeMetrics.renderMilliseconds = performance.now() - renderStartedAt;
3099
- },
3100
- onRemove() {
3101
- clearTimeout(emptyRefreshTimer);
3102
- clearTimeout(refreshTimer);
3103
- map.off("movestart", handleMoveStart);
3104
- map.off("moveend", handleMoveEnd);
3105
- map.off("sourcedataloading", handleSourceLoading);
3106
- map.off("sourcedata", handleSourceData);
3107
- map.off("tileflow:trees-toggle", handleTreesToggle);
3108
- if (!fallbackVisible && map.getLayer(styleLayer.id)) {
3109
- map.setPaintProperty(
3110
- styleLayer.id,
3111
- "circle-opacity",
3112
- fallbackCircleOpacity
3113
- );
3114
- map.setPaintProperty(
3115
- styleLayer.id,
3116
- "circle-stroke-opacity",
3117
- fallbackCircleStrokeOpacity
3118
- );
3119
- }
3120
- for (const treeMesh of treeMeshes) {
3121
- treeMesh.dispose();
3122
- treeMesh.geometry.dispose();
3123
- }
3124
- treeMeshes = [];
3125
- treeMeshCapacities = [];
3126
- treeGeometries = [];
3127
- treeShadowMesh?.dispose();
3128
- treeShadowMesh?.geometry.dispose();
3129
- treeShadowMesh = undefined;
3130
- treeShadowGeometry = undefined;
3131
- treeShadowCapacity = 0;
3132
- treeShadowMaterial?.dispose();
3133
- treeShadowMaterial = undefined;
3134
- treeMaterial?.dispose();
3135
- renderer?.dispose();
3136
- disposeRawTreeResources();
3137
- }
3138
- };
3139
- }
3140
-
3141
- function addTreeLayerIfConfigured(map, styleLayers) {
3142
- const styleLayer = styleLayers.find(
3143
- (layer) => layer.id === "tileflow-vegetation-trees"
3144
- );
3145
- if (
3146
- !styleLayer ||
3147
- styleLayer.metadata?.["tileflow:vegetation-mode"] !== "3d" ||
3148
- map.getLayer("tileflow-vegetation-trees-3d")
3149
- ) return;
3150
- if (treeRendererMode === "circle") {
3151
- treeMetrics.treeCount = 0;
3152
- updateTreeStatus("native circles", {
3153
- phase: "configured",
3154
- renderer: treeRendererMode,
3155
- treeCount: 0
3156
- });
3157
- return;
3158
- }
3159
- updateTreeStatus("configured", {phase: "configured", treeCount: 0});
3160
- // Three.js consumes Mercator world coordinates directly. MapLibre's
3161
- // plain custom-layer matrix only supports those coordinates in the
3162
- // Mercator projection; globe requires a projection-aware shader.
3163
- if (map.getProjection().type !== "mercator") {
3164
- map.setProjection({type: "mercator"});
3165
- }
3166
- if (map.getLayer("tileflow-vegetation-trees-3d")) return;
3167
- const styleLayerIndex = styleLayers.findIndex((layer) => layer.id === styleLayer.id);
3168
- const layerAboveTrees = styleLayers[styleLayerIndex + 1]?.id;
3169
- map.addLayer(createTreeLayer(map, styleLayer), layerAboveTrees);
3170
- }
3171
-
3172
1468
  function styleLayerZoomRange(layer) {
3173
1469
  return {
3174
1470
  minzoom: Number.isFinite(layer?.minzoom) ? layer.minzoom : 0,
@@ -3668,8 +1964,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3668
1964
  spatialFallbacks:
3669
1965
  layer.metadata?.["tileflow:landmark-spatial-fallbacks"] ?? []
3670
1966
  }));
3671
- // Landmark custom layers use Mercator world coordinates. Switch here
3672
- // instead of relying on the optional detailed-tree renderer to do it.
1967
+ // Landmark custom layers use Mercator world coordinates.
3673
1968
  if (map.getProjection().type !== "mercator") {
3674
1969
  map.setProjection({type: "mercator"});
3675
1970
  }
@@ -3677,16 +1972,94 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3677
1972
  (layer) =>
3678
1973
  (layer.id.startsWith("tileflow-label-") &&
3679
1974
  !layer.id.startsWith("tileflow-label-road-")) ||
1975
+ layer.id === "tileflow-poi" ||
3680
1976
  layer.id.startsWith("tileflow-poi-")
3681
1977
  )?.id;
3682
- const treeLayer = map.getLayer("tileflow-vegetation-trees-3d")
3683
- ? "tileflow-vegetation-trees-3d"
3684
- : map.getLayer("tileflow-vegetation-trees")
3685
- ? "tileflow-vegetation-trees"
3686
- : firstForegroundLabel;
1978
+ const treeLayer = map.getLayer("tileflow-vegetation-trees")
1979
+ ? "tileflow-vegetation-trees"
1980
+ : firstForegroundLabel;
3687
1981
  map.addLayer(createLandmarkLayer(map, configLayer, fallbackLayers), treeLayer);
3688
1982
  }
3689
1983
 
1984
+ // Core's 3D layers, as an application mounts them: the map's style describes how its
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
1987
+ // time, so the style on screen holds the sun and the design.
1988
+ const coreScene = {buildings: undefined, trees: undefined, lights: undefined, buildingsShown: false, animation: "0"};
1989
+ const runtimeLayerVisibility = {};
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
+ });
2004
+ function coreSceneThemes(map) {
2005
+ return {
2006
+ getSun() {
2007
+ const preset = readTileflowSunPreset(map.getStyle()?.metadata);
2008
+ return resolveTileflowSun(preset ?? tileflowDefaultSunPreset);
2009
+ },
2010
+ getThemeMetadata(key) {
2011
+ return {values: [map.getStyle()?.metadata?.[key]], position: 0};
2012
+ }
2013
+ };
2014
+ }
2015
+ let mountingCoreScene = false;
2016
+ function mountCoreScene(map) {
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;}
2061
+ }
2062
+
3690
2063
  class ThreeDimensionalControl {
3691
2064
  constructor() {
3692
2065
  this.enabled = threeDimensionalEnabled;
@@ -3722,6 +2095,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3722
2095
  this.update();
3723
2096
  writeToggleStateToUrl();
3724
2097
  this.map.fire("tileflow:3d-toggle", {enabled: this.enabled});
2098
+ mountCoreScene(this.map);
3725
2099
  this.map.triggerRepaint();
3726
2100
  }
3727
2101
 
@@ -3729,11 +2103,14 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3729
2103
  for (const layer of this.map.getStyle()?.layers || []) {
3730
2104
  const toggle = layer.metadata?.["tileflow:3d-toggle"];
3731
2105
  if (toggle !== "building" && toggle !== "landmark") continue;
3732
- const visibility = this.enabled ? "visible" : "none";
2106
+ // Core's 3D buildings stand in for the compiled extrusion where the style describes them.
2107
+ const replaced = toggle === "building" && this.map.getStyle()?.metadata?.["tileflow:buildings"];
2108
+ const visibility = this.enabled && !replaced ? "visible" : "none";
3733
2109
  if (this.map.getLayoutProperty(layer.id, "visibility") !== visibility) {
3734
2110
  this.map.setLayoutProperty(layer.id, "visibility", visibility);
3735
2111
  }
3736
2112
  }
2113
+ if (this.map.isStyleLoaded()) mountCoreScene(this.map);
3737
2114
  this.button.textContent = this.enabled ? "3D ON" : "3D OFF";
3738
2115
  this.button.setAttribute("aria-pressed", String(this.enabled));
3739
2116
  this.button.title = this.enabled ? "Disable 3D buildings" : "Enable 3D buildings";
@@ -3780,22 +2157,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3780
2157
  this.update();
3781
2158
  writeToggleStateToUrl();
3782
2159
  this.map.fire("tileflow:trees-toggle", {enabled: this.enabled});
2160
+ mountCoreScene(this.map);
3783
2161
  this.map.triggerRepaint();
3784
2162
  }
3785
2163
 
3786
2164
  update() {
3787
- const treeLayer = this.map.getStyle()?.layers?.find(
3788
- (layer) => layer.id === "tileflow-vegetation-trees"
3789
- );
3790
- if (treeLayer) {
3791
- const visibility = this.enabled ? "visible" : "none";
3792
- if (this.map.getLayoutProperty(treeLayer.id, "visibility") !== visibility) {
3793
- this.map.setLayoutProperty(treeLayer.id, "visibility", visibility);
3794
- }
3795
- }
2165
+ if (this.map.isStyleLoaded()) mountCoreScene(this.map);
2166
+ this.enabled = treesEnabled;
3796
2167
  this.button.textContent = this.enabled ? "TREES ON" : "TREES OFF";
3797
2168
  this.button.setAttribute("aria-pressed", String(this.enabled));
3798
- this.button.title = this.enabled ? "Hide trees" : "Show trees";
2169
+ this.button.title = this.enabled ? "Hide vegetation" : "Show vegetation";
3799
2170
  }
3800
2171
 
3801
2172
  onRemove() {
@@ -3891,24 +2262,21 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3891
2262
  ...visibleLayerGroups,
3892
2263
  ].join(",");
3893
2264
  }
2265
+ const vegetationLayers = map.getStyle()?.metadata?.["tileflow:vegetation-display"]?.flatLayers;
3894
2266
  for (const layer of map.getStyle()?.layers || []) {
3895
- if (!defaultLayerVisibility.has(layer.id)) {
3896
- defaultLayerVisibility.set(
3897
- layer.id,
3898
- map.getLayoutProperty(layer.id, "visibility") || "visible"
3899
- );
3900
- }
3901
2267
  if (layer.type === "symbol" && layer.layout?.["text-field"] !== undefined) {
3902
- if (!defaultLayerTextField.has(layer.id)) {
2268
+ const hideLabels = !visibleLayerGroups.has("labels");
2269
+ if (hideLabels && !defaultLayerTextField.has(layer.id)) {
3903
2270
  defaultLayerTextField.set(
3904
2271
  layer.id,
3905
2272
  map.getLayoutProperty(layer.id, "text-field")
3906
2273
  );
3907
2274
  }
3908
- const textField = visibleLayerGroups.has("labels")
3909
- ? defaultLayerTextField.get(layer.id)
3910
- : "";
2275
+ const restoreText = !hideLabels && defaultLayerTextField.has(layer.id);
2276
+ const textField = hideLabels ? "" : defaultLayerTextField.get(layer.id);
2277
+ if (restoreText) defaultLayerTextField.delete(layer.id);
3911
2278
  if (
2279
+ (hideLabels || restoreText) &&
3912
2280
  !previewLayoutValuesEqual(
3913
2281
  map.getLayoutProperty(layer.id, "text-field"),
3914
2282
  textField
@@ -3917,14 +2285,29 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3917
2285
  map.setLayoutProperty(layer.id, "text-field", textField);
3918
2286
  }
3919
2287
  }
2288
+ // The shared vegetation controller owns every flat representation.
2289
+ if (vegetationLayers?.some((entry) => entry.id === layer.id.split("::")[0])) continue;
3920
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;
3921
2298
  const threeDimensionalToggle = layer.metadata?.["tileflow:3d-toggle"];
3922
2299
  const authoredVisibility = defaultLayerVisibility.get(layer.id);
3923
- const toggleVisibility =
3924
- threeDimensionalToggle === "building" || threeDimensionalToggle === "landmark"
2300
+ if (!hideLayer) defaultLayerVisibility.delete(layer.id);
2301
+ const styleMetadata = map.getStyle()?.metadata ?? {};
2302
+ // Core's 3D buildings and trees stand in for compiled extrusions and flat trees.
2303
+ const replacedByCore =
2304
+ threeDimensionalToggle === "building" && styleMetadata["tileflow:buildings"];
2305
+ const toggleVisibility = replacedByCore
2306
+ ? "none"
2307
+ : threeDimensionalToggle === "building" || threeDimensionalToggle === "landmark"
3925
2308
  ? (threeDimensionalEnabled ? "visible" : "none")
3926
2309
  : authoredVisibility;
3927
- const visibility = groups.some((group) => !visibleLayerGroups.has(group))
2310
+ const visibility = hideLayer
3928
2311
  ? "none"
3929
2312
  : toggleVisibility;
3930
2313
  if (map.getLayoutProperty(layer.id, "visibility") !== visibility) {
@@ -3939,6 +2322,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3939
2322
  map.fire("tileflow:visible-layer-groups", {groups: [...visibleLayerGroups]});
3940
2323
  }
3941
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
+
3942
2335
  if (styleUrl) {
3943
2336
  // Dense vector tiles finish in bursts with multiple workers, causing several
3944
2337
  // bucket uploads in one frame. One worker keeps navigation smooth; benchmarks
@@ -3949,23 +2342,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3949
2342
  registerTileflowContourProtocol({addProtocol: maplibregl.addProtocol});
3950
2343
  registerTileflowPmtilesProtocol({addProtocol: maplibregl.addProtocol});
3951
2344
  const map = new maplibregl.Map({
3952
- collectResourceTiming: mapBenchmarkEnabled,
2345
+ collectResourceTiming: mapBenchmarkEnabled || Boolean(devtoolsObserver),
3953
2346
  container: "map",
3954
2347
  style: styleUrl,
3955
- transformRequest: (url) => {
3956
- try {
3957
- return new URL(url, location.href).hostname === "dev-tiles.tileflow.dev"
3958
- ? {credentials: "include", url}
3959
- : {url};
3960
- } catch {
3961
- return {url};
3962
- }
3963
- },
2348
+ transformRequest: previewTransformRequest,
3964
2349
  ...resolveInitialMapOptions(previewMapOptions)
3965
2350
  });
3966
2351
  globalThis.__tileflowPreviewMap = map;
2352
+ devtoolsObserver?.attach(map);
3967
2353
  installMapBenchmark(map);
3968
- map.addControl(new maplibregl.NavigationControl(), "top-right");
2354
+ if (window.parent === window) map.addControl(new maplibregl.NavigationControl(), "top-right");
3969
2355
  let threeDimensionalControl;
3970
2356
  let treeControl;
3971
2357
  if (isSemanticPreview) {
@@ -3973,10 +2359,17 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3973
2359
  treeControl = new TreeControl();
3974
2360
  map.addControl(threeDimensionalControl, "top-right");
3975
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
+ }
3976
2368
  }
3977
- 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");
3978
2372
  let ensuringThreeDimensionalLayers;
3979
- const treeRuntimeMinimumZoom = 16;
3980
2373
  const ensureThreeDimensionalLayers = () => {
3981
2374
  if (ensuringThreeDimensionalLayers) return ensuringThreeDimensionalLayers;
3982
2375
  const currentStyleLayers = map.getStyle()?.layers || [];
@@ -3995,11 +2388,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3995
2388
  landmarkManifestUrl &&
3996
2389
  threeDimensionalEnabled &&
3997
2390
  map.getZoom() >= landmarkPreloadZoom;
3998
- const needsTrees =
3999
- treesEnabled &&
4000
- treeRendererMode !== "circle" &&
4001
- map.getZoom() >= treeRuntimeMinimumZoom &&
4002
- !map.getLayer("tileflow-vegetation-trees-3d");
4003
2391
  const needsLandmarks =
4004
2392
  landmarkConfigLayer &&
4005
2393
  threeDimensionalEnabled &&
@@ -4011,7 +2399,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
4011
2399
  styleLayerIsVisibleAtZoom(buildingWireframeConfigLayer, map.getZoom()) &&
4012
2400
  !map.getLayer("tileflow-buildings-wireframe-3d");
4013
2401
  if (
4014
- !needsTrees &&
4015
2402
  !needsLandmarks &&
4016
2403
  !needsBuildingWireframe &&
4017
2404
  !shouldPreloadLandmarks
@@ -4038,11 +2425,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
4038
2425
  await loadBuildingWireframeRuntime();
4039
2426
  addBuildingWireframeLayerIfConfigured(map, styleLayers);
4040
2427
  }
4041
- if (
4042
- treesEnabled &&
4043
- treeRendererMode !== "circle" &&
4044
- map.getZoom() >= treeRuntimeMinimumZoom
4045
- ) addTreeLayerIfConfigured(map, styleLayers);
4046
2428
  const landmarkConfigLayer = styleLayers.find(
4047
2429
  (layer) => layer.metadata?.["tileflow:landmark-manifest-url"]
4048
2430
  );
@@ -4073,12 +2455,16 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
4073
2455
  map.on("tileflow:3d-toggle", ensureThreeDimensionalLayers);
4074
2456
  map.on("tileflow:trees-toggle", ensureThreeDimensionalLayers);
4075
2457
  map.on("load", () => {
2458
+ mountCoreScene(map);
2459
+ treeControl?.update();
4076
2460
  writeCameraToUrl(map);
4077
2461
  updateGlobeBackdrop(map);
4078
2462
  ensureThreeDimensionalLayers();
4079
2463
  });
4080
2464
  map.on("move", () => updateGlobeBackdrop(map));
2465
+ map.on("style.load", () => updateGlobeBackdrop(map));
4081
2466
  map.on("moveend", () => writeCameraToUrl(map));
2467
+ ${previewSceneClient}
4082
2468
  addEventListener("message", (event) => {
4083
2469
  if (
4084
2470
  window.parent === window ||
@@ -4149,6 +2535,15 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
4149
2535
  }
4150
2536
 
4151
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 {
4152
2547
  const events = new EventSource(${JSON.stringify(`${basePath}/__events`)});
4153
2548
  // Generations are monotonic only within one dev-server process. A source-code watcher can
4154
2549
  // restart that process at generation 1, so reconnecting after a live connection was lost
@@ -4169,6 +2564,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
4169
2564
  for (const eventName of ["building", "ready", "invalid"]) {
4170
2565
  events.addEventListener(eventName, (event) => applyStatus(JSON.parse(event.data)));
4171
2566
  }
2567
+ }
4172
2568
  </script>
4173
2569
  </body>
4174
2570
  </html>`;