@deck.gl-community/three 9.2.5 → 9.3.1-alpha.0
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
|
@@ -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,156 @@ 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 unknown as number[];
|
|
241
|
+
const droppedColor: Color = [
|
|
242
|
+
c[0],
|
|
243
|
+
c[1],
|
|
244
|
+
c[2],
|
|
245
|
+
Math.round((c[3] ?? 255) * 0.45)
|
|
246
|
+
] as unknown as Color;
|
|
247
|
+
|
|
248
|
+
for (let i = 0; i < droppedCount; i++) {
|
|
249
|
+
const theta = rng() * Math.PI * 2;
|
|
250
|
+
// sqrt for uniform-area disk sampling
|
|
251
|
+
const dist = Math.sqrt(rng()) * footprintRadius;
|
|
252
|
+
|
|
253
|
+
const dx = dist * Math.cos(theta);
|
|
254
|
+
const dy = dist * Math.sin(theta);
|
|
255
|
+
|
|
256
|
+
out.push({
|
|
257
|
+
position: [lng + dx * dLng, lat + dy * DEG_PER_METER_LAT, elevation + 0.05],
|
|
258
|
+
color: droppedColor,
|
|
259
|
+
scale: cropConfig.radius
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
90
264
|
// ---------------------------------------------------------------------------
|
|
91
265
|
// Props
|
|
92
266
|
// ---------------------------------------------------------------------------
|
|
@@ -163,6 +337,23 @@ type _TreeLayerProps<DataT> = {
|
|
|
163
337
|
*/
|
|
164
338
|
getBranchLevels?: (d: DataT) => number;
|
|
165
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Optional crop configuration for this tree.
|
|
342
|
+
*
|
|
343
|
+
* Return a `CropConfig` to render small spherical crop points in the outer
|
|
344
|
+
* canopy volume (live crops) and/or scattered on the ground around the trunk
|
|
345
|
+
* (dropped crops). Return `null` to show no crops for this tree.
|
|
346
|
+
*
|
|
347
|
+
* The same accessor can express fruit, nuts, or flowering stage — pass
|
|
348
|
+
* flower-coloured points (e.g. `[255, 200, 220, 255]`) for a blossom effect.
|
|
349
|
+
*
|
|
350
|
+
* Crop positions are randomised deterministically from the tree's geographic
|
|
351
|
+
* coordinates; they are stable across re-renders.
|
|
352
|
+
*
|
|
353
|
+
* @default null (no crops)
|
|
354
|
+
*/
|
|
355
|
+
getCrop?: (d: DataT) => CropConfig | null;
|
|
356
|
+
|
|
166
357
|
/**
|
|
167
358
|
* Global size multiplier applied to all dimensions.
|
|
168
359
|
* @default 1
|
|
@@ -184,6 +375,7 @@ const defaultProps: DefaultProps<TreeLayerProps<unknown>> = {
|
|
|
184
375
|
getCanopyColor: {type: 'accessor', value: (_d: any) => null},
|
|
185
376
|
getSeason: {type: 'accessor', value: (_d: any) => 'summer' as Season},
|
|
186
377
|
getBranchLevels: {type: 'accessor', value: (_d: any) => 3},
|
|
378
|
+
getCrop: {type: 'accessor', value: (_d: any) => null},
|
|
187
379
|
sizeScale: {type: 'number', value: 1, min: 0}
|
|
188
380
|
};
|
|
189
381
|
|
|
@@ -194,6 +386,8 @@ const defaultProps: DefaultProps<TreeLayerProps<unknown>> = {
|
|
|
194
386
|
type TreeLayerState = {
|
|
195
387
|
grouped: Record<TreeType, unknown[]>;
|
|
196
388
|
pineMeshes: Record<number, ReturnType<typeof createPineCanopyMesh>>;
|
|
389
|
+
liveCropPoints: CropPoint[];
|
|
390
|
+
droppedCropPoints: CropPoint[];
|
|
197
391
|
};
|
|
198
392
|
|
|
199
393
|
// ---------------------------------------------------------------------------
|
|
@@ -216,6 +410,7 @@ type TreeLayerState = {
|
|
|
216
410
|
* - Trunk and canopy radii (`getTrunkRadius`, `getCanopyRadius`)
|
|
217
411
|
* - Explicit or season-driven colours (`getTrunkColor`, `getCanopyColor`, `getSeason`)
|
|
218
412
|
* - Pine tier density (`getBranchLevels`)
|
|
413
|
+
* - Crop / fruit / flower visualisation (`getCrop`)
|
|
219
414
|
* - Global scale factor (`sizeScale`)
|
|
220
415
|
*/
|
|
221
416
|
export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends CompositeLayer<
|
|
@@ -229,13 +424,27 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
229
424
|
initializeState() {
|
|
230
425
|
this.state = {
|
|
231
426
|
grouped: {pine: [], oak: [], palm: [], birch: [], cherry: []},
|
|
232
|
-
pineMeshes: {}
|
|
427
|
+
pineMeshes: {},
|
|
428
|
+
liveCropPoints: [],
|
|
429
|
+
droppedCropPoints: []
|
|
233
430
|
};
|
|
234
431
|
}
|
|
235
432
|
|
|
236
|
-
updateState({props, changeFlags}) {
|
|
237
|
-
if (changeFlags.dataChanged || changeFlags.updateTriggersChanged) {
|
|
238
|
-
const {
|
|
433
|
+
updateState({props, oldProps, changeFlags}) {
|
|
434
|
+
if (changeFlags.dataChanged || changeFlags.propsChanged || changeFlags.updateTriggersChanged) {
|
|
435
|
+
const {
|
|
436
|
+
data,
|
|
437
|
+
getTreeType,
|
|
438
|
+
getBranchLevels,
|
|
439
|
+
getCrop,
|
|
440
|
+
getPosition,
|
|
441
|
+
getElevation,
|
|
442
|
+
getHeight,
|
|
443
|
+
getTrunkHeightFraction,
|
|
444
|
+
getCanopyRadius,
|
|
445
|
+
sizeScale
|
|
446
|
+
} = props;
|
|
447
|
+
|
|
239
448
|
const grouped: Record<TreeType, DataT[]> = {
|
|
240
449
|
pine: [],
|
|
241
450
|
oak: [],
|
|
@@ -244,24 +453,71 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
244
453
|
cherry: []
|
|
245
454
|
};
|
|
246
455
|
|
|
247
|
-
// Build per-level pine mesh cache
|
|
248
456
|
const pineMeshes: Record<number, ReturnType<typeof createPineCanopyMesh>> = {};
|
|
457
|
+
const liveCropPoints: CropPoint[] = [];
|
|
458
|
+
const droppedCropPoints: CropPoint[] = [];
|
|
249
459
|
|
|
250
460
|
for (const d of data as DataT[]) {
|
|
251
461
|
const type = getTreeType(d) as TreeType;
|
|
252
462
|
if (grouped[type]) grouped[type].push(d);
|
|
463
|
+
|
|
253
464
|
if (type === 'pine') {
|
|
254
465
|
const levels = Math.max(1, Math.min(5, Math.round(getBranchLevels(d) as number)));
|
|
255
466
|
pineMeshes[levels] ??= createPineCanopyMesh(levels);
|
|
256
467
|
}
|
|
468
|
+
|
|
469
|
+
const cropConfig = getCrop(d);
|
|
470
|
+
if (cropConfig) {
|
|
471
|
+
const pos = getPosition(d);
|
|
472
|
+
const lng = pos[0];
|
|
473
|
+
const lat = pos[1];
|
|
474
|
+
const elev = getElevation(d) || 0;
|
|
475
|
+
const h = getHeight(d) * sizeScale;
|
|
476
|
+
const f = getTrunkHeightFraction(d);
|
|
477
|
+
const r = getCanopyRadius(d) * sizeScale;
|
|
478
|
+
|
|
479
|
+
// Scale crop radius in lock-step with all other dimensions
|
|
480
|
+
const scaledCropConfig: CropConfig = {
|
|
481
|
+
...cropConfig,
|
|
482
|
+
radius: cropConfig.radius * sizeScale
|
|
483
|
+
};
|
|
484
|
+
expandLiveCropPoints({
|
|
485
|
+
lng,
|
|
486
|
+
lat,
|
|
487
|
+
elevation: elev,
|
|
488
|
+
height: h,
|
|
489
|
+
trunkFraction: f,
|
|
490
|
+
canopyRadius: r,
|
|
491
|
+
cropConfig: scaledCropConfig,
|
|
492
|
+
out: liveCropPoints
|
|
493
|
+
});
|
|
494
|
+
expandDroppedCropPoints({
|
|
495
|
+
lng,
|
|
496
|
+
lat,
|
|
497
|
+
elevation: elev,
|
|
498
|
+
canopyRadius: r,
|
|
499
|
+
cropConfig: scaledCropConfig,
|
|
500
|
+
out: droppedCropPoints
|
|
501
|
+
});
|
|
502
|
+
}
|
|
257
503
|
}
|
|
258
504
|
|
|
259
|
-
this.setState({grouped, pineMeshes});
|
|
505
|
+
this.setState({grouped, pineMeshes, liveCropPoints, droppedCropPoints});
|
|
260
506
|
}
|
|
261
507
|
}
|
|
262
508
|
|
|
263
|
-
/**
|
|
264
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Build a single canopy sub-layer.
|
|
511
|
+
*
|
|
512
|
+
* Takes explicit `mesh`, `data`, and `layerId` so that pine trees can be
|
|
513
|
+
* split into one sub-layer per level count (each with its own mesh).
|
|
514
|
+
*/
|
|
515
|
+
private _buildCanopyLayer(
|
|
516
|
+
type: TreeType,
|
|
517
|
+
mesh: ReturnType<typeof createTrunkMesh>,
|
|
518
|
+
data: unknown[],
|
|
519
|
+
layerId: string
|
|
520
|
+
): SimpleMeshLayer {
|
|
265
521
|
const {
|
|
266
522
|
getPosition,
|
|
267
523
|
getElevation,
|
|
@@ -271,32 +527,42 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
271
527
|
getCanopyColor,
|
|
272
528
|
getSeason,
|
|
273
529
|
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
|
-
}
|
|
530
|
+
} = this.props;
|
|
282
531
|
|
|
283
532
|
return new SimpleMeshLayer(
|
|
284
533
|
this.getSubLayerProps({
|
|
285
|
-
id:
|
|
286
|
-
data
|
|
534
|
+
id: layerId,
|
|
535
|
+
data,
|
|
287
536
|
mesh,
|
|
288
537
|
getPosition: (d) => {
|
|
289
538
|
const pos = getPosition(d);
|
|
290
539
|
const elevation = getElevation(d) || 0;
|
|
291
540
|
const h = getHeight(d) * sizeScale;
|
|
292
541
|
const f = getTrunkHeightFraction(d);
|
|
293
|
-
|
|
542
|
+
const canopyH = h * (1 - f);
|
|
543
|
+
return [pos[0], pos[1], elevation + h * f - canopyH * CANOPY_TRUNK_OVERLAP];
|
|
294
544
|
},
|
|
295
545
|
getScale: (d) => {
|
|
546
|
+
const pos = getPosition(d);
|
|
296
547
|
const h = getHeight(d) * sizeScale;
|
|
297
548
|
const f = getTrunkHeightFraction(d);
|
|
298
549
|
const r = getCanopyRadius(d) * sizeScale;
|
|
299
|
-
|
|
550
|
+
// Per-tree asymmetric XY scale from position hash — no two canopies
|
|
551
|
+
// are the same oval, giving organic variety with zero extra draw calls.
|
|
552
|
+
const seed = positionSeed(pos[0], pos[1]);
|
|
553
|
+
const sx = 1 + ((seed & 0xffff) / 65535 - 0.5) * 0.6;
|
|
554
|
+
const sy = 1 + (((seed >>> 16) & 0xffff) / 65535 - 0.5) * 0.6;
|
|
555
|
+
return [r * sx, r * sy, h * (1 - f)];
|
|
556
|
+
},
|
|
557
|
+
getOrientation: (d) => {
|
|
558
|
+
// Random bearing + slight pitch per tree so spherical canopies show
|
|
559
|
+
// visible lean variety at typical farm viewing angles (30–60° pitch).
|
|
560
|
+
// pitch [0]: ±12°, yaw [1]: full 360°, roll [2]: 0
|
|
561
|
+
const pos = getPosition(d);
|
|
562
|
+
const seed = positionSeed(pos[0], pos[1]);
|
|
563
|
+
const yaw = (((seed ^ (seed >>> 13)) & 0xffff) / 65535) * 360;
|
|
564
|
+
const pitch = (((seed ^ (seed >>> 7)) & 0xff) / 255 - 0.5) * 24;
|
|
565
|
+
return [pitch, yaw, 0];
|
|
300
566
|
},
|
|
301
567
|
getColor: (d) => {
|
|
302
568
|
const explicit = getCanopyColor(d);
|
|
@@ -305,7 +571,12 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
305
571
|
return DEFAULT_CANOPY_COLORS[type][season];
|
|
306
572
|
},
|
|
307
573
|
pickable: this.props.pickable,
|
|
308
|
-
material: {ambient: 0.
|
|
574
|
+
material: {ambient: 0.55, diffuse: 0.55, shininess: 0},
|
|
575
|
+
updateTriggers: {
|
|
576
|
+
getPosition: sizeScale,
|
|
577
|
+
getScale: sizeScale,
|
|
578
|
+
getOrientation: sizeScale
|
|
579
|
+
}
|
|
309
580
|
})
|
|
310
581
|
);
|
|
311
582
|
}
|
|
@@ -319,10 +590,11 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
319
590
|
getTrunkHeightFraction,
|
|
320
591
|
getTrunkRadius,
|
|
321
592
|
getTrunkColor,
|
|
593
|
+
getBranchLevels,
|
|
322
594
|
sizeScale
|
|
323
595
|
} = this.props;
|
|
324
596
|
|
|
325
|
-
const {grouped} = this.state;
|
|
597
|
+
const {grouped, pineMeshes, liveCropPoints, droppedCropPoints} = this.state;
|
|
326
598
|
|
|
327
599
|
// -----------------------------------------------------------------------
|
|
328
600
|
// 1. Trunk layer — one layer for ALL tree types, shared cylinder geometry
|
|
@@ -349,17 +621,73 @@ export class TreeLayer<DataT = unknown, ExtraPropsT extends {} = {}> extends Com
|
|
|
349
621
|
return DEFAULT_TRUNK_COLORS[type] ?? DEFAULT_TRUNK_COLORS.pine;
|
|
350
622
|
},
|
|
351
623
|
pickable: this.props.pickable,
|
|
352
|
-
material: {ambient: 0.
|
|
624
|
+
material: {ambient: 0.45, diffuse: 0.55, shininess: 4},
|
|
625
|
+
updateTriggers: {getScale: sizeScale}
|
|
353
626
|
})
|
|
354
627
|
);
|
|
355
628
|
|
|
356
629
|
// -----------------------------------------------------------------------
|
|
357
|
-
// 2. Canopy layers
|
|
630
|
+
// 2. Canopy layers
|
|
631
|
+
// Non-pine: one sub-layer per species.
|
|
632
|
+
// Pine: one sub-layer per branch-level count, each using its own mesh,
|
|
633
|
+
// so trees with 2/3/4 tiers never share a mismatched mesh.
|
|
358
634
|
// -----------------------------------------------------------------------
|
|
359
|
-
const
|
|
360
|
-
this._buildCanopyLayer(
|
|
635
|
+
const nonPineCanopies = ALL_TREE_TYPES.filter((t) => t !== 'pine' && grouped[t].length > 0).map(
|
|
636
|
+
(t) => this._buildCanopyLayer(t, CANOPY_MESHES[t], grouped[t], `canopy-${t}`)
|
|
361
637
|
);
|
|
362
638
|
|
|
363
|
-
|
|
639
|
+
const pineCanopies = Object.entries(pineMeshes).flatMap(([levelStr, mesh]) => {
|
|
640
|
+
const levels = Number(levelStr);
|
|
641
|
+
const pineData = grouped.pine.filter(
|
|
642
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
643
|
+
(d) => Math.max(1, Math.min(5, Math.round(getBranchLevels(d as any)))) === levels
|
|
644
|
+
);
|
|
645
|
+
return pineData.length > 0
|
|
646
|
+
? [this._buildCanopyLayer('pine', mesh, pineData, `canopy-pine-${levels}`)]
|
|
647
|
+
: [];
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const canopyLayers = [...nonPineCanopies, ...pineCanopies];
|
|
651
|
+
|
|
652
|
+
// -----------------------------------------------------------------------
|
|
653
|
+
// 3. Crop layers — live (in canopy) and dropped (on ground)
|
|
654
|
+
// -----------------------------------------------------------------------
|
|
655
|
+
const cropLayers = [];
|
|
656
|
+
|
|
657
|
+
if (liveCropPoints.length > 0) {
|
|
658
|
+
cropLayers.push(
|
|
659
|
+
new SimpleMeshLayer(
|
|
660
|
+
this.getSubLayerProps({
|
|
661
|
+
id: 'live-crops',
|
|
662
|
+
data: liveCropPoints,
|
|
663
|
+
mesh: CROP_MESH,
|
|
664
|
+
getPosition: (d: CropPoint) => d.position,
|
|
665
|
+
getScale: (d: CropPoint) => [d.scale, d.scale, d.scale],
|
|
666
|
+
getColor: (d: CropPoint) => d.color,
|
|
667
|
+
pickable: false,
|
|
668
|
+
material: {ambient: 0.5, diffuse: 0.8, shininess: 40}
|
|
669
|
+
})
|
|
670
|
+
)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (droppedCropPoints.length > 0) {
|
|
675
|
+
cropLayers.push(
|
|
676
|
+
new SimpleMeshLayer(
|
|
677
|
+
this.getSubLayerProps({
|
|
678
|
+
id: 'dropped-crops',
|
|
679
|
+
data: droppedCropPoints,
|
|
680
|
+
mesh: CROP_MESH,
|
|
681
|
+
getPosition: (d: CropPoint) => d.position,
|
|
682
|
+
getScale: (d: CropPoint) => [d.scale, d.scale, d.scale],
|
|
683
|
+
getColor: (d: CropPoint) => d.color,
|
|
684
|
+
pickable: false,
|
|
685
|
+
material: {ambient: 0.6, diffuse: 0.5, shininess: 10}
|
|
686
|
+
})
|
|
687
|
+
)
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
return [trunkLayer, ...canopyLayers, ...cropLayers];
|
|
364
692
|
}
|
|
365
693
|
}
|