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

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
@@ -265,6 +265,8 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
265
265
  import {loadTileflowStyleFonts} from "${basePath}/__runtime/tileflow-browser.js";
266
266
  import {registerTileflowContourProtocol} from "${basePath}/__runtime/tileflow-browser.js";
267
267
  import {registerTileflowPmtilesProtocol} from "${basePath}/__runtime/tileflow-browser.js";
268
+ import {createTileflowBuildings3D, createTileflowStreetLights, createTileflowTrees3D} from "${basePath}/__runtime/tileflow-browser.js";
269
+ import {readTileflowSunPreset, resolveTileflowSun, tileflowDefaultSunPreset} from "${basePath}/__runtime/tileflow-browser.js";
268
270
 
269
271
  maplibregl.setWorkerUrl("${basePath}/__runtime/maplibre-gl-worker.mjs");
270
272
 
@@ -282,66 +284,42 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
282
284
  const previewThemes = ${serializeInlineJson(preview?.themeNames ?? [])};
283
285
  const isSemanticPreview = ${JSON.stringify(isSemanticPreview)};
284
286
  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"));
287
+ const previewSearchParameters = new URL(location.href).searchParams;
288
+ const mapBenchmarkEnabled = previewSearchParameters.get("mapBenchmark") === "1";
289
+ const mapSweepEnabled = previewSearchParameters.get("mapSweep") === "1";
290
+ const mapCompareEnabled = previewSearchParameters.get("mapCompare") === "1";
291
+ const requestedMapCompareLimit = Number(previewSearchParameters.get("mapCompareLimit"));
298
292
  const mapCompareLimit =
299
293
  Number.isInteger(requestedMapCompareLimit) &&
300
294
  requestedMapCompareLimit >= 1 &&
301
295
  requestedMapCompareLimit <= 64
302
296
  ? requestedMapCompareLimit
303
297
  : 16;
304
- const requestedMapWorkerCount = Number(treeSearchParameters.get("mapWorkers"));
298
+ const requestedMapWorkerCount = Number(previewSearchParameters.get("mapWorkers"));
305
299
  const mapWorkerCountOverride =
306
300
  Number.isInteger(requestedMapWorkerCount) &&
307
301
  requestedMapWorkerCount >= 1 &&
308
302
  requestedMapWorkerCount <= 3
309
303
  ? requestedMapWorkerCount
310
304
  : undefined;
311
- const mapBenchmarkLongitude = Number(treeSearchParameters.get("lng"));
312
- const mapBenchmarkLatitude = Number(treeSearchParameters.get("lat"));
305
+ const mapBenchmarkLongitude = Number(previewSearchParameters.get("lng"));
306
+ const mapBenchmarkLatitude = Number(previewSearchParameters.get("lat"));
313
307
  const mapBenchmarkCenter =
314
- treeSearchParameters.has("lng") &&
315
- treeSearchParameters.has("lat") &&
308
+ previewSearchParameters.has("lng") &&
309
+ previewSearchParameters.has("lat") &&
316
310
  Number.isFinite(mapBenchmarkLongitude) &&
317
311
  Number.isFinite(mapBenchmarkLatitude)
318
312
  ? [mapBenchmarkLongitude, mapBenchmarkLatitude]
319
313
  : undefined;
320
314
  function readToggleFromUrl(name, fallback) {
321
- const values = treeSearchParameters.getAll(name);
315
+ const values = previewSearchParameters.getAll(name);
322
316
  if (values.length !== 1) return fallback;
323
317
  if (values[0] === "on") return true;
324
318
  if (values[0] === "off") return false;
325
319
  return fallback;
326
320
  }
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
321
  let threeDimensionalEnabled = readToggleFromUrl("buildings3d", false);
344
- let treesEnabled = readToggleFromUrl("trees3d", true);
322
+ let treesEnabled = readToggleFromUrl("trees3d", false);
345
323
  const landmarkState = {
346
324
  active: [],
347
325
  cacheLimit: undefined,
@@ -493,26 +471,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
493
471
  return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * percentileValue) - 1)];
494
472
  }
495
473
 
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
474
  function installMapBenchmark(map) {
517
475
  if (!mapBenchmarkEnabled) return;
518
476
  let activeRun;
@@ -1456,1719 +1414,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
1456
1414
  map.addLayer(createBuildingWireframeLayer(map, styleLayer), layerAboveBuildings);
1457
1415
  }
1458
1416
 
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
1417
  function styleLayerZoomRange(layer) {
3173
1418
  return {
3174
1419
  minzoom: Number.isFinite(layer?.minzoom) ? layer.minzoom : 0,
@@ -3668,8 +1913,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3668
1913
  spatialFallbacks:
3669
1914
  layer.metadata?.["tileflow:landmark-spatial-fallbacks"] ?? []
3670
1915
  }));
3671
- // Landmark custom layers use Mercator world coordinates. Switch here
3672
- // instead of relying on the optional detailed-tree renderer to do it.
1916
+ // Landmark custom layers use Mercator world coordinates.
3673
1917
  if (map.getProjection().type !== "mercator") {
3674
1918
  map.setProjection({type: "mercator"});
3675
1919
  }
@@ -3679,14 +1923,57 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3679
1923
  !layer.id.startsWith("tileflow-label-road-")) ||
3680
1924
  layer.id.startsWith("tileflow-poi-")
3681
1925
  )?.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;
1926
+ const treeLayer = map.getLayer("tileflow-vegetation-trees")
1927
+ ? "tileflow-vegetation-trees"
1928
+ : firstForegroundLabel;
3687
1929
  map.addLayer(createLandmarkLayer(map, configLayer, fallbackLayers), treeLayer);
3688
1930
  }
3689
1931
 
1932
+ // Core's 3D layers, as an application mounts them: the map's style describes how its
1933
+ // buildings, trees and street lights look, and the controls only show and hide them. With
1934
+ // both controls off the preview draws what Capture draws. The preview shows one theme at a
1935
+ // time, so the style on screen holds the sun and the design.
1936
+ const coreScene = {buildings: undefined, trees: undefined, lights: undefined, buildingsShown: false, treesShown: false};
1937
+ globalThis.__tileflowPreviewScene = coreScene;
1938
+ function coreSceneThemes(map) {
1939
+ return {
1940
+ getSun() {
1941
+ const preset = readTileflowSunPreset(map.getStyle()?.metadata);
1942
+ return resolveTileflowSun(preset ?? tileflowDefaultSunPreset);
1943
+ },
1944
+ getThemeMetadata(key) {
1945
+ return {values: [map.getStyle()?.metadata?.[key]], position: 0};
1946
+ }
1947
+ };
1948
+ }
1949
+ function mountCoreScene(map) {
1950
+ const metadata = map.getStyle()?.metadata ?? {};
1951
+ const controller = coreSceneThemes(map);
1952
+ if (!coreScene.buildings && metadata["tileflow:buildings"]) {
1953
+ coreScene.buildings = createTileflowBuildings3D({map, controller});
1954
+ }
1955
+ if (!coreScene.trees && metadata["tileflow:vegetation"]) {
1956
+ coreScene.trees = createTileflowTrees3D({map, controller});
1957
+ }
1958
+ // Street lights have no capture fallback, so they shine only with the 3D control on.
1959
+ if (!coreScene.lights && threeDimensionalEnabled && metadata["tileflow:street-lights"]) {
1960
+ coreScene.lights = createTileflowStreetLights({map, controller});
1961
+ } else if (coreScene.lights && !threeDimensionalEnabled) {
1962
+ coreScene.lights.dispose();
1963
+ coreScene.lights = undefined;
1964
+ }
1965
+ const instant = {transition: {duration: 0}};
1966
+ // Only a change of the controls moves the layers, so a style event does not restart them.
1967
+ if (coreScene.buildings && coreScene.buildingsShown !== threeDimensionalEnabled) {
1968
+ coreScene.buildingsShown = threeDimensionalEnabled;
1969
+ void (threeDimensionalEnabled ? coreScene.buildings.show() : coreScene.buildings.hide(instant));
1970
+ }
1971
+ if (coreScene.trees && coreScene.treesShown !== treesEnabled) {
1972
+ coreScene.treesShown = treesEnabled;
1973
+ void (treesEnabled ? coreScene.trees.show() : coreScene.trees.hide(instant));
1974
+ }
1975
+ }
1976
+
3690
1977
  class ThreeDimensionalControl {
3691
1978
  constructor() {
3692
1979
  this.enabled = threeDimensionalEnabled;
@@ -3722,6 +2009,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3722
2009
  this.update();
3723
2010
  writeToggleStateToUrl();
3724
2011
  this.map.fire("tileflow:3d-toggle", {enabled: this.enabled});
2012
+ mountCoreScene(this.map);
3725
2013
  this.map.triggerRepaint();
3726
2014
  }
3727
2015
 
@@ -3729,11 +2017,14 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3729
2017
  for (const layer of this.map.getStyle()?.layers || []) {
3730
2018
  const toggle = layer.metadata?.["tileflow:3d-toggle"];
3731
2019
  if (toggle !== "building" && toggle !== "landmark") continue;
3732
- const visibility = this.enabled ? "visible" : "none";
2020
+ // Core's 3D buildings stand in for the compiled extrusion where the style describes them.
2021
+ const replaced = toggle === "building" && this.map.getStyle()?.metadata?.["tileflow:buildings"];
2022
+ const visibility = this.enabled && !replaced ? "visible" : "none";
3733
2023
  if (this.map.getLayoutProperty(layer.id, "visibility") !== visibility) {
3734
2024
  this.map.setLayoutProperty(layer.id, "visibility", visibility);
3735
2025
  }
3736
2026
  }
2027
+ if (this.map.isStyleLoaded()) mountCoreScene(this.map);
3737
2028
  this.button.textContent = this.enabled ? "3D ON" : "3D OFF";
3738
2029
  this.button.setAttribute("aria-pressed", String(this.enabled));
3739
2030
  this.button.title = this.enabled ? "Disable 3D buildings" : "Enable 3D buildings";
@@ -3780,6 +2071,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3780
2071
  this.update();
3781
2072
  writeToggleStateToUrl();
3782
2073
  this.map.fire("tileflow:trees-toggle", {enabled: this.enabled});
2074
+ mountCoreScene(this.map);
3783
2075
  this.map.triggerRepaint();
3784
2076
  }
3785
2077
 
@@ -3787,15 +2079,15 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3787
2079
  const treeLayer = this.map.getStyle()?.layers?.find(
3788
2080
  (layer) => layer.id === "tileflow-vegetation-trees"
3789
2081
  );
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);
2082
+ // Core's 3D trees hide the style's flat trees while they stand (mountCoreScene).
2083
+ if (treeLayer && !this.map.getStyle()?.metadata?.["tileflow:vegetation"]) {
2084
+ if (this.map.getLayoutProperty(treeLayer.id, "visibility") !== "visible") {
2085
+ this.map.setLayoutProperty(treeLayer.id, "visibility", "visible");
3794
2086
  }
3795
2087
  }
3796
2088
  this.button.textContent = this.enabled ? "TREES ON" : "TREES OFF";
3797
2089
  this.button.setAttribute("aria-pressed", String(this.enabled));
3798
- this.button.title = this.enabled ? "Hide trees" : "Show trees";
2090
+ this.button.title = this.enabled ? "Show flat trees" : "Show 3D trees";
3799
2091
  }
3800
2092
 
3801
2093
  onRemove() {
@@ -3920,8 +2212,15 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3920
2212
  const groups = previewLayerGroups(layer);
3921
2213
  const threeDimensionalToggle = layer.metadata?.["tileflow:3d-toggle"];
3922
2214
  const authoredVisibility = defaultLayerVisibility.get(layer.id);
3923
- const toggleVisibility =
3924
- threeDimensionalToggle === "building" || threeDimensionalToggle === "landmark"
2215
+ const styleMetadata = map.getStyle()?.metadata ?? {};
2216
+ // Core's 3D buildings and trees stand in for compiled extrusions and flat trees.
2217
+ const flatTrees = styleMetadata["tileflow:vegetation"]?.flatLayers ?? [];
2218
+ const replacedByCore =
2219
+ (threeDimensionalToggle === "building" && styleMetadata["tileflow:buildings"]) ||
2220
+ (treesEnabled && flatTrees.includes(layer.id.split("::")[0]));
2221
+ const toggleVisibility = replacedByCore
2222
+ ? "none"
2223
+ : threeDimensionalToggle === "building" || threeDimensionalToggle === "landmark"
3925
2224
  ? (threeDimensionalEnabled ? "visible" : "none")
3926
2225
  : authoredVisibility;
3927
2226
  const visibility = groups.some((group) => !visibleLayerGroups.has(group))
@@ -3976,7 +2275,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3976
2275
  }
3977
2276
  if (previewThemes.length > 1) map.addControl(new ThemeControl(), "top-right");
3978
2277
  let ensuringThreeDimensionalLayers;
3979
- const treeRuntimeMinimumZoom = 16;
3980
2278
  const ensureThreeDimensionalLayers = () => {
3981
2279
  if (ensuringThreeDimensionalLayers) return ensuringThreeDimensionalLayers;
3982
2280
  const currentStyleLayers = map.getStyle()?.layers || [];
@@ -3995,11 +2293,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
3995
2293
  landmarkManifestUrl &&
3996
2294
  threeDimensionalEnabled &&
3997
2295
  map.getZoom() >= landmarkPreloadZoom;
3998
- const needsTrees =
3999
- treesEnabled &&
4000
- treeRendererMode !== "circle" &&
4001
- map.getZoom() >= treeRuntimeMinimumZoom &&
4002
- !map.getLayer("tileflow-vegetation-trees-3d");
4003
2296
  const needsLandmarks =
4004
2297
  landmarkConfigLayer &&
4005
2298
  threeDimensionalEnabled &&
@@ -4011,7 +2304,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
4011
2304
  styleLayerIsVisibleAtZoom(buildingWireframeConfigLayer, map.getZoom()) &&
4012
2305
  !map.getLayer("tileflow-buildings-wireframe-3d");
4013
2306
  if (
4014
- !needsTrees &&
4015
2307
  !needsLandmarks &&
4016
2308
  !needsBuildingWireframe &&
4017
2309
  !shouldPreloadLandmarks
@@ -4038,11 +2330,6 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
4038
2330
  await loadBuildingWireframeRuntime();
4039
2331
  addBuildingWireframeLayerIfConfigured(map, styleLayers);
4040
2332
  }
4041
- if (
4042
- treesEnabled &&
4043
- treeRendererMode !== "circle" &&
4044
- map.getZoom() >= treeRuntimeMinimumZoom
4045
- ) addTreeLayerIfConfigured(map, styleLayers);
4046
2333
  const landmarkConfigLayer = styleLayers.find(
4047
2334
  (layer) => layer.metadata?.["tileflow:landmark-manifest-url"]
4048
2335
  );
@@ -4076,6 +2363,7 @@ function renderTileflowPreviewHtml(preview, basePath, initialStatus, isSemanticP
4076
2363
  writeCameraToUrl(map);
4077
2364
  updateGlobeBackdrop(map);
4078
2365
  ensureThreeDimensionalLayers();
2366
+ mountCoreScene(map);
4079
2367
  });
4080
2368
  map.on("move", () => updateGlobeBackdrop(map));
4081
2369
  map.on("moveend", () => writeCameraToUrl(map));