@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 +127 -13
- package/dist/index.cjs +255 -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 +236 -28
- package/dist/tree-layer/tree-layer.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +1 -1
- package/src/tree-layer/tree-geometry.ts +119 -18
- package/src/tree-layer/tree-layer.ts +356 -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,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
|
-
/**
|
|
247
|
-
|
|
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:
|
|
258
|
-
data
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
496
|
+
material: { ambient: 0.45, diffuse: 0.55, shininess: 4 },
|
|
497
|
+
updateTriggers: { getScale: sizeScale }
|
|
310
498
|
}));
|
|
311
|
-
const
|
|
312
|
-
|
|
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");
|