@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/artifacts.js +3 -3
- package/dist/{chunk-L56FLV4R.js → chunk-2QXKVQ6O.js} +4 -3
- package/dist/{chunk-6MGFMKG3.js → chunk-AQUGMUZW.js} +43 -3
- package/dist/{chunk-A2VTI43Y.js → chunk-IR22ZTQW.js} +575 -7
- package/dist/config.js +1 -1
- package/dist/icons.d.ts +3 -1
- package/dist/icons.js +1 -1
- package/dist/inspect.js +1 -1
- package/dist/preview.js +85 -1797
- package/dist/server.js +3 -3
- package/docs/local-comparison-workbench-and-compiler-inspection.md +14 -36
- package/package.json +2 -2
package/dist/preview.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getFirstTileflowMapName
|
|
3
|
-
} from "./chunk-
|
|
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
|
|
286
|
-
const
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
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(
|
|
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(
|
|
312
|
-
const mapBenchmarkLatitude = Number(
|
|
305
|
+
const mapBenchmarkLongitude = Number(previewSearchParameters.get("lng"));
|
|
306
|
+
const mapBenchmarkLatitude = Number(previewSearchParameters.get("lat"));
|
|
313
307
|
const mapBenchmarkCenter =
|
|
314
|
-
|
|
315
|
-
|
|
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 =
|
|
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",
|
|
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.
|
|
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
|
|
3683
|
-
? "tileflow-vegetation-trees
|
|
3684
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
3791
|
-
|
|
3792
|
-
if (this.map.getLayoutProperty(treeLayer.id, "visibility") !==
|
|
3793
|
-
this.map.setLayoutProperty(treeLayer.id, "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 ? "
|
|
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
|
|
3924
|
-
|
|
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));
|