@deck.gl-community/three 9.2.5 → 9.3.0-beta.1

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/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # @deck.gl-community/three
2
2
 
3
- A collection of deck.gl layers powered by [Three.js](https://threejs.org/), giving access to Three.js geometry primitives, materials, and scene graph tooling directly inside deck.gl visualisations.
3
+ [![NPM Version](https://img.shields.io/npm/v/@deck.gl-community/three.svg)](https://www.npmjs.com/package/@deck.gl-community/three)
4
+ [![NPM Downloads](https://img.shields.io/npm/dw/@deck.gl-community/three.svg)](https://www.npmjs.com/package/@deck.gl-community/three)
5
+
6
+ A collection of deck.gl layers powered by [Three.js](https://threejs.org/), giving access to Three.js geometry primitives and scene graph tooling directly inside deck.gl visualisations.
4
7
 
5
8
  `TreeLayer` is the first layer in this module — a fully parametric 3D tree renderer backed by Three.js `BufferGeometry` and rendered via deck.gl's `SimpleMeshLayer`.
6
9
 
@@ -8,7 +11,7 @@ A collection of deck.gl layers powered by [Three.js](https://threejs.org/), givi
8
11
 
9
12
  | Layer | Description |
10
13
  |-------|-------------|
11
- | [`TreeLayer`](#treelayer) | Procedural 3D trees with 5 species silhouettes and season-driven colours |
14
+ | [`TreeLayer`](#treelayer) | Procedural 3D trees with 5 species silhouettes, season colours, and crop/fruit visualisation |
12
15
 
13
16
  ---
14
17
 
@@ -19,10 +22,13 @@ Renders richly configurable 3D trees at geographic positions using procedural ge
19
22
  ### Features
20
23
 
21
24
  - **5 tree species / silhouettes**: pine (tiered cones), oak (sphere), palm (flat crown), birch (narrow oval), cherry (round sphere)
25
+ - **Organic canopy geometry**: smooth low-frequency vertex jitter baked into each species mesh at init time — no runtime cost, no mesh gaps
26
+ - **Per-tree variety**: position-derived random bearing and asymmetric XY scale give every instance a unique silhouette with zero extra draw calls
22
27
  - **Parametric geometry**: per-instance height, trunk-to-canopy ratio, trunk radius, canopy radius
23
28
  - **Season-driven colours**: spring / summer / autumn / winter palettes with species-specific defaults
24
29
  - **Explicit colour overrides**: `getTrunkColor` and `getCanopyColor` accessors for full control
25
- - **Pine tier density**: `getBranchLevels` (1–5) controls the number of overlapping cone tiers
30
+ - **Pine tier density**: `getBranchLevels` (1–5) controls the number of overlapping cone tiers; each tier drifts progressively for a windswept look
31
+ - **Crop / fruit / flower visualisation**: `getCrop` places coloured spheres in the outer canopy volume and scattered on the ground beneath the tree
26
32
  - **Global scale factor**: `sizeScale` multiplier for easy zoom-level adjustment
27
33
 
28
34
  ## Install
@@ -35,47 +41,155 @@ yarn add @deck.gl-community/three
35
41
 
36
42
  > Three.js is a peer dependency pulled in automatically.
37
43
 
38
- ### Usage
44
+ ## Usage
39
45
 
40
46
  ```tsx
41
47
  import {TreeLayer} from '@deck.gl-community/three';
48
+ import type {CropConfig} from '@deck.gl-community/three';
42
49
 
43
50
  const layer = new TreeLayer({
44
51
  id: 'trees',
45
52
  data: myForestData,
46
53
  getPosition: d => d.coordinates,
47
- getTreeType: d => d.species, // 'pine' | 'oak' | 'palm' | 'birch' | 'cherry'
54
+ getTreeType: d => d.species, // 'pine' | 'oak' | 'palm' | 'birch' | 'cherry'
48
55
  getHeight: d => d.heightMetres,
49
56
  getTrunkRadius: d => d.trunkRadius,
50
57
  getCanopyRadius: d => d.canopyRadius,
51
58
  getTrunkHeightFraction: d => 0.35,
52
59
  getSeason: d => 'autumn',
60
+ getCrop: d => d.crop, // CropConfig | null
53
61
  sizeScale: 1,
54
62
  pickable: true,
55
63
  });
56
64
  ```
57
65
 
66
+ ### Crop / fruit / flower example
67
+
68
+ ```tsx
69
+ import type {CropConfig} from '@deck.gl-community/three';
70
+
71
+ // Orange orchard
72
+ const citrusCrop: CropConfig = {
73
+ color: [255, 140, 0, 255], // orange
74
+ count: 30, // live fruits in canopy
75
+ droppedCount: 10, // fallen fruits on ground (rendered at 45% opacity)
76
+ radius: 0.12, // metres per fruit sphere (scaled by sizeScale)
77
+ };
78
+
79
+ // Cherry blossom (flowering stage — just use flower colour)
80
+ const blossomCrop: CropConfig = {
81
+ color: [255, 200, 220, 200],
82
+ count: 25,
83
+ droppedCount: 8,
84
+ radius: 0.07,
85
+ };
86
+ ```
87
+
88
+ Crop positions are seeded deterministically from each tree's geographic coordinates, so they are stable across re-renders and sizeScale changes.
89
+
90
+ ---
91
+
58
92
  ## TreeLayer Props
59
93
 
94
+ ### Geometry
95
+
60
96
  | Prop | Type | Default | Description |
61
97
  |------|------|---------|-------------|
62
98
  | `getPosition` | accessor → `[lon, lat]` | `d.position` | Tree base position |
63
99
  | `getElevation` | accessor → `number` | `0` | Base elevation in metres |
64
100
  | `getTreeType` | accessor → `TreeType` | `'pine'` | Silhouette variant |
65
101
  | `getHeight` | accessor → `number` | `10` | Total height (metres) |
66
- | `getTrunkHeightFraction` | accessor → `number` | `0.35` | Trunk fraction of total height |
102
+ | `getTrunkHeightFraction` | accessor → `number` | `0.35` | Fraction of total height occupied by trunk (0–1) |
67
103
  | `getTrunkRadius` | accessor → `number` | `0.5` | Trunk base radius (metres) |
68
104
  | `getCanopyRadius` | accessor → `number` | `3` | Canopy horizontal radius (metres) |
105
+ | `getBranchLevels` | accessor → `number` | `3` | Pine tier count (1–5) |
106
+ | `sizeScale` | `number` | `1` | Global size multiplier applied to all dimensions |
107
+
108
+ ### Colour
109
+
110
+ | Prop | Type | Default | Description |
111
+ |------|------|---------|-------------|
69
112
  | `getTrunkColor` | accessor → `Color\|null` | `null` | Explicit trunk RGBA; `null` uses species default |
70
113
  | `getCanopyColor` | accessor → `Color\|null` | `null` | Explicit canopy RGBA; `null` uses season default |
71
- | `getSeason` | accessor → `Season` | `'summer'` | Drives default canopy colour |
72
- | `getBranchLevels` | accessor → `number` | `3` | Pine tier count (1–5) |
73
- | `sizeScale` | `number` | `1` | Global size multiplier |
114
+ | `getSeason` | accessor → `Season` | `'summer'` | Drives default canopy colour when no explicit colour is given |
74
115
 
75
- ## TreeType values
116
+ ### Crops
117
+
118
+ | Prop | Type | Default | Description |
119
+ |------|------|---------|-------------|
120
+ | `getCrop` | accessor → `CropConfig\|null` | `null` | Crop configuration per tree. `null` renders no crops |
76
121
 
77
- `'pine'` · `'oak'` · `'palm'` · `'birch'` · `'cherry'`
122
+ ---
123
+
124
+ ## Types
125
+
126
+ ### `TreeType`
127
+
128
+ ```ts
129
+ type TreeType = 'pine' | 'oak' | 'palm' | 'birch' | 'cherry';
130
+ ```
78
131
 
79
- ## Season values
132
+ ### `Season`
133
+
134
+ ```ts
135
+ type Season = 'spring' | 'summer' | 'autumn' | 'winter';
136
+ ```
137
+
138
+ ### `CropConfig`
139
+
140
+ Configuration for crop/fruit/flower visualisation on a single tree.
141
+
142
+ ```ts
143
+ type CropConfig = {
144
+ /** Colour of each crop sphere [r, g, b, a]. */
145
+ color: Color;
146
+
147
+ /** Number of crop spheres placed in the outer canopy volume (live / in-tree). */
148
+ count: number;
149
+
150
+ /**
151
+ * Number of crop spheres scattered on the ground within the canopy footprint
152
+ * (dropped / fallen). Rendered at ~45 % opacity relative to `color`.
153
+ * @default 0
154
+ */
155
+ droppedCount?: number;
156
+
157
+ /**
158
+ * Radius of each individual crop sphere in metres (scaled by `sizeScale`).
159
+ * Typical values: 0.06–0.15 m for fruit, 0.05–0.10 m for nuts/blossoms.
160
+ */
161
+ radius: number;
162
+ };
163
+ ```
164
+
165
+ #### Crop placement details
166
+
167
+ - **Live crops** are placed on the outer 90–102 % of the canopy ellipsoid surface (equatorial band only — never on the crown or base) so they appear nestled into the canopy with tips just visible.
168
+ - **Dropped crops** are scattered uniformly across the ground disk within the canopy footprint at a fixed slight elevation (0.05 m), rendered semi-transparent.
169
+ - All positions are derived deterministically from the tree's geographic coordinates via a seeded PRNG, so they are stable across re-renders and `sizeScale` changes.
170
+
171
+ ---
172
+
173
+ ## Default canopy colours
174
+
175
+ | Species | Spring | Summer | Autumn | Winter |
176
+ |---------|--------|--------|--------|--------|
177
+ | pine | `[34, 100, 34]` | `[0, 64, 0]` | `[0, 64, 0]` | `[0, 55, 0]` |
178
+ | oak | `[100, 180, 80]` | `[34, 120, 15]` | `[180, 85, 20]` | `[100, 80, 60]` (α 160) |
179
+ | palm | `[50, 160, 50]` | `[20, 145, 20]` | `[55, 150, 30]` | `[40, 130, 30]` |
180
+ | birch | `[150, 210, 110]` | `[80, 160, 60]` | `[230, 185, 40]` | `[180, 180, 170]` (α 90) |
181
+ | cherry | `[255, 180, 205]` | `[50, 140, 50]` | `[200, 60, 40]` | `[120, 90, 80]` (α 110) |
182
+
183
+ ---
184
+
185
+ ## Wild-Forest example
186
+
187
+ A full demo with 9 forest zones (pines, oaks, palms, birches, cherry blossoms, citrus orchards, almond groves) is available at `examples/three/wild-forest/`.
188
+
189
+ ```bash
190
+ cd examples/three/wild-forest
191
+ yarn # first time only
192
+ yarn start # opens http://localhost:8080
193
+ ```
80
194
 
81
- `'spring'` · `'summer'` · `'autumn'` · `'winter'`
195
+ The example includes a live `sizeScale` slider, a crop toggle, and a zone legend.
package/dist/index.cjs CHANGED
@@ -35,6 +35,44 @@ var import_mesh_layers = require("@deck.gl/mesh-layers");
35
35
  // dist/tree-layer/tree-geometry.js
36
36
  var import_three = require("three");
37
37
  var Y_TO_Z_UP = new import_three.Matrix4().makeRotationX(-Math.PI / 2);
38
+ function jitterSmooth(geo, magnitude, seed) {
39
+ let s = seed >>> 0;
40
+ const rng = () => {
41
+ s = s + 2654435769 | 0;
42
+ let t = s ^ s >>> 16;
43
+ t = Math.imul(t, 569420461);
44
+ t ^= t >>> 15;
45
+ t = Math.imul(t, 1935289751);
46
+ return ((t ^ t >>> 15) >>> 0) / 4294967296;
47
+ };
48
+ const waves = Array.from({ length: 4 }, () => ({
49
+ fx: 2 + rng() * 3,
50
+ fy: 2 + rng() * 3,
51
+ fz: 2 + rng() * 3,
52
+ phase: rng() * Math.PI * 2
53
+ }));
54
+ const pos = geo.attributes.position.array;
55
+ for (let i = 0; i < pos.length; i += 3) {
56
+ const x = pos[i];
57
+ const y = pos[i + 1];
58
+ const z = pos[i + 2];
59
+ const r = Math.sqrt(x * x + y * y + z * z);
60
+ if (r !== 0) {
61
+ const nx = x / r;
62
+ const ny = y / r;
63
+ const nz = z / r;
64
+ let noise = 0;
65
+ for (const w of waves) {
66
+ noise += Math.sin(nx * w.fx + ny * w.fy + nz * w.fz + w.phase);
67
+ }
68
+ noise /= 4;
69
+ const scale = 1 + noise * magnitude;
70
+ pos[i] = x * scale;
71
+ pos[i + 1] = y * scale;
72
+ pos[i + 2] = z * scale;
73
+ }
74
+ }
75
+ }
38
76
  function extractMesh(geo) {
39
77
  geo.computeVertexNormals();
40
78
  const posAttr = geo.attributes.position;
@@ -97,32 +135,49 @@ function createTrunkMesh(segments = 8) {
97
135
  }
98
136
  function createPineCanopyMesh(levels = 3, segments = 8) {
99
137
  const geos = [];
100
- const tierHeight = 0.55 / levels;
138
+ let s = levels * 2654435761 >>> 0;
139
+ const rng = () => {
140
+ s = s + 2654435769 | 0;
141
+ let t = s ^ s >>> 16;
142
+ t = Math.imul(t, 569420461);
143
+ t ^= t >>> 15;
144
+ t = Math.imul(t, 1935289751);
145
+ return ((t ^ t >>> 15) >>> 0) / 4294967296;
146
+ };
147
+ const tierHeight = 1.6 / (levels + 1);
148
+ const step = tierHeight / 2;
149
+ let zCursor = 0;
101
150
  for (let i = 0; i < levels; i++) {
102
151
  const t = i / (levels - 1 || 1);
103
- const radius = (1 - t * 0.5) * 0.85;
104
- const zBase = t * (1 - tierHeight * 1.2);
105
- const cone = new import_three.ConeGeometry(radius, tierHeight, segments);
152
+ const baseRadius = (1 - t * 0.5) * 0.85;
153
+ const radius = baseRadius * (0.8 + rng() * 0.4);
154
+ const tierH = tierHeight * (0.85 + rng() * 0.3);
155
+ const cone = new import_three.ConeGeometry(radius, tierH, segments);
106
156
  cone.applyMatrix4(Y_TO_Z_UP);
107
- cone.translate(0, 0, zBase + tierHeight);
157
+ const driftScale = levels > 1 ? i / (levels - 1) : 0;
158
+ const driftX = (rng() - 0.5) * 0.2 * driftScale;
159
+ const driftY = (rng() - 0.5) * 0.2 * driftScale;
160
+ cone.translate(driftX, driftY, zCursor + tierH / 2);
108
161
  geos.push(cone);
162
+ zCursor += step;
109
163
  }
110
- const tip = new import_three.ConeGeometry(0.12, 0.18, 6);
164
+ const tip = new import_three.ConeGeometry(0.08, 0.22, 6);
111
165
  tip.applyMatrix4(Y_TO_Z_UP);
112
- tip.translate(0, 0, 1);
166
+ tip.translate((rng() - 0.5) * 0.08, (rng() - 0.5) * 0.08, zCursor + 0.05);
113
167
  geos.push(tip);
114
168
  const merged = mergeGeometries(geos);
115
- merged.computeVertexNormals();
116
169
  return extractMesh(merged);
117
170
  }
118
171
  function createOakCanopyMesh() {
119
- const geo = new import_three.SphereGeometry(0.5, 12, 8);
172
+ const geo = new import_three.SphereGeometry(0.5, 14, 10);
173
+ jitterSmooth(geo, 0.18, 1);
120
174
  geo.applyMatrix4(Y_TO_Z_UP);
121
175
  geo.translate(0, 0, 0.5);
122
176
  return extractMesh(geo);
123
177
  }
124
178
  function createPalmCanopyMesh() {
125
179
  const geo = new import_three.SphereGeometry(0.7, 12, 5);
180
+ jitterSmooth(geo, 0.1, 4);
126
181
  const flatten = new import_three.Matrix4().makeScale(1.4, 0.35, 1.4);
127
182
  geo.applyMatrix4(flatten);
128
183
  geo.applyMatrix4(Y_TO_Z_UP);
@@ -131,6 +186,7 @@ function createPalmCanopyMesh() {
131
186
  }
132
187
  function createBirchCanopyMesh() {
133
188
  const geo = new import_three.SphereGeometry(0.42, 10, 8);
189
+ jitterSmooth(geo, 0.14, 2);
134
190
  const elongate = new import_three.Matrix4().makeScale(1, 1.45, 1);
135
191
  geo.applyMatrix4(elongate);
136
192
  geo.applyMatrix4(Y_TO_Z_UP);
@@ -139,6 +195,13 @@ function createBirchCanopyMesh() {
139
195
  }
140
196
  function createCherryCanopyMesh() {
141
197
  const geo = new import_three.SphereGeometry(0.52, 12, 8);
198
+ jitterSmooth(geo, 0.2, 3);
199
+ geo.applyMatrix4(Y_TO_Z_UP);
200
+ geo.translate(0, 0, 0.5);
201
+ return extractMesh(geo);
202
+ }
203
+ function createCropMesh() {
204
+ const geo = new import_three.SphereGeometry(0.5, 6, 4);
142
205
  geo.applyMatrix4(Y_TO_Z_UP);
143
206
  geo.translate(0, 0, 0.5);
144
207
  return extractMesh(geo);
@@ -198,7 +261,79 @@ var CANOPY_MESHES = {
198
261
  birch: createBirchCanopyMesh(),
199
262
  cherry: createCherryCanopyMesh()
200
263
  };
264
+ var CROP_MESH = createCropMesh();
201
265
  var ALL_TREE_TYPES = ["pine", "oak", "palm", "birch", "cherry"];
266
+ var CANOPY_TRUNK_OVERLAP = 0.22;
267
+ function createRng(seed) {
268
+ let s = seed >>> 0;
269
+ return () => {
270
+ s = s + 2654435769 | 0;
271
+ let t = s ^ s >>> 16;
272
+ t = Math.imul(t, 569420461);
273
+ t ^= t >>> 15;
274
+ t = Math.imul(t, 1935289751);
275
+ return ((t ^ t >>> 15) >>> 0) / 4294967296;
276
+ };
277
+ }
278
+ function positionSeed(lng, lat) {
279
+ return (Math.round(lng * 1e4) * 92821 ^ Math.round(lat * 1e4) * 65537) >>> 0;
280
+ }
281
+ var DEG_PER_METER_LAT = 1 / 111320;
282
+ function lngDegreesPerMeter(latDeg) {
283
+ return 1 / (111320 * Math.cos(latDeg * Math.PI / 180));
284
+ }
285
+ function expandLiveCropPoints(opts) {
286
+ const { lng, lat, elevation, height, trunkFraction, canopyRadius, cropConfig, out } = opts;
287
+ if (cropConfig.count <= 0)
288
+ return;
289
+ const rxy = canopyRadius * 0.5;
290
+ const canopyH = height * (1 - trunkFraction);
291
+ const rz = canopyH * 0.5;
292
+ const canopyCenterZ = elevation + height * trunkFraction - canopyH * CANOPY_TRUNK_OVERLAP + rz;
293
+ const dLng = lngDegreesPerMeter(lat);
294
+ const rng = createRng(positionSeed(lng, lat));
295
+ for (let i = 0; i < cropConfig.count; i++) {
296
+ const theta = rng() * Math.PI * 2;
297
+ const cosPhi = -0.8 + rng() * 1.6;
298
+ const sinPhi = Math.sqrt(Math.max(0, 1 - cosPhi * cosPhi));
299
+ const radFrac = 0.9 + rng() * 0.12;
300
+ const dx = rxy * radFrac * sinPhi * Math.cos(theta);
301
+ const dy = rxy * radFrac * sinPhi * Math.sin(theta);
302
+ const dz = rz * radFrac * cosPhi;
303
+ out.push({
304
+ position: [lng + dx * dLng, lat + dy * DEG_PER_METER_LAT, canopyCenterZ + dz],
305
+ color: cropConfig.color,
306
+ scale: cropConfig.radius
307
+ });
308
+ }
309
+ }
310
+ function expandDroppedCropPoints(opts) {
311
+ const { lng, lat, elevation, canopyRadius, cropConfig, out } = opts;
312
+ const droppedCount = cropConfig.droppedCount ?? 0;
313
+ if (droppedCount <= 0)
314
+ return;
315
+ const footprintRadius = canopyRadius * 0.5;
316
+ const dLng = lngDegreesPerMeter(lat);
317
+ const rng = createRng(positionSeed(lng, lat) ^ 439041101);
318
+ const c = cropConfig.color;
319
+ const droppedColor = [
320
+ c[0],
321
+ c[1],
322
+ c[2],
323
+ Math.round((c[3] ?? 255) * 0.45)
324
+ ];
325
+ for (let i = 0; i < droppedCount; i++) {
326
+ const theta = rng() * Math.PI * 2;
327
+ const dist = Math.sqrt(rng()) * footprintRadius;
328
+ const dx = dist * Math.cos(theta);
329
+ const dy = dist * Math.sin(theta);
330
+ out.push({
331
+ position: [lng + dx * dLng, lat + dy * DEG_PER_METER_LAT, elevation + 0.05],
332
+ color: droppedColor,
333
+ scale: cropConfig.radius
334
+ });
335
+ }
336
+ }
202
337
  var defaultProps = {
203
338
  getPosition: { type: "accessor", value: (d) => d.position },
204
339
  getElevation: { type: "accessor", value: (_d) => 0 },
@@ -211,18 +346,21 @@ var defaultProps = {
211
346
  getCanopyColor: { type: "accessor", value: (_d) => null },
212
347
  getSeason: { type: "accessor", value: (_d) => "summer" },
213
348
  getBranchLevels: { type: "accessor", value: (_d) => 3 },
349
+ getCrop: { type: "accessor", value: (_d) => null },
214
350
  sizeScale: { type: "number", value: 1, min: 0 }
215
351
  };
216
352
  var TreeLayer = class extends import_core.CompositeLayer {
217
353
  initializeState() {
218
354
  this.state = {
219
355
  grouped: { pine: [], oak: [], palm: [], birch: [], cherry: [] },
220
- pineMeshes: {}
356
+ pineMeshes: {},
357
+ liveCropPoints: [],
358
+ droppedCropPoints: []
221
359
  };
222
360
  }
223
- updateState({ props, changeFlags }) {
224
- if (changeFlags.dataChanged || changeFlags.updateTriggersChanged) {
225
- const { data, getTreeType, getBranchLevels } = props;
361
+ updateState({ props, oldProps, changeFlags }) {
362
+ if (changeFlags.dataChanged || changeFlags.propsChanged || changeFlags.updateTriggersChanged) {
363
+ const { data, getTreeType, getBranchLevels, getCrop, getPosition, getElevation, getHeight, getTrunkHeightFraction, getCanopyRadius, sizeScale } = props;
226
364
  const grouped = {
227
365
  pine: [],
228
366
  oak: [],
@@ -231,6 +369,8 @@ var TreeLayer = class extends import_core.CompositeLayer {
231
369
  cherry: []
232
370
  };
233
371
  const pineMeshes = {};
372
+ const liveCropPoints = [];
373
+ const droppedCropPoints = [];
234
374
  for (const d of data) {
235
375
  const type = getTreeType(d);
236
376
  if (grouped[type])
@@ -239,36 +379,78 @@ var TreeLayer = class extends import_core.CompositeLayer {
239
379
  const levels = Math.max(1, Math.min(5, Math.round(getBranchLevels(d))));
240
380
  pineMeshes[levels] ??= createPineCanopyMesh(levels);
241
381
  }
382
+ const cropConfig = getCrop(d);
383
+ if (cropConfig) {
384
+ const pos = getPosition(d);
385
+ const lng = pos[0];
386
+ const lat = pos[1];
387
+ const elev = getElevation(d) || 0;
388
+ const h = getHeight(d) * sizeScale;
389
+ const f = getTrunkHeightFraction(d);
390
+ const r = getCanopyRadius(d) * sizeScale;
391
+ const scaledCropConfig = {
392
+ ...cropConfig,
393
+ radius: cropConfig.radius * sizeScale
394
+ };
395
+ expandLiveCropPoints({
396
+ lng,
397
+ lat,
398
+ elevation: elev,
399
+ height: h,
400
+ trunkFraction: f,
401
+ canopyRadius: r,
402
+ cropConfig: scaledCropConfig,
403
+ out: liveCropPoints
404
+ });
405
+ expandDroppedCropPoints({
406
+ lng,
407
+ lat,
408
+ elevation: elev,
409
+ canopyRadius: r,
410
+ cropConfig: scaledCropConfig,
411
+ out: droppedCropPoints
412
+ });
413
+ }
242
414
  }
243
- this.setState({ grouped, pineMeshes });
415
+ this.setState({ grouped, pineMeshes, liveCropPoints, droppedCropPoints });
244
416
  }
245
417
  }
246
- /** Build a single canopy sub-layer for one tree type. */
247
- _buildCanopyLayer(type) {
418
+ /**
419
+ * Build a single canopy sub-layer.
420
+ *
421
+ * Takes explicit `mesh`, `data`, and `layerId` so that pine trees can be
422
+ * split into one sub-layer per level count (each with its own mesh).
423
+ */
424
+ _buildCanopyLayer(type, mesh, data, layerId) {
248
425
  const { getPosition, getElevation, getHeight, getTrunkHeightFraction, getCanopyRadius, getCanopyColor, getSeason, sizeScale } = this.props;
249
- const { grouped, pineMeshes } = this.state;
250
- let mesh = CANOPY_MESHES[type];
251
- if (type === "pine") {
252
- const firstLevel = Object.keys(pineMeshes)[0];
253
- if (firstLevel)
254
- mesh = pineMeshes[Number(firstLevel)];
255
- }
256
426
  return new import_mesh_layers.SimpleMeshLayer(this.getSubLayerProps({
257
- id: `canopy-${type}`,
258
- data: grouped[type],
427
+ id: layerId,
428
+ data,
259
429
  mesh,
260
430
  getPosition: (d) => {
261
431
  const pos = getPosition(d);
262
432
  const elevation = getElevation(d) || 0;
263
433
  const h = getHeight(d) * sizeScale;
264
434
  const f = getTrunkHeightFraction(d);
265
- return [pos[0], pos[1], elevation + h * f];
435
+ const canopyH = h * (1 - f);
436
+ return [pos[0], pos[1], elevation + h * f - canopyH * CANOPY_TRUNK_OVERLAP];
266
437
  },
267
438
  getScale: (d) => {
439
+ const pos = getPosition(d);
268
440
  const h = getHeight(d) * sizeScale;
269
441
  const f = getTrunkHeightFraction(d);
270
442
  const r = getCanopyRadius(d) * sizeScale;
271
- return [r, r, h * (1 - f)];
443
+ const seed = positionSeed(pos[0], pos[1]);
444
+ const sx = 1 + ((seed & 65535) / 65535 - 0.5) * 0.6;
445
+ const sy = 1 + ((seed >>> 16 & 65535) / 65535 - 0.5) * 0.6;
446
+ return [r * sx, r * sy, h * (1 - f)];
447
+ },
448
+ getOrientation: (d) => {
449
+ const pos = getPosition(d);
450
+ const seed = positionSeed(pos[0], pos[1]);
451
+ const yaw = ((seed ^ seed >>> 13) & 65535) / 65535 * 360;
452
+ const pitch = (((seed ^ seed >>> 7) & 255) / 255 - 0.5) * 24;
453
+ return [pitch, yaw, 0];
272
454
  },
273
455
  getColor: (d) => {
274
456
  const explicit = getCanopyColor(d);
@@ -278,12 +460,17 @@ var TreeLayer = class extends import_core.CompositeLayer {
278
460
  return DEFAULT_CANOPY_COLORS[type][season];
279
461
  },
280
462
  pickable: this.props.pickable,
281
- material: { ambient: 0.4, diffuse: 0.7, shininess: 12 }
463
+ material: { ambient: 0.55, diffuse: 0.55, shininess: 0 },
464
+ updateTriggers: {
465
+ getPosition: sizeScale,
466
+ getScale: sizeScale,
467
+ getOrientation: sizeScale
468
+ }
282
469
  }));
283
470
  }
284
471
  renderLayers() {
285
- const { getPosition, getElevation, getTreeType, getHeight, getTrunkHeightFraction, getTrunkRadius, getTrunkColor, sizeScale } = this.props;
286
- const { grouped } = this.state;
472
+ const { getPosition, getElevation, getTreeType, getHeight, getTrunkHeightFraction, getTrunkRadius, getTrunkColor, getBranchLevels, sizeScale } = this.props;
473
+ const { grouped, pineMeshes, liveCropPoints, droppedCropPoints } = this.state;
287
474
  const trunkLayer = new import_mesh_layers.SimpleMeshLayer(this.getSubLayerProps({
288
475
  id: "trunks",
289
476
  data: this.props.data,
@@ -306,10 +493,45 @@ var TreeLayer = class extends import_core.CompositeLayer {
306
493
  return DEFAULT_TRUNK_COLORS[type] ?? DEFAULT_TRUNK_COLORS.pine;
307
494
  },
308
495
  pickable: this.props.pickable,
309
- material: { ambient: 0.35, diffuse: 0.6, shininess: 8 }
496
+ material: { ambient: 0.45, diffuse: 0.55, shininess: 4 },
497
+ updateTriggers: { getScale: sizeScale }
310
498
  }));
311
- const canopyLayers = ALL_TREE_TYPES.filter((type) => grouped[type].length > 0).map((type) => this._buildCanopyLayer(type));
312
- return [trunkLayer, ...canopyLayers];
499
+ const nonPineCanopies = ALL_TREE_TYPES.filter((t) => t !== "pine" && grouped[t].length > 0).map((t) => this._buildCanopyLayer(t, CANOPY_MESHES[t], grouped[t], `canopy-${t}`));
500
+ const pineCanopies = Object.entries(pineMeshes).flatMap(([levelStr, mesh]) => {
501
+ const levels = Number(levelStr);
502
+ const pineData = grouped.pine.filter(
503
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
504
+ (d) => Math.max(1, Math.min(5, Math.round(getBranchLevels(d)))) === levels
505
+ );
506
+ return pineData.length > 0 ? [this._buildCanopyLayer("pine", mesh, pineData, `canopy-pine-${levels}`)] : [];
507
+ });
508
+ const canopyLayers = [...nonPineCanopies, ...pineCanopies];
509
+ const cropLayers = [];
510
+ if (liveCropPoints.length > 0) {
511
+ cropLayers.push(new import_mesh_layers.SimpleMeshLayer(this.getSubLayerProps({
512
+ id: "live-crops",
513
+ data: liveCropPoints,
514
+ mesh: CROP_MESH,
515
+ getPosition: (d) => d.position,
516
+ getScale: (d) => [d.scale, d.scale, d.scale],
517
+ getColor: (d) => d.color,
518
+ pickable: false,
519
+ material: { ambient: 0.5, diffuse: 0.8, shininess: 40 }
520
+ })));
521
+ }
522
+ if (droppedCropPoints.length > 0) {
523
+ cropLayers.push(new import_mesh_layers.SimpleMeshLayer(this.getSubLayerProps({
524
+ id: "dropped-crops",
525
+ data: droppedCropPoints,
526
+ mesh: CROP_MESH,
527
+ getPosition: (d) => d.position,
528
+ getScale: (d) => [d.scale, d.scale, d.scale],
529
+ getColor: (d) => d.color,
530
+ pickable: false,
531
+ material: { ambient: 0.6, diffuse: 0.5, shininess: 10 }
532
+ })));
533
+ }
534
+ return [trunkLayer, ...canopyLayers, ...cropLayers];
313
535
  }
314
536
  };
315
537
  __publicField(TreeLayer, "layerName", "TreeLayer");