@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 +127 -13
- package/dist/index.cjs +249 -33
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/tree-layer/tree-geometry.d.ts +19 -2
- package/dist/tree-layer/tree-geometry.d.ts.map +1 -1
- package/dist/tree-layer/tree-geometry.js +109 -18
- package/dist/tree-layer/tree-geometry.js.map +1 -1
- package/dist/tree-layer/tree-layer.d.ts +58 -4
- package/dist/tree-layer/tree-layer.d.ts.map +1 -1
- package/dist/tree-layer/tree-layer.js +231 -28
- package/dist/tree-layer/tree-layer.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +1 -1
- package/src/tree-layer/tree-geometry.ts +119 -18
- package/src/tree-layer/tree-layer.ts +351 -28
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# @deck.gl-community/three
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@deck.gl-community/three)
|
|
4
|
+
[](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
|
|
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
|
-
|
|
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,
|
|
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` |
|
|
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
|
-
|
|
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
|
-
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Types
|
|
125
|
+
|
|
126
|
+
### `TreeType`
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
type TreeType = 'pine' | 'oak' | 'palm' | 'birch' | 'cherry';
|
|
130
|
+
```
|
|
78
131
|
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
104
|
-
const
|
|
105
|
-
const
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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
|
-
/**
|
|
247
|
-
|
|
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:
|
|
258
|
-
data
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
490
|
+
material: { ambient: 0.45, diffuse: 0.55, shininess: 4 },
|
|
491
|
+
updateTriggers: { getScale: sizeScale }
|
|
310
492
|
}));
|
|
311
|
-
const
|
|
312
|
-
|
|
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");
|