@deck.gl-community/three 9.2.5
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/LICENSE +19 -0
- package/README.md +81 -0
- package/dist/index.cjs +317 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/tree-layer/tree-geometry.d.ts +56 -0
- package/dist/tree-layer/tree-geometry.d.ts.map +1 -0
- package/dist/tree-layer/tree-geometry.js +163 -0
- package/dist/tree-layer/tree-geometry.js.map +1 -0
- package/dist/tree-layer/tree-layer.d.ts +112 -0
- package/dist/tree-layer/tree-layer.d.ts.map +1 -0
- package/dist/tree-layer/tree-layer.js +206 -0
- package/dist/tree-layer/tree-layer.js.map +1 -0
- package/package.json +46 -0
- package/src/index.ts +6 -0
- package/src/tree-layer/tree-geometry.ts +204 -0
- package/src/tree-layer/tree-layer.ts +365 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BufferGeometry,
|
|
7
|
+
BufferAttribute,
|
|
8
|
+
CylinderGeometry,
|
|
9
|
+
ConeGeometry,
|
|
10
|
+
SphereGeometry,
|
|
11
|
+
Matrix4
|
|
12
|
+
} from 'three';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Mesh format compatible with deck.gl SimpleMeshLayer.
|
|
16
|
+
* All geometries are Z-up (deck.gl convention), unit scale (0..1 in Z = bottom to top).
|
|
17
|
+
*/
|
|
18
|
+
export type TreeMesh = {
|
|
19
|
+
attributes: {
|
|
20
|
+
POSITION: {value: Float32Array; size: 3};
|
|
21
|
+
NORMAL: {value: Float32Array; size: 3};
|
|
22
|
+
};
|
|
23
|
+
indices: {value: Uint32Array; size: 1};
|
|
24
|
+
topology: 'triangle-list';
|
|
25
|
+
mode: 4;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Rotation matrix that converts from Three.js Y-up to deck.gl Z-up.
|
|
30
|
+
* Rotates -90 degrees around the X axis: Y -> Z, Z -> -Y.
|
|
31
|
+
*/
|
|
32
|
+
const Y_TO_Z_UP = new Matrix4().makeRotationX(-Math.PI / 2);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract a TreeMesh from a Three.js BufferGeometry.
|
|
36
|
+
* Assumes the geometry has already been rotated to Z-up.
|
|
37
|
+
*/
|
|
38
|
+
function extractMesh(geo: BufferGeometry): TreeMesh {
|
|
39
|
+
geo.computeVertexNormals();
|
|
40
|
+
const posAttr = geo.attributes.position as BufferAttribute;
|
|
41
|
+
const norAttr = geo.attributes.normal as BufferAttribute;
|
|
42
|
+
const idx = geo.index;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
attributes: {
|
|
46
|
+
POSITION: {value: new Float32Array(posAttr.array), size: 3},
|
|
47
|
+
NORMAL: {value: new Float32Array(norAttr.array), size: 3}
|
|
48
|
+
},
|
|
49
|
+
indices: {value: new Uint32Array(idx ? idx.array : new Uint32Array(0)), size: 1},
|
|
50
|
+
topology: 'triangle-list',
|
|
51
|
+
mode: 4
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Copy indices for one geometry slice, offsetting by the current vertex base. */
|
|
56
|
+
function copyIndices(
|
|
57
|
+
out: Uint32Array,
|
|
58
|
+
outOffset: number,
|
|
59
|
+
geo: BufferGeometry,
|
|
60
|
+
vertexBase: number
|
|
61
|
+
): number {
|
|
62
|
+
if (geo.index) {
|
|
63
|
+
const src = geo.index.array;
|
|
64
|
+
for (let i = 0; i < src.length; i++) out[outOffset + i] = src[i] + vertexBase;
|
|
65
|
+
return src.length;
|
|
66
|
+
}
|
|
67
|
+
const count = geo.attributes.position.count;
|
|
68
|
+
for (let i = 0; i < count; i++) out[outOffset + i] = vertexBase + i;
|
|
69
|
+
return count;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Merge multiple Three.js BufferGeometries into a single geometry.
|
|
74
|
+
* All input geometries must be indexed.
|
|
75
|
+
*/
|
|
76
|
+
function mergeGeometries(geos: BufferGeometry[]): BufferGeometry {
|
|
77
|
+
let totalVertices = 0;
|
|
78
|
+
let totalIndices = 0;
|
|
79
|
+
for (const geo of geos) {
|
|
80
|
+
totalVertices += geo.attributes.position.count;
|
|
81
|
+
totalIndices += geo.index ? geo.index.count : geo.attributes.position.count;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const positions = new Float32Array(totalVertices * 3);
|
|
85
|
+
const normals = new Float32Array(totalVertices * 3);
|
|
86
|
+
const indices = new Uint32Array(totalIndices);
|
|
87
|
+
let vOffset = 0;
|
|
88
|
+
let iOffset = 0;
|
|
89
|
+
|
|
90
|
+
for (const geo of geos) {
|
|
91
|
+
const count = geo.attributes.position.count;
|
|
92
|
+
const srcNor = geo.attributes.normal ? (geo.attributes.normal.array as Float32Array) : null;
|
|
93
|
+
positions.set(geo.attributes.position.array as Float32Array, vOffset * 3);
|
|
94
|
+
if (srcNor) normals.set(srcNor, vOffset * 3);
|
|
95
|
+
iOffset += copyIndices(indices, iOffset, geo, vOffset);
|
|
96
|
+
vOffset += count;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const merged = new BufferGeometry();
|
|
100
|
+
merged.setAttribute('position', new BufferAttribute(positions, 3));
|
|
101
|
+
merged.setAttribute('normal', new BufferAttribute(normals, 3));
|
|
102
|
+
merged.setIndex(new BufferAttribute(indices, 1));
|
|
103
|
+
return merged;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Unit trunk cylinder mesh: from z=0 (base) to z=1 (top), radius tapers from 1 to 0.7.
|
|
108
|
+
* Scale via `getScale = [trunkRadius, trunkRadius, trunkHeight]`.
|
|
109
|
+
*/
|
|
110
|
+
export function createTrunkMesh(segments = 8): TreeMesh {
|
|
111
|
+
const geo = new CylinderGeometry(0.7, 1.0, 1.0, segments);
|
|
112
|
+
// Three.js CylinderGeometry is centered at origin, extends from y=-0.5 to y=0.5
|
|
113
|
+
geo.applyMatrix4(Y_TO_Z_UP); // Rotate to Z-up: now z=-0.5 to z=0.5
|
|
114
|
+
geo.translate(0, 0, 0.5); // Shift so base is at z=0, top at z=1
|
|
115
|
+
return extractMesh(geo);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Unit pine canopy mesh: multiple tiered cones creating a Christmas tree silhouette.
|
|
120
|
+
* Extends from z=0 (base of canopy) to z=1 (tip).
|
|
121
|
+
*
|
|
122
|
+
* @param levels - number of cone tiers (1-5)
|
|
123
|
+
* @param segments - polygon segments per cone
|
|
124
|
+
*/
|
|
125
|
+
export function createPineCanopyMesh(levels = 3, segments = 8): TreeMesh {
|
|
126
|
+
const geos: BufferGeometry[] = [];
|
|
127
|
+
const tierHeight = 0.55 / levels;
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < levels; i++) {
|
|
130
|
+
const t = i / (levels - 1 || 1);
|
|
131
|
+
// Bottom tiers are wider, top tiers are narrower
|
|
132
|
+
const radius = (1 - t * 0.5) * 0.85;
|
|
133
|
+
// Tiers are staggered: each one starts 60% up the previous tier
|
|
134
|
+
const zBase = t * (1 - tierHeight * 1.2);
|
|
135
|
+
|
|
136
|
+
const cone = new ConeGeometry(radius, tierHeight, segments);
|
|
137
|
+
cone.applyMatrix4(Y_TO_Z_UP);
|
|
138
|
+
// ConeGeometry apex is at y=+height/2 -> z=+height/2 after rotation
|
|
139
|
+
// Translate so apex points upward and base is at zBase
|
|
140
|
+
cone.translate(0, 0, zBase + tierHeight);
|
|
141
|
+
geos.push(cone);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Sharp tip at top
|
|
145
|
+
const tip = new ConeGeometry(0.12, 0.18, 6);
|
|
146
|
+
tip.applyMatrix4(Y_TO_Z_UP);
|
|
147
|
+
tip.translate(0, 0, 1.0);
|
|
148
|
+
geos.push(tip);
|
|
149
|
+
|
|
150
|
+
const merged = mergeGeometries(geos);
|
|
151
|
+
merged.computeVertexNormals();
|
|
152
|
+
return extractMesh(merged);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Unit oak canopy mesh: a large sphere.
|
|
157
|
+
* Extends from z=0 to z=1, center at z=0.5.
|
|
158
|
+
*/
|
|
159
|
+
export function createOakCanopyMesh(): TreeMesh {
|
|
160
|
+
const geo = new SphereGeometry(0.5, 12, 8);
|
|
161
|
+
// SphereGeometry is centered at origin, radius=0.5
|
|
162
|
+
geo.applyMatrix4(Y_TO_Z_UP);
|
|
163
|
+
geo.translate(0, 0, 0.5); // center at z=0.5, extends z=0 to z=1
|
|
164
|
+
return extractMesh(geo);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Unit palm canopy mesh: a flat, wide disk crown typical of palm trees.
|
|
169
|
+
* Extends from z=0 to z=0.35, radius=1.
|
|
170
|
+
*/
|
|
171
|
+
export function createPalmCanopyMesh(): TreeMesh {
|
|
172
|
+
// Flattened sphere acting as a spread crown
|
|
173
|
+
const geo = new SphereGeometry(0.7, 12, 5);
|
|
174
|
+
const flatten = new Matrix4().makeScale(1.4, 0.35, 1.4);
|
|
175
|
+
geo.applyMatrix4(flatten);
|
|
176
|
+
geo.applyMatrix4(Y_TO_Z_UP);
|
|
177
|
+
geo.translate(0, 0, 0.18);
|
|
178
|
+
return extractMesh(geo);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unit birch canopy mesh: a narrow oval / diamond shape.
|
|
183
|
+
* Extends from z=0 to z=1, narrower than an oak.
|
|
184
|
+
*/
|
|
185
|
+
export function createBirchCanopyMesh(): TreeMesh {
|
|
186
|
+
const geo = new SphereGeometry(0.42, 10, 8);
|
|
187
|
+
// Elongate vertically (Z after rotation)
|
|
188
|
+
const elongate = new Matrix4().makeScale(1, 1.45, 1);
|
|
189
|
+
geo.applyMatrix4(elongate);
|
|
190
|
+
geo.applyMatrix4(Y_TO_Z_UP);
|
|
191
|
+
geo.translate(0, 0, 0.5);
|
|
192
|
+
return extractMesh(geo);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Unit cherry canopy mesh: a full, round sphere slightly larger than oak.
|
|
197
|
+
* Extends from z=0 to z=1.1 (slightly wider than tall for a lush look).
|
|
198
|
+
*/
|
|
199
|
+
export function createCherryCanopyMesh(): TreeMesh {
|
|
200
|
+
const geo = new SphereGeometry(0.52, 12, 8);
|
|
201
|
+
geo.applyMatrix4(Y_TO_Z_UP);
|
|
202
|
+
geo.translate(0, 0, 0.5);
|
|
203
|
+
return extractMesh(geo);
|
|
204
|
+
}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
// deck.gl-community
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import {CompositeLayer} from '@deck.gl/core';
|
|
6
|
+
import type {Color, DefaultProps, LayerProps, Position} from '@deck.gl/core';
|
|
7
|
+
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
|
|
8
|
+
import {
|
|
9
|
+
createTrunkMesh,
|
|
10
|
+
createPineCanopyMesh,
|
|
11
|
+
createOakCanopyMesh,
|
|
12
|
+
createPalmCanopyMesh,
|
|
13
|
+
createBirchCanopyMesh,
|
|
14
|
+
createCherryCanopyMesh
|
|
15
|
+
} from './tree-geometry';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Public types
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
/** Tree species / silhouette variant. */
|
|
22
|
+
export type TreeType = 'pine' | 'oak' | 'palm' | 'birch' | 'cherry';
|
|
23
|
+
|
|
24
|
+
/** Season that drives default canopy colour when no explicit colour is supplied. */
|
|
25
|
+
export type Season = 'spring' | 'summer' | 'autumn' | 'winter';
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Default colours
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/** Default trunk colours per tree type [r, g, b, a]. */
|
|
32
|
+
const DEFAULT_TRUNK_COLORS: Record<TreeType, Color> = {
|
|
33
|
+
pine: [80, 50, 20, 255],
|
|
34
|
+
oak: [91, 57, 23, 255],
|
|
35
|
+
palm: [140, 100, 55, 255],
|
|
36
|
+
birch: [220, 215, 205, 255], // white-grey birch bark
|
|
37
|
+
cherry: [100, 60, 40, 255]
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Default canopy colours per (tree type, season) [r, g, b, a]. */
|
|
41
|
+
const DEFAULT_CANOPY_COLORS: Record<TreeType, Record<Season, Color>> = {
|
|
42
|
+
pine: {
|
|
43
|
+
spring: [34, 100, 34, 255],
|
|
44
|
+
summer: [0, 64, 0, 255],
|
|
45
|
+
autumn: [0, 64, 0, 255], // evergreen — no colour change
|
|
46
|
+
winter: [0, 55, 0, 255]
|
|
47
|
+
},
|
|
48
|
+
oak: {
|
|
49
|
+
spring: [100, 180, 80, 255],
|
|
50
|
+
summer: [34, 120, 15, 255],
|
|
51
|
+
autumn: [180, 85, 20, 255],
|
|
52
|
+
winter: [100, 80, 60, 160] // sparse, semi-transparent
|
|
53
|
+
},
|
|
54
|
+
palm: {
|
|
55
|
+
spring: [50, 160, 50, 255],
|
|
56
|
+
summer: [20, 145, 20, 255],
|
|
57
|
+
autumn: [55, 150, 30, 255],
|
|
58
|
+
winter: [40, 130, 30, 255]
|
|
59
|
+
},
|
|
60
|
+
birch: {
|
|
61
|
+
spring: [150, 210, 110, 255],
|
|
62
|
+
summer: [80, 160, 60, 255],
|
|
63
|
+
autumn: [230, 185, 40, 255],
|
|
64
|
+
winter: [180, 180, 170, 90] // near-bare
|
|
65
|
+
},
|
|
66
|
+
cherry: {
|
|
67
|
+
spring: [255, 180, 205, 255], // pink blossom
|
|
68
|
+
summer: [50, 140, 50, 255],
|
|
69
|
+
autumn: [200, 60, 40, 255],
|
|
70
|
+
winter: [120, 90, 80, 110] // bare
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// Pre-built unit-scale meshes (shared across all layer instances)
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
const TRUNK_MESH = createTrunkMesh();
|
|
79
|
+
|
|
80
|
+
const CANOPY_MESHES: Record<TreeType, ReturnType<typeof createTrunkMesh>> = {
|
|
81
|
+
pine: createPineCanopyMesh(3),
|
|
82
|
+
oak: createOakCanopyMesh(),
|
|
83
|
+
palm: createPalmCanopyMesh(),
|
|
84
|
+
birch: createBirchCanopyMesh(),
|
|
85
|
+
cherry: createCherryCanopyMesh()
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const ALL_TREE_TYPES: TreeType[] = ['pine', 'oak', 'palm', 'birch', 'cherry'];
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Props
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
type _TreeLayerProps<DataT> = {
|
|
95
|
+
/** Source data. */
|
|
96
|
+
data: DataT[];
|
|
97
|
+
|
|
98
|
+
/** Longitude/latitude position of the tree base. */
|
|
99
|
+
getPosition?: (d: DataT) => Position;
|
|
100
|
+
|
|
101
|
+
/** Base elevation (metres above sea level). @default 0 */
|
|
102
|
+
getElevation?: (d: DataT) => number;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Silhouette / species variant.
|
|
106
|
+
* 'pine' – layered conical tiers (evergreen)
|
|
107
|
+
* 'oak' – wide spherical canopy
|
|
108
|
+
* 'palm' – tall thin trunk with flat crown
|
|
109
|
+
* 'birch' – narrow oval canopy, pale bark
|
|
110
|
+
* 'cherry' – round lush canopy, seasonal blossom
|
|
111
|
+
* @default 'pine'
|
|
112
|
+
*/
|
|
113
|
+
getTreeType?: (d: DataT) => TreeType;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Total tree height in metres.
|
|
117
|
+
* @default 10
|
|
118
|
+
*/
|
|
119
|
+
getHeight?: (d: DataT) => number;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Fraction of total height occupied by the trunk (0–1).
|
|
123
|
+
* @default 0.35
|
|
124
|
+
*/
|
|
125
|
+
getTrunkHeightFraction?: (d: DataT) => number;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Trunk base radius in metres.
|
|
129
|
+
* @default 0.5
|
|
130
|
+
*/
|
|
131
|
+
getTrunkRadius?: (d: DataT) => number;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Horizontal radius of the canopy in metres.
|
|
135
|
+
* @default 3
|
|
136
|
+
*/
|
|
137
|
+
getCanopyRadius?: (d: DataT) => number;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Explicit trunk colour [r, g, b, a].
|
|
141
|
+
* When null the species default is used.
|
|
142
|
+
* @default null
|
|
143
|
+
*/
|
|
144
|
+
getTrunkColor?: (d: DataT) => Color | null;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Explicit canopy colour [r, g, b, a].
|
|
148
|
+
* When null the species × season default is used.
|
|
149
|
+
* @default null
|
|
150
|
+
*/
|
|
151
|
+
getCanopyColor?: (d: DataT) => Color | null;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Season used to pick the default canopy colour when no explicit colour is provided.
|
|
155
|
+
* @default 'summer'
|
|
156
|
+
*/
|
|
157
|
+
getSeason?: (d: DataT) => Season;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Number of cone tiers for pine trees (1–5).
|
|
161
|
+
* Higher values produce a denser layered silhouette.
|
|
162
|
+
* @default 3
|
|
163
|
+
*/
|
|
164
|
+
getBranchLevels?: (d: DataT) => number;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Global size multiplier applied to all dimensions.
|
|
168
|
+
* @default 1
|
|
169
|
+
*/
|
|
170
|
+
sizeScale?: number;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type TreeLayerProps<DataT = unknown> = _TreeLayerProps<DataT> & LayerProps;
|
|
174
|
+
|
|
175
|
+
const defaultProps: DefaultProps<TreeLayerProps<unknown>> = {
|
|
176
|
+
getPosition: {type: 'accessor', value: (d: any) => d.position},
|
|
177
|
+
getElevation: {type: 'accessor', value: (_d: any) => 0},
|
|
178
|
+
getTreeType: {type: 'accessor', value: (_d: any) => 'pine' as TreeType},
|
|
179
|
+
getHeight: {type: 'accessor', value: (_d: any) => 10},
|
|
180
|
+
getTrunkHeightFraction: {type: 'accessor', value: (_d: any) => 0.35},
|
|
181
|
+
getTrunkRadius: {type: 'accessor', value: (_d: any) => 0.5},
|
|
182
|
+
getCanopyRadius: {type: 'accessor', value: (_d: any) => 3},
|
|
183
|
+
getTrunkColor: {type: 'accessor', value: (_d: any) => null},
|
|
184
|
+
getCanopyColor: {type: 'accessor', value: (_d: any) => null},
|
|
185
|
+
getSeason: {type: 'accessor', value: (_d: any) => 'summer' as Season},
|
|
186
|
+
getBranchLevels: {type: 'accessor', value: (_d: any) => 3},
|
|
187
|
+
sizeScale: {type: 'number', value: 1, min: 0}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// State
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
type TreeLayerState = {
|
|
195
|
+
grouped: Record<TreeType, unknown[]>;
|
|
196
|
+
pineMeshes: Record<number, ReturnType<typeof createPineCanopyMesh>>;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Layer
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* **TreeLayer** — A parametric, Three.js-powered deck.gl layer that renders
|
|
205
|
+
* richly configurable 3D trees at geographic positions.
|
|
206
|
+
*
|
|
207
|
+
* Each tree is composed of two `SimpleMeshLayer` instances: one for the trunk
|
|
208
|
+
* (a tapered cylinder) and one for the canopy (silhouette depends on `getTreeType`).
|
|
209
|
+
* All geometry is generated procedurally using Three.js `BufferGeometry` primitives
|
|
210
|
+
* and converted to the `@loaders.gl/schema` `MeshGeometry` format accepted by
|
|
211
|
+
* `SimpleMeshLayer`.
|
|
212
|
+
*
|
|
213
|
+
* Parametric controls include:
|
|
214
|
+
* - Species / silhouette (`getTreeType`)
|
|
215
|
+
* - Total height (`getHeight`) and trunk-to-canopy proportion (`getTrunkHeightFraction`)
|
|
216
|
+
* - Trunk and canopy radii (`getTrunkRadius`, `getCanopyRadius`)
|
|
217
|
+
* - Explicit or season-driven colours (`getTrunkColor`, `getCanopyColor`, `getSeason`)
|
|
218
|
+
* - Pine tier density (`getBranchLevels`)
|
|
219
|
+
* - Global scale factor (`sizeScale`)
|
|
220
|
+
*/
|
|
221
|
+
export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends CompositeLayer<
|
|
222
|
+
ExtraPropsT & Required<_TreeLayerProps<DataT>>
|
|
223
|
+
> {
|
|
224
|
+
static layerName = 'TreeLayer';
|
|
225
|
+
static defaultProps = defaultProps;
|
|
226
|
+
|
|
227
|
+
declare state: TreeLayerState;
|
|
228
|
+
|
|
229
|
+
initializeState() {
|
|
230
|
+
this.state = {
|
|
231
|
+
grouped: {pine: [], oak: [], palm: [], birch: [], cherry: []},
|
|
232
|
+
pineMeshes: {}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
updateState({props, changeFlags}) {
|
|
237
|
+
if (changeFlags.dataChanged || changeFlags.updateTriggersChanged) {
|
|
238
|
+
const {data, getTreeType, getBranchLevels} = props;
|
|
239
|
+
const grouped: Record<TreeType, DataT[]> = {
|
|
240
|
+
pine: [],
|
|
241
|
+
oak: [],
|
|
242
|
+
palm: [],
|
|
243
|
+
birch: [],
|
|
244
|
+
cherry: []
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
// Build per-level pine mesh cache
|
|
248
|
+
const pineMeshes: Record<number, ReturnType<typeof createPineCanopyMesh>> = {};
|
|
249
|
+
|
|
250
|
+
for (const d of data as DataT[]) {
|
|
251
|
+
const type = getTreeType(d) as TreeType;
|
|
252
|
+
if (grouped[type]) grouped[type].push(d);
|
|
253
|
+
if (type === 'pine') {
|
|
254
|
+
const levels = Math.max(1, Math.min(5, Math.round(getBranchLevels(d) as number)));
|
|
255
|
+
pineMeshes[levels] ??= createPineCanopyMesh(levels);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
this.setState({grouped, pineMeshes});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Build a single canopy sub-layer for one tree type. */
|
|
264
|
+
private _buildCanopyLayer(type: TreeType) {
|
|
265
|
+
const {
|
|
266
|
+
getPosition,
|
|
267
|
+
getElevation,
|
|
268
|
+
getHeight,
|
|
269
|
+
getTrunkHeightFraction,
|
|
270
|
+
getCanopyRadius,
|
|
271
|
+
getCanopyColor,
|
|
272
|
+
getSeason,
|
|
273
|
+
sizeScale
|
|
274
|
+
} = this.props; // eslint-disable-line max-len
|
|
275
|
+
const {grouped, pineMeshes} = this.state;
|
|
276
|
+
|
|
277
|
+
let mesh = CANOPY_MESHES[type];
|
|
278
|
+
if (type === 'pine') {
|
|
279
|
+
const firstLevel = Object.keys(pineMeshes)[0];
|
|
280
|
+
if (firstLevel) mesh = pineMeshes[Number(firstLevel)];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return new SimpleMeshLayer(
|
|
284
|
+
this.getSubLayerProps({
|
|
285
|
+
id: `canopy-${type}`,
|
|
286
|
+
data: grouped[type],
|
|
287
|
+
mesh,
|
|
288
|
+
getPosition: (d) => {
|
|
289
|
+
const pos = getPosition(d);
|
|
290
|
+
const elevation = getElevation(d) || 0;
|
|
291
|
+
const h = getHeight(d) * sizeScale;
|
|
292
|
+
const f = getTrunkHeightFraction(d);
|
|
293
|
+
return [pos[0], pos[1], elevation + h * f];
|
|
294
|
+
},
|
|
295
|
+
getScale: (d) => {
|
|
296
|
+
const h = getHeight(d) * sizeScale;
|
|
297
|
+
const f = getTrunkHeightFraction(d);
|
|
298
|
+
const r = getCanopyRadius(d) * sizeScale;
|
|
299
|
+
return [r, r, h * (1 - f)];
|
|
300
|
+
},
|
|
301
|
+
getColor: (d) => {
|
|
302
|
+
const explicit = getCanopyColor(d);
|
|
303
|
+
if (explicit) return explicit;
|
|
304
|
+
const season = getSeason(d) || 'summer';
|
|
305
|
+
return DEFAULT_CANOPY_COLORS[type][season];
|
|
306
|
+
},
|
|
307
|
+
pickable: this.props.pickable,
|
|
308
|
+
material: {ambient: 0.4, diffuse: 0.7, shininess: 12}
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
renderLayers() {
|
|
314
|
+
const {
|
|
315
|
+
getPosition,
|
|
316
|
+
getElevation,
|
|
317
|
+
getTreeType,
|
|
318
|
+
getHeight,
|
|
319
|
+
getTrunkHeightFraction,
|
|
320
|
+
getTrunkRadius,
|
|
321
|
+
getTrunkColor,
|
|
322
|
+
sizeScale
|
|
323
|
+
} = this.props;
|
|
324
|
+
|
|
325
|
+
const {grouped} = this.state;
|
|
326
|
+
|
|
327
|
+
// -----------------------------------------------------------------------
|
|
328
|
+
// 1. Trunk layer — one layer for ALL tree types, shared cylinder geometry
|
|
329
|
+
// -----------------------------------------------------------------------
|
|
330
|
+
const trunkLayer = new SimpleMeshLayer(
|
|
331
|
+
this.getSubLayerProps({
|
|
332
|
+
id: 'trunks',
|
|
333
|
+
data: this.props.data,
|
|
334
|
+
mesh: TRUNK_MESH,
|
|
335
|
+
getPosition: (d) => {
|
|
336
|
+
const pos = getPosition(d);
|
|
337
|
+
return [pos[0], pos[1], getElevation(d) || 0];
|
|
338
|
+
},
|
|
339
|
+
getScale: (d) => {
|
|
340
|
+
const h = getHeight(d) * sizeScale;
|
|
341
|
+
const f = getTrunkHeightFraction(d);
|
|
342
|
+
const r = getTrunkRadius(d) * sizeScale;
|
|
343
|
+
return [r, r, h * f];
|
|
344
|
+
},
|
|
345
|
+
getColor: (d) => {
|
|
346
|
+
const explicit = getTrunkColor(d);
|
|
347
|
+
if (explicit) return explicit;
|
|
348
|
+
const type = getTreeType(d) || 'pine';
|
|
349
|
+
return DEFAULT_TRUNK_COLORS[type] ?? DEFAULT_TRUNK_COLORS.pine;
|
|
350
|
+
},
|
|
351
|
+
pickable: this.props.pickable,
|
|
352
|
+
material: {ambient: 0.35, diffuse: 0.6, shininess: 8}
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// -----------------------------------------------------------------------
|
|
357
|
+
// 2. Canopy layers — one per tree type, only for trees of that type
|
|
358
|
+
// -----------------------------------------------------------------------
|
|
359
|
+
const canopyLayers = ALL_TREE_TYPES.filter((type) => grouped[type].length > 0).map((type) =>
|
|
360
|
+
this._buildCanopyLayer(type)
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
return [trunkLayer, ...canopyLayers];
|
|
364
|
+
}
|
|
365
|
+
}
|