@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
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
createOakCanopyMesh,
|
|
12
12
|
createPalmCanopyMesh,
|
|
13
13
|
createBirchCanopyMesh,
|
|
14
|
-
createCherryCanopyMesh
|
|
14
|
+
createCherryCanopyMesh,
|
|
15
|
+
createCropMesh
|
|
15
16
|
} from './tree-geometry';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -24,6 +25,31 @@ export type TreeType = 'pine' | 'oak' | 'palm' | 'birch' | 'cherry';
|
|
|
24
25
|
/** Season that drives default canopy colour when no explicit colour is supplied. */
|
|
25
26
|
export type Season = 'spring' | 'summer' | 'autumn' | 'winter';
|
|
26
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Crop configuration for a single tree.
|
|
30
|
+
*
|
|
31
|
+
* Pass this from `getCrop` to render small spherical crop points on the tree
|
|
32
|
+
* and/or scattered on the ground around it. Works for fruit, nuts, or flowers
|
|
33
|
+
* (flowering stage is expressed simply as a flower-coloured crop config).
|
|
34
|
+
*
|
|
35
|
+
* Positions are randomised deterministically from the tree's geographic
|
|
36
|
+
* coordinates, so they are stable across re-renders.
|
|
37
|
+
*/
|
|
38
|
+
export type CropConfig = {
|
|
39
|
+
/** Colour of each crop sphere [r, g, b, a]. */
|
|
40
|
+
color: Color;
|
|
41
|
+
/** Number of crop spheres placed in the outer canopy volume (live/in-tree crops). */
|
|
42
|
+
count: number;
|
|
43
|
+
/**
|
|
44
|
+
* Number of crop spheres scattered on the ground within the canopy footprint
|
|
45
|
+
* (dropped/fallen crops).
|
|
46
|
+
* @default 0
|
|
47
|
+
*/
|
|
48
|
+
droppedCount?: number;
|
|
49
|
+
/** Radius of each individual crop sphere in metres. */
|
|
50
|
+
radius: number;
|
|
51
|
+
};
|
|
52
|
+
|
|
27
53
|
// ---------------------------------------------------------------------------
|
|
28
54
|
// Default colours
|
|
29
55
|
// ---------------------------------------------------------------------------
|
|
@@ -85,8 +111,151 @@ const CANOPY_MESHES: Record<TreeType, ReturnType<typeof createTrunkMesh>> = {
|
|
|
85
111
|
cherry: createCherryCanopyMesh()
|
|
86
112
|
};
|
|
87
113
|
|
|
114
|
+
const CROP_MESH = createCropMesh();
|
|
115
|
+
|
|
88
116
|
const ALL_TREE_TYPES: TreeType[] = ['pine', 'oak', 'palm', 'birch', 'cherry'];
|
|
89
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Fraction of canopy height by which the canopy mesh is lowered into the trunk.
|
|
120
|
+
* Hides the trunk-top disk that would otherwise peek above the canopy base.
|
|
121
|
+
* 0.22 means the canopy base sits 22% of canopy-height below the trunk top.
|
|
122
|
+
*/
|
|
123
|
+
const CANOPY_TRUNK_OVERLAP = 0.22;
|
|
124
|
+
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Crop helpers
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/** splitmix32 — fast, high-quality seeded PRNG returning values in [0, 1). */
|
|
130
|
+
function createRng(seed: number): () => number {
|
|
131
|
+
let s = seed >>> 0;
|
|
132
|
+
return (): number => {
|
|
133
|
+
s = (s + 0x9e3779b9) | 0;
|
|
134
|
+
let t = s ^ (s >>> 16);
|
|
135
|
+
t = Math.imul(t, 0x21f0aaad);
|
|
136
|
+
t ^= t >>> 15;
|
|
137
|
+
t = Math.imul(t, 0x735a2d97);
|
|
138
|
+
return ((t ^ (t >>> 15)) >>> 0) / 4294967296;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Deterministic integer seed derived from a geographic position. */
|
|
143
|
+
function positionSeed(lng: number, lat: number): number {
|
|
144
|
+
return ((Math.round(lng * 10000) * 92821) ^ (Math.round(lat * 10000) * 65537)) >>> 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const DEG_PER_METER_LAT = 1 / 111320;
|
|
148
|
+
|
|
149
|
+
function lngDegreesPerMeter(latDeg: number): number {
|
|
150
|
+
return 1 / (111320 * Math.cos((latDeg * Math.PI) / 180));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Internal flat record for a single rendered crop sphere. */
|
|
154
|
+
type CropPoint = {
|
|
155
|
+
position: [number, number, number];
|
|
156
|
+
color: Color;
|
|
157
|
+
scale: number;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Expand live crop positions so they straddle the canopy surface.
|
|
162
|
+
*
|
|
163
|
+
* The canopy mesh is a SphereGeometry(0.5, …) which, after SimpleMeshLayer
|
|
164
|
+
* applies getScale = [r, r, H], has a true XY radius of 0.5 * r and a true
|
|
165
|
+
* Z half-height of 0.5 * H. Crops are placed at 85–110 % of those real
|
|
166
|
+
* dimensions so most of each sphere sits just outside the canopy surface.
|
|
167
|
+
*
|
|
168
|
+
* Positions are seeded from the tree's geographic coordinates so they are
|
|
169
|
+
* stable across re-renders.
|
|
170
|
+
*/
|
|
171
|
+
function expandLiveCropPoints(opts: {
|
|
172
|
+
lng: number;
|
|
173
|
+
lat: number;
|
|
174
|
+
elevation: number;
|
|
175
|
+
height: number;
|
|
176
|
+
trunkFraction: number;
|
|
177
|
+
canopyRadius: number;
|
|
178
|
+
cropConfig: CropConfig;
|
|
179
|
+
out: CropPoint[];
|
|
180
|
+
}): void {
|
|
181
|
+
const {lng, lat, elevation, height, trunkFraction, canopyRadius, cropConfig, out} = opts;
|
|
182
|
+
if (cropConfig.count <= 0) return;
|
|
183
|
+
|
|
184
|
+
// Actual canopy sphere radii after SimpleMeshLayer scaling.
|
|
185
|
+
// SphereGeometry has unit radius 0.5, so world radius = 0.5 * getScale component.
|
|
186
|
+
const rxy = canopyRadius * 0.5;
|
|
187
|
+
const canopyH = height * (1 - trunkFraction);
|
|
188
|
+
const rz = canopyH * 0.5;
|
|
189
|
+
// Canopy position is lowered by CANOPY_TRUNK_OVERLAP to hide the trunk-top disk
|
|
190
|
+
const canopyCenterZ = elevation + height * trunkFraction - canopyH * CANOPY_TRUNK_OVERLAP + rz;
|
|
191
|
+
|
|
192
|
+
const dLng = lngDegreesPerMeter(lat);
|
|
193
|
+
const rng = createRng(positionSeed(lng, lat));
|
|
194
|
+
|
|
195
|
+
for (let i = 0; i < cropConfig.count; i++) {
|
|
196
|
+
const theta = rng() * Math.PI * 2;
|
|
197
|
+
// Exclude top and bottom caps so crops never crown the canopy or hang below.
|
|
198
|
+
// cos(phi) in [-0.80, 0.80] → phi from ~37° to ~143° (equatorial band).
|
|
199
|
+
const cosPhi = -0.8 + rng() * 1.6;
|
|
200
|
+
const sinPhi = Math.sqrt(Math.max(0, 1 - cosPhi * cosPhi));
|
|
201
|
+
// 90–102 % of canopy radius: crops sit just at/inside the surface, tips barely poke out
|
|
202
|
+
const radFrac = 0.9 + rng() * 0.12;
|
|
203
|
+
|
|
204
|
+
const dx = rxy * radFrac * sinPhi * Math.cos(theta);
|
|
205
|
+
const dy = rxy * radFrac * sinPhi * Math.sin(theta);
|
|
206
|
+
const dz = rz * radFrac * cosPhi;
|
|
207
|
+
|
|
208
|
+
out.push({
|
|
209
|
+
position: [lng + dx * dLng, lat + dy * DEG_PER_METER_LAT, canopyCenterZ + dz],
|
|
210
|
+
color: cropConfig.color,
|
|
211
|
+
scale: cropConfig.radius
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Expand dropped crop positions uniformly across the ground disk within the
|
|
218
|
+
* canopy footprint. Uses a separate seed offset from live crops so that
|
|
219
|
+
* changing `count` does not affect dropped positions.
|
|
220
|
+
*/
|
|
221
|
+
function expandDroppedCropPoints(opts: {
|
|
222
|
+
lng: number;
|
|
223
|
+
lat: number;
|
|
224
|
+
elevation: number;
|
|
225
|
+
canopyRadius: number;
|
|
226
|
+
cropConfig: CropConfig;
|
|
227
|
+
out: CropPoint[];
|
|
228
|
+
}): void {
|
|
229
|
+
const {lng, lat, elevation, canopyRadius, cropConfig, out} = opts;
|
|
230
|
+
const droppedCount = cropConfig.droppedCount ?? 0;
|
|
231
|
+
if (droppedCount <= 0) return;
|
|
232
|
+
|
|
233
|
+
// Actual canopy footprint radius (see note in expandLiveCropPoints)
|
|
234
|
+
const footprintRadius = canopyRadius * 0.5;
|
|
235
|
+
const dLng = lngDegreesPerMeter(lat);
|
|
236
|
+
// XOR with a constant so the dropped sequence is independent of the live one
|
|
237
|
+
const rng = createRng(positionSeed(lng, lat) ^ 0x1a2b3c4d);
|
|
238
|
+
|
|
239
|
+
// Dropped crops are semi-transparent so they read as fallen/decaying
|
|
240
|
+
const c = cropConfig.color as number[];
|
|
241
|
+
const droppedColor: Color = [c[0], c[1], c[2], Math.round((c[3] ?? 255) * 0.45)] as Color;
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < droppedCount; i++) {
|
|
244
|
+
const theta = rng() * Math.PI * 2;
|
|
245
|
+
// sqrt for uniform-area disk sampling
|
|
246
|
+
const dist = Math.sqrt(rng()) * footprintRadius;
|
|
247
|
+
|
|
248
|
+
const dx = dist * Math.cos(theta);
|
|
249
|
+
const dy = dist * Math.sin(theta);
|
|
250
|
+
|
|
251
|
+
out.push({
|
|
252
|
+
position: [lng + dx * dLng, lat + dy * DEG_PER_METER_LAT, elevation + 0.05],
|
|
253
|
+
color: droppedColor,
|
|
254
|
+
scale: cropConfig.radius
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
90
259
|
// ---------------------------------------------------------------------------
|
|
91
260
|
// Props
|
|
92
261
|
// ---------------------------------------------------------------------------
|
|
@@ -163,6 +332,23 @@ type _TreeLayerProps<DataT> = {
|
|
|
163
332
|
*/
|
|
164
333
|
getBranchLevels?: (d: DataT) => number;
|
|
165
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Optional crop configuration for this tree.
|
|
337
|
+
*
|
|
338
|
+
* Return a `CropConfig` to render small spherical crop points in the outer
|
|
339
|
+
* canopy volume (live crops) and/or scattered on the ground around the trunk
|
|
340
|
+
* (dropped crops). Return `null` to show no crops for this tree.
|
|
341
|
+
*
|
|
342
|
+
* The same accessor can express fruit, nuts, or flowering stage — pass
|
|
343
|
+
* flower-coloured points (e.g. `[255, 200, 220, 255]`) for a blossom effect.
|
|
344
|
+
*
|
|
345
|
+
* Crop positions are randomised deterministically from the tree's geographic
|
|
346
|
+
* coordinates; they are stable across re-renders.
|
|
347
|
+
*
|
|
348
|
+
* @default null (no crops)
|
|
349
|
+
*/
|
|
350
|
+
getCrop?: (d: DataT) => CropConfig | null;
|
|
351
|
+
|
|
166
352
|
/**
|
|
167
353
|
* Global size multiplier applied to all dimensions.
|
|
168
354
|
* @default 1
|
|
@@ -184,6 +370,7 @@ const defaultProps: DefaultProps<TreeLayerProps<unknown>> = {
|
|
|
184
370
|
getCanopyColor: {type: 'accessor', value: (_d: any) => null},
|
|
185
371
|
getSeason: {type: 'accessor', value: (_d: any) => 'summer' as Season},
|
|
186
372
|
getBranchLevels: {type: 'accessor', value: (_d: any) => 3},
|
|
373
|
+
getCrop: {type: 'accessor', value: (_d: any) => null},
|
|
187
374
|
sizeScale: {type: 'number', value: 1, min: 0}
|
|
188
375
|
};
|
|
189
376
|
|
|
@@ -194,6 +381,8 @@ const defaultProps: DefaultProps<TreeLayerProps<unknown>> = {
|
|
|
194
381
|
type TreeLayerState = {
|
|
195
382
|
grouped: Record<TreeType, unknown[]>;
|
|
196
383
|
pineMeshes: Record<number, ReturnType<typeof createPineCanopyMesh>>;
|
|
384
|
+
liveCropPoints: CropPoint[];
|
|
385
|
+
droppedCropPoints: CropPoint[];
|
|
197
386
|
};
|
|
198
387
|
|
|
199
388
|
// ---------------------------------------------------------------------------
|
|
@@ -216,6 +405,7 @@ type TreeLayerState = {
|
|
|
216
405
|
* - Trunk and canopy radii (`getTrunkRadius`, `getCanopyRadius`)
|
|
217
406
|
* - Explicit or season-driven colours (`getTrunkColor`, `getCanopyColor`, `getSeason`)
|
|
218
407
|
* - Pine tier density (`getBranchLevels`)
|
|
408
|
+
* - Crop / fruit / flower visualisation (`getCrop`)
|
|
219
409
|
* - Global scale factor (`sizeScale`)
|
|
220
410
|
*/
|
|
221
411
|
export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends CompositeLayer<
|
|
@@ -229,13 +419,27 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
229
419
|
initializeState() {
|
|
230
420
|
this.state = {
|
|
231
421
|
grouped: {pine: [], oak: [], palm: [], birch: [], cherry: []},
|
|
232
|
-
pineMeshes: {}
|
|
422
|
+
pineMeshes: {},
|
|
423
|
+
liveCropPoints: [],
|
|
424
|
+
droppedCropPoints: []
|
|
233
425
|
};
|
|
234
426
|
}
|
|
235
427
|
|
|
236
|
-
updateState({props, changeFlags}) {
|
|
237
|
-
if (changeFlags.dataChanged || changeFlags.updateTriggersChanged) {
|
|
238
|
-
const {
|
|
428
|
+
updateState({props, oldProps, changeFlags}) {
|
|
429
|
+
if (changeFlags.dataChanged || changeFlags.propsChanged || changeFlags.updateTriggersChanged) {
|
|
430
|
+
const {
|
|
431
|
+
data,
|
|
432
|
+
getTreeType,
|
|
433
|
+
getBranchLevels,
|
|
434
|
+
getCrop,
|
|
435
|
+
getPosition,
|
|
436
|
+
getElevation,
|
|
437
|
+
getHeight,
|
|
438
|
+
getTrunkHeightFraction,
|
|
439
|
+
getCanopyRadius,
|
|
440
|
+
sizeScale
|
|
441
|
+
} = props;
|
|
442
|
+
|
|
239
443
|
const grouped: Record<TreeType, DataT[]> = {
|
|
240
444
|
pine: [],
|
|
241
445
|
oak: [],
|
|
@@ -244,24 +448,71 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
244
448
|
cherry: []
|
|
245
449
|
};
|
|
246
450
|
|
|
247
|
-
// Build per-level pine mesh cache
|
|
248
451
|
const pineMeshes: Record<number, ReturnType<typeof createPineCanopyMesh>> = {};
|
|
452
|
+
const liveCropPoints: CropPoint[] = [];
|
|
453
|
+
const droppedCropPoints: CropPoint[] = [];
|
|
249
454
|
|
|
250
455
|
for (const d of data as DataT[]) {
|
|
251
456
|
const type = getTreeType(d) as TreeType;
|
|
252
457
|
if (grouped[type]) grouped[type].push(d);
|
|
458
|
+
|
|
253
459
|
if (type === 'pine') {
|
|
254
460
|
const levels = Math.max(1, Math.min(5, Math.round(getBranchLevels(d) as number)));
|
|
255
461
|
pineMeshes[levels] ??= createPineCanopyMesh(levels);
|
|
256
462
|
}
|
|
463
|
+
|
|
464
|
+
const cropConfig = getCrop(d);
|
|
465
|
+
if (cropConfig) {
|
|
466
|
+
const pos = getPosition(d);
|
|
467
|
+
const lng = pos[0];
|
|
468
|
+
const lat = pos[1];
|
|
469
|
+
const elev = getElevation(d) || 0;
|
|
470
|
+
const h = getHeight(d) * sizeScale;
|
|
471
|
+
const f = getTrunkHeightFraction(d);
|
|
472
|
+
const r = getCanopyRadius(d) * sizeScale;
|
|
473
|
+
|
|
474
|
+
// Scale crop radius in lock-step with all other dimensions
|
|
475
|
+
const scaledCropConfig: CropConfig = {
|
|
476
|
+
...cropConfig,
|
|
477
|
+
radius: cropConfig.radius * sizeScale
|
|
478
|
+
};
|
|
479
|
+
expandLiveCropPoints({
|
|
480
|
+
lng,
|
|
481
|
+
lat,
|
|
482
|
+
elevation: elev,
|
|
483
|
+
height: h,
|
|
484
|
+
trunkFraction: f,
|
|
485
|
+
canopyRadius: r,
|
|
486
|
+
cropConfig: scaledCropConfig,
|
|
487
|
+
out: liveCropPoints
|
|
488
|
+
});
|
|
489
|
+
expandDroppedCropPoints({
|
|
490
|
+
lng,
|
|
491
|
+
lat,
|
|
492
|
+
elevation: elev,
|
|
493
|
+
canopyRadius: r,
|
|
494
|
+
cropConfig: scaledCropConfig,
|
|
495
|
+
out: droppedCropPoints
|
|
496
|
+
});
|
|
497
|
+
}
|
|
257
498
|
}
|
|
258
499
|
|
|
259
|
-
this.setState({grouped, pineMeshes});
|
|
500
|
+
this.setState({grouped, pineMeshes, liveCropPoints, droppedCropPoints});
|
|
260
501
|
}
|
|
261
502
|
}
|
|
262
503
|
|
|
263
|
-
/**
|
|
264
|
-
|
|
504
|
+
/**
|
|
505
|
+
* Build a single canopy sub-layer.
|
|
506
|
+
*
|
|
507
|
+
* Takes explicit `mesh`, `data`, and `layerId` so that pine trees can be
|
|
508
|
+
* split into one sub-layer per level count (each with its own mesh).
|
|
509
|
+
*/
|
|
510
|
+
private _buildCanopyLayer(
|
|
511
|
+
type: TreeType,
|
|
512
|
+
mesh: ReturnType<typeof createTrunkMesh>,
|
|
513
|
+
data: unknown[],
|
|
514
|
+
layerId: string
|
|
515
|
+
): SimpleMeshLayer {
|
|
265
516
|
const {
|
|
266
517
|
getPosition,
|
|
267
518
|
getElevation,
|
|
@@ -271,32 +522,42 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
271
522
|
getCanopyColor,
|
|
272
523
|
getSeason,
|
|
273
524
|
sizeScale
|
|
274
|
-
} = this.props;
|
|
275
|
-
const {grouped, pineMeshes} = this.state;
|
|
276
|
-
|
|
277
|
-
let mesh = CANOPY_MESHES[type];
|
|
278
|
-
if (type === 'pine') {
|
|
279
|
-
const firstLevel = Object.keys(pineMeshes)[0];
|
|
280
|
-
if (firstLevel) mesh = pineMeshes[Number(firstLevel)];
|
|
281
|
-
}
|
|
525
|
+
} = this.props;
|
|
282
526
|
|
|
283
527
|
return new SimpleMeshLayer(
|
|
284
528
|
this.getSubLayerProps({
|
|
285
|
-
id:
|
|
286
|
-
data
|
|
529
|
+
id: layerId,
|
|
530
|
+
data,
|
|
287
531
|
mesh,
|
|
288
532
|
getPosition: (d) => {
|
|
289
533
|
const pos = getPosition(d);
|
|
290
534
|
const elevation = getElevation(d) || 0;
|
|
291
535
|
const h = getHeight(d) * sizeScale;
|
|
292
536
|
const f = getTrunkHeightFraction(d);
|
|
293
|
-
|
|
537
|
+
const canopyH = h * (1 - f);
|
|
538
|
+
return [pos[0], pos[1], elevation + h * f - canopyH * CANOPY_TRUNK_OVERLAP];
|
|
294
539
|
},
|
|
295
540
|
getScale: (d) => {
|
|
541
|
+
const pos = getPosition(d);
|
|
296
542
|
const h = getHeight(d) * sizeScale;
|
|
297
543
|
const f = getTrunkHeightFraction(d);
|
|
298
544
|
const r = getCanopyRadius(d) * sizeScale;
|
|
299
|
-
|
|
545
|
+
// Per-tree asymmetric XY scale from position hash — no two canopies
|
|
546
|
+
// are the same oval, giving organic variety with zero extra draw calls.
|
|
547
|
+
const seed = positionSeed(pos[0], pos[1]);
|
|
548
|
+
const sx = 1 + ((seed & 0xffff) / 65535 - 0.5) * 0.3;
|
|
549
|
+
const sy = 1 + (((seed >>> 16) & 0xffff) / 65535 - 0.5) * 0.3;
|
|
550
|
+
return [r * sx, r * sy, h * (1 - f)];
|
|
551
|
+
},
|
|
552
|
+
getOrientation: (d) => {
|
|
553
|
+
// Random bearing per tree: yaw (index 1) rotates around the vertical
|
|
554
|
+
// Z axis in deck.gl's [pitch, yaw, roll] convention.
|
|
555
|
+
// Pine tiers face different compass directions; bumpy canopies present
|
|
556
|
+
// a unique silhouette from every viewing angle.
|
|
557
|
+
const pos = getPosition(d);
|
|
558
|
+
const seed = positionSeed(pos[0], pos[1]);
|
|
559
|
+
const angle = (((seed ^ (seed >>> 13)) & 0xffff) / 65535) * 360;
|
|
560
|
+
return [0, angle, 0];
|
|
300
561
|
},
|
|
301
562
|
getColor: (d) => {
|
|
302
563
|
const explicit = getCanopyColor(d);
|
|
@@ -305,7 +566,12 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
305
566
|
return DEFAULT_CANOPY_COLORS[type][season];
|
|
306
567
|
},
|
|
307
568
|
pickable: this.props.pickable,
|
|
308
|
-
material: {ambient: 0.
|
|
569
|
+
material: {ambient: 0.55, diffuse: 0.55, shininess: 0},
|
|
570
|
+
updateTriggers: {
|
|
571
|
+
getPosition: sizeScale,
|
|
572
|
+
getScale: sizeScale,
|
|
573
|
+
getOrientation: sizeScale
|
|
574
|
+
}
|
|
309
575
|
})
|
|
310
576
|
);
|
|
311
577
|
}
|
|
@@ -319,10 +585,11 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
319
585
|
getTrunkHeightFraction,
|
|
320
586
|
getTrunkRadius,
|
|
321
587
|
getTrunkColor,
|
|
588
|
+
getBranchLevels,
|
|
322
589
|
sizeScale
|
|
323
590
|
} = this.props;
|
|
324
591
|
|
|
325
|
-
const {grouped} = this.state;
|
|
592
|
+
const {grouped, pineMeshes, liveCropPoints, droppedCropPoints} = this.state;
|
|
326
593
|
|
|
327
594
|
// -----------------------------------------------------------------------
|
|
328
595
|
// 1. Trunk layer — one layer for ALL tree types, shared cylinder geometry
|
|
@@ -349,17 +616,73 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
349
616
|
return DEFAULT_TRUNK_COLORS[type] ?? DEFAULT_TRUNK_COLORS.pine;
|
|
350
617
|
},
|
|
351
618
|
pickable: this.props.pickable,
|
|
352
|
-
material: {ambient: 0.
|
|
619
|
+
material: {ambient: 0.45, diffuse: 0.55, shininess: 4},
|
|
620
|
+
updateTriggers: {getScale: sizeScale}
|
|
353
621
|
})
|
|
354
622
|
);
|
|
355
623
|
|
|
356
624
|
// -----------------------------------------------------------------------
|
|
357
|
-
// 2. Canopy layers
|
|
625
|
+
// 2. Canopy layers
|
|
626
|
+
// Non-pine: one sub-layer per species.
|
|
627
|
+
// Pine: one sub-layer per branch-level count, each using its own mesh,
|
|
628
|
+
// so trees with 2/3/4 tiers never share a mismatched mesh.
|
|
358
629
|
// -----------------------------------------------------------------------
|
|
359
|
-
const
|
|
360
|
-
this._buildCanopyLayer(
|
|
630
|
+
const nonPineCanopies = ALL_TREE_TYPES.filter((t) => t !== 'pine' && grouped[t].length > 0).map(
|
|
631
|
+
(t) => this._buildCanopyLayer(t, CANOPY_MESHES[t], grouped[t], `canopy-${t}`)
|
|
361
632
|
);
|
|
362
633
|
|
|
363
|
-
|
|
634
|
+
const pineCanopies = Object.entries(pineMeshes).flatMap(([levelStr, mesh]) => {
|
|
635
|
+
const levels = Number(levelStr);
|
|
636
|
+
const pineData = grouped.pine.filter(
|
|
637
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
638
|
+
(d) => Math.max(1, Math.min(5, Math.round(getBranchLevels(d as any)))) === levels
|
|
639
|
+
);
|
|
640
|
+
return pineData.length > 0
|
|
641
|
+
? [this._buildCanopyLayer('pine', mesh, pineData, `canopy-pine-${levels}`)]
|
|
642
|
+
: [];
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
const canopyLayers = [...nonPineCanopies, ...pineCanopies];
|
|
646
|
+
|
|
647
|
+
// -----------------------------------------------------------------------
|
|
648
|
+
// 3. Crop layers — live (in canopy) and dropped (on ground)
|
|
649
|
+
// -----------------------------------------------------------------------
|
|
650
|
+
const cropLayers = [];
|
|
651
|
+
|
|
652
|
+
if (liveCropPoints.length > 0) {
|
|
653
|
+
cropLayers.push(
|
|
654
|
+
new SimpleMeshLayer(
|
|
655
|
+
this.getSubLayerProps({
|
|
656
|
+
id: 'live-crops',
|
|
657
|
+
data: liveCropPoints,
|
|
658
|
+
mesh: CROP_MESH,
|
|
659
|
+
getPosition: (d: CropPoint) => d.position,
|
|
660
|
+
getScale: (d: CropPoint) => [d.scale, d.scale, d.scale],
|
|
661
|
+
getColor: (d: CropPoint) => d.color,
|
|
662
|
+
pickable: false,
|
|
663
|
+
material: {ambient: 0.5, diffuse: 0.8, shininess: 40}
|
|
664
|
+
})
|
|
665
|
+
)
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (droppedCropPoints.length > 0) {
|
|
670
|
+
cropLayers.push(
|
|
671
|
+
new SimpleMeshLayer(
|
|
672
|
+
this.getSubLayerProps({
|
|
673
|
+
id: 'dropped-crops',
|
|
674
|
+
data: droppedCropPoints,
|
|
675
|
+
mesh: CROP_MESH,
|
|
676
|
+
getPosition: (d: CropPoint) => d.position,
|
|
677
|
+
getScale: (d: CropPoint) => [d.scale, d.scale, d.scale],
|
|
678
|
+
getColor: (d: CropPoint) => d.color,
|
|
679
|
+
pickable: false,
|
|
680
|
+
material: {ambient: 0.6, diffuse: 0.5, shininess: 10}
|
|
681
|
+
})
|
|
682
|
+
)
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
return [trunkLayer, ...canopyLayers, ...cropLayers];
|
|
364
687
|
}
|
|
365
688
|
}
|