@deck.gl-community/three 9.2.5 → 9.2.8

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,74 @@ 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 = [c[0], c[1], c[2], Math.round((c[3] ?? 255) * 0.45)];
320
+ for (let i = 0; i < droppedCount; i++) {
321
+ const theta = rng() * Math.PI * 2;
322
+ const dist = Math.sqrt(rng()) * footprintRadius;
323
+ const dx = dist * Math.cos(theta);
324
+ const dy = dist * Math.sin(theta);
325
+ out.push({
326
+ position: [lng + dx * dLng, lat + dy * DEG_PER_METER_LAT, elevation + 0.05],
327
+ color: droppedColor,
328
+ scale: cropConfig.radius
329
+ });
330
+ }
331
+ }
202
332
  var defaultProps = {
203
333
  getPosition: { type: "accessor", value: (d) => d.position },
204
334
  getElevation: { type: "accessor", value: (_d) => 0 },
@@ -211,18 +341,21 @@ var defaultProps = {
211
341
  getCanopyColor: { type: "accessor", value: (_d) => null },
212
342
  getSeason: { type: "accessor", value: (_d) => "summer" },
213
343
  getBranchLevels: { type: "accessor", value: (_d) => 3 },
344
+ getCrop: { type: "accessor", value: (_d) => null },
214
345
  sizeScale: { type: "number", value: 1, min: 0 }
215
346
  };
216
347
  var TreeLayer = class extends import_core.CompositeLayer {
217
348
  initializeState() {
218
349
  this.state = {
219
350
  grouped: { pine: [], oak: [], palm: [], birch: [], cherry: [] },
220
- pineMeshes: {}
351
+ pineMeshes: {},
352
+ liveCropPoints: [],
353
+ droppedCropPoints: []
221
354
  };
222
355
  }
223
- updateState({ props, changeFlags }) {
224
- if (changeFlags.dataChanged || changeFlags.updateTriggersChanged) {
225
- const { data, getTreeType, getBranchLevels } = props;
356
+ updateState({ props, oldProps, changeFlags }) {
357
+ if (changeFlags.dataChanged || changeFlags.propsChanged || changeFlags.updateTriggersChanged) {
358
+ const { data, getTreeType, getBranchLevels, getCrop, getPosition, getElevation, getHeight, getTrunkHeightFraction, getCanopyRadius, sizeScale } = props;
226
359
  const grouped = {
227
360
  pine: [],
228
361
  oak: [],
@@ -231,6 +364,8 @@ var TreeLayer = class extends import_core.CompositeLayer {
231
364
  cherry: []
232
365
  };
233
366
  const pineMeshes = {};
367
+ const liveCropPoints = [];
368
+ const droppedCropPoints = [];
234
369
  for (const d of data) {
235
370
  const type = getTreeType(d);
236
371
  if (grouped[type])
@@ -239,36 +374,77 @@ var TreeLayer = class extends import_core.CompositeLayer {
239
374
  const levels = Math.max(1, Math.min(5, Math.round(getBranchLevels(d))));
240
375
  pineMeshes[levels] ??= createPineCanopyMesh(levels);
241
376
  }
377
+ const cropConfig = getCrop(d);
378
+ if (cropConfig) {
379
+ const pos = getPosition(d);
380
+ const lng = pos[0];
381
+ const lat = pos[1];
382
+ const elev = getElevation(d) || 0;
383
+ const h = getHeight(d) * sizeScale;
384
+ const f = getTrunkHeightFraction(d);
385
+ const r = getCanopyRadius(d) * sizeScale;
386
+ const scaledCropConfig = {
387
+ ...cropConfig,
388
+ radius: cropConfig.radius * sizeScale
389
+ };
390
+ expandLiveCropPoints({
391
+ lng,
392
+ lat,
393
+ elevation: elev,
394
+ height: h,
395
+ trunkFraction: f,
396
+ canopyRadius: r,
397
+ cropConfig: scaledCropConfig,
398
+ out: liveCropPoints
399
+ });
400
+ expandDroppedCropPoints({
401
+ lng,
402
+ lat,
403
+ elevation: elev,
404
+ canopyRadius: r,
405
+ cropConfig: scaledCropConfig,
406
+ out: droppedCropPoints
407
+ });
408
+ }
242
409
  }
243
- this.setState({ grouped, pineMeshes });
410
+ this.setState({ grouped, pineMeshes, liveCropPoints, droppedCropPoints });
244
411
  }
245
412
  }
246
- /** Build a single canopy sub-layer for one tree type. */
247
- _buildCanopyLayer(type) {
413
+ /**
414
+ * Build a single canopy sub-layer.
415
+ *
416
+ * Takes explicit `mesh`, `data`, and `layerId` so that pine trees can be
417
+ * split into one sub-layer per level count (each with its own mesh).
418
+ */
419
+ _buildCanopyLayer(type, mesh, data, layerId) {
248
420
  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
421
  return new import_mesh_layers.SimpleMeshLayer(this.getSubLayerProps({
257
- id: `canopy-${type}`,
258
- data: grouped[type],
422
+ id: layerId,
423
+ data,
259
424
  mesh,
260
425
  getPosition: (d) => {
261
426
  const pos = getPosition(d);
262
427
  const elevation = getElevation(d) || 0;
263
428
  const h = getHeight(d) * sizeScale;
264
429
  const f = getTrunkHeightFraction(d);
265
- return [pos[0], pos[1], elevation + h * f];
430
+ const canopyH = h * (1 - f);
431
+ return [pos[0], pos[1], elevation + h * f - canopyH * CANOPY_TRUNK_OVERLAP];
266
432
  },
267
433
  getScale: (d) => {
434
+ const pos = getPosition(d);
268
435
  const h = getHeight(d) * sizeScale;
269
436
  const f = getTrunkHeightFraction(d);
270
437
  const r = getCanopyRadius(d) * sizeScale;
271
- return [r, r, h * (1 - f)];
438
+ const seed = positionSeed(pos[0], pos[1]);
439
+ const sx = 1 + ((seed & 65535) / 65535 - 0.5) * 0.3;
440
+ const sy = 1 + ((seed >>> 16 & 65535) / 65535 - 0.5) * 0.3;
441
+ return [r * sx, r * sy, h * (1 - f)];
442
+ },
443
+ getOrientation: (d) => {
444
+ const pos = getPosition(d);
445
+ const seed = positionSeed(pos[0], pos[1]);
446
+ const angle = ((seed ^ seed >>> 13) & 65535) / 65535 * 360;
447
+ return [0, angle, 0];
272
448
  },
273
449
  getColor: (d) => {
274
450
  const explicit = getCanopyColor(d);
@@ -278,12 +454,17 @@ var TreeLayer = class extends import_core.CompositeLayer {
278
454
  return DEFAULT_CANOPY_COLORS[type][season];
279
455
  },
280
456
  pickable: this.props.pickable,
281
- material: { ambient: 0.4, diffuse: 0.7, shininess: 12 }
457
+ material: { ambient: 0.55, diffuse: 0.55, shininess: 0 },
458
+ updateTriggers: {
459
+ getPosition: sizeScale,
460
+ getScale: sizeScale,
461
+ getOrientation: sizeScale
462
+ }
282
463
  }));
283
464
  }
284
465
  renderLayers() {
285
- const { getPosition, getElevation, getTreeType, getHeight, getTrunkHeightFraction, getTrunkRadius, getTrunkColor, sizeScale } = this.props;
286
- const { grouped } = this.state;
466
+ const { getPosition, getElevation, getTreeType, getHeight, getTrunkHeightFraction, getTrunkRadius, getTrunkColor, getBranchLevels, sizeScale } = this.props;
467
+ const { grouped, pineMeshes, liveCropPoints, droppedCropPoints } = this.state;
287
468
  const trunkLayer = new import_mesh_layers.SimpleMeshLayer(this.getSubLayerProps({
288
469
  id: "trunks",
289
470
  data: this.props.data,
@@ -306,10 +487,45 @@ var TreeLayer = class extends import_core.CompositeLayer {
306
487
  return DEFAULT_TRUNK_COLORS[type] ?? DEFAULT_TRUNK_COLORS.pine;
307
488
  },
308
489
  pickable: this.props.pickable,
309
- material: { ambient: 0.35, diffuse: 0.6, shininess: 8 }
490
+ material: { ambient: 0.45, diffuse: 0.55, shininess: 4 },
491
+ updateTriggers: { getScale: sizeScale }
310
492
  }));
311
- const canopyLayers = ALL_TREE_TYPES.filter((type) => grouped[type].length > 0).map((type) => this._buildCanopyLayer(type));
312
- return [trunkLayer, ...canopyLayers];
493
+ 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}`));
494
+ const pineCanopies = Object.entries(pineMeshes).flatMap(([levelStr, mesh]) => {
495
+ const levels = Number(levelStr);
496
+ const pineData = grouped.pine.filter(
497
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
498
+ (d) => Math.max(1, Math.min(5, Math.round(getBranchLevels(d)))) === levels
499
+ );
500
+ return pineData.length > 0 ? [this._buildCanopyLayer("pine", mesh, pineData, `canopy-pine-${levels}`)] : [];
501
+ });
502
+ const canopyLayers = [...nonPineCanopies, ...pineCanopies];
503
+ const cropLayers = [];
504
+ if (liveCropPoints.length > 0) {
505
+ cropLayers.push(new import_mesh_layers.SimpleMeshLayer(this.getSubLayerProps({
506
+ id: "live-crops",
507
+ data: liveCropPoints,
508
+ mesh: CROP_MESH,
509
+ getPosition: (d) => d.position,
510
+ getScale: (d) => [d.scale, d.scale, d.scale],
511
+ getColor: (d) => d.color,
512
+ pickable: false,
513
+ material: { ambient: 0.5, diffuse: 0.8, shininess: 40 }
514
+ })));
515
+ }
516
+ if (droppedCropPoints.length > 0) {
517
+ cropLayers.push(new import_mesh_layers.SimpleMeshLayer(this.getSubLayerProps({
518
+ id: "dropped-crops",
519
+ data: droppedCropPoints,
520
+ mesh: CROP_MESH,
521
+ getPosition: (d) => d.position,
522
+ getScale: (d) => [d.scale, d.scale, d.scale],
523
+ getColor: (d) => d.color,
524
+ pickable: false,
525
+ material: { ambient: 0.6, diffuse: 0.5, shininess: 10 }
526
+ })));
527
+ }
528
+ return [trunkLayer, ...canopyLayers, ...cropLayers];
313
529
  }
314
530
  };
315
531
  __publicField(TreeLayer, "layerName", "TreeLayer");