@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.
@@ -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
+ }