@reearth/core 0.0.7-alpha.67 → 0.0.7-alpha.69
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/dist/core.js +259 -241
- package/dist/core.umd.cjs +27 -27
- package/package.json +1 -1
- package/src/engines/Cesium/core/Imagery.test.ts +120 -5
- package/src/engines/Cesium/core/Imagery.tsx +82 -9
- package/src/engines/Cesium/index.tsx +3 -1
package/package.json
CHANGED
|
@@ -228,17 +228,132 @@ test("ImageryLayers should not re-render when tiles array reference changes but
|
|
|
228
228
|
mockAdd.mockClear();
|
|
229
229
|
mockRemove.mockClear();
|
|
230
230
|
|
|
231
|
-
// Re-render with
|
|
232
|
-
const
|
|
233
|
-
{ id: "1", type: "open_street_map", opacity: 0.5 }, // opacity changed
|
|
231
|
+
// Re-render with ONLY opacity changed
|
|
232
|
+
const opacityChangedTiles: Tile[] = [
|
|
233
|
+
{ id: "1", type: "open_street_map", opacity: 0.5 }, // only opacity changed
|
|
234
234
|
];
|
|
235
235
|
|
|
236
|
-
rerender({ tiles:
|
|
236
|
+
rerender({ tiles: opacityChangedTiles });
|
|
237
237
|
|
|
238
238
|
// Wait for effects to run
|
|
239
239
|
await new Promise(resolve => setTimeout(resolve, 0));
|
|
240
240
|
|
|
241
|
-
//
|
|
241
|
+
// With our optimization, when ONLY opacity changes, the layer is NOT recreated
|
|
242
|
+
// The layer.alpha property is updated directly without removing/adding the layer
|
|
243
|
+
expect(mockRemove).not.toHaveBeenCalled();
|
|
244
|
+
expect(mockAdd).not.toHaveBeenCalled();
|
|
245
|
+
|
|
246
|
+
// Clear mocks
|
|
247
|
+
mockAdd.mockClear();
|
|
248
|
+
mockRemove.mockClear();
|
|
249
|
+
|
|
250
|
+
// Re-render with DIFFERENT tile type (requires layer recreation)
|
|
251
|
+
const differentTypeTiles: Tile[] = [
|
|
252
|
+
{ id: "1", type: "stamen_watercolor", opacity: 0.5 }, // type changed
|
|
253
|
+
];
|
|
254
|
+
|
|
255
|
+
rerender({ tiles: differentTypeTiles });
|
|
256
|
+
|
|
257
|
+
// Wait for effects to run
|
|
258
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
259
|
+
|
|
260
|
+
// When provider changes (type, url, etc.), layer IS recreated
|
|
261
|
+
expect(mockRemove).toHaveBeenCalled();
|
|
262
|
+
expect(mockAdd).toHaveBeenCalled();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("ImageryLayers should optimize opacity changes without recreating layers", async () => {
|
|
266
|
+
const tiles: Tile[] = [{ id: "1", type: "open_street_map", opacity: 1.0 }];
|
|
267
|
+
|
|
268
|
+
const { rerender } = renderHook(
|
|
269
|
+
({ tiles }: { tiles: Tile[] }) => {
|
|
270
|
+
return ImageryLayers({
|
|
271
|
+
tiles,
|
|
272
|
+
cesiumIonAccessToken: undefined,
|
|
273
|
+
customProvider: undefined,
|
|
274
|
+
onTilesChange: undefined,
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
initialProps: { tiles },
|
|
279
|
+
},
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Wait for initial render
|
|
283
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
284
|
+
|
|
285
|
+
// Clear initial render calls
|
|
286
|
+
mockAdd.mockClear();
|
|
287
|
+
mockRemove.mockClear();
|
|
288
|
+
|
|
289
|
+
// Change opacity multiple times
|
|
290
|
+
for (const opacity of [0.8, 0.6, 0.4, 0.2]) {
|
|
291
|
+
rerender({ tiles: [{ id: "1", type: "open_street_map", opacity }] });
|
|
292
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// After multiple opacity changes, layers should NEVER be removed/added
|
|
296
|
+
// The optimization updates layer.alpha directly
|
|
297
|
+
expect(mockRemove).not.toHaveBeenCalled();
|
|
298
|
+
expect(mockAdd).not.toHaveBeenCalled();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("ImageryLayers should recreate layer when customProvider changes even if tile properties are same", async () => {
|
|
302
|
+
// Use a custom provider that we can change
|
|
303
|
+
const tiles: Tile[] = [{ id: "1", type: "my_custom", opacity: 0.8 }];
|
|
304
|
+
|
|
305
|
+
const customProvider1: CustomProviderConfig = {
|
|
306
|
+
imagery: {
|
|
307
|
+
providers: [
|
|
308
|
+
{
|
|
309
|
+
id: "my_custom",
|
|
310
|
+
url: "https://tiles1.example.com/{z}/{x}/{y}.png",
|
|
311
|
+
credit: "© Example 1",
|
|
312
|
+
},
|
|
313
|
+
],
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const customProvider2: CustomProviderConfig = {
|
|
318
|
+
imagery: {
|
|
319
|
+
providers: [
|
|
320
|
+
{
|
|
321
|
+
id: "my_custom",
|
|
322
|
+
url: "https://tiles2.example.com/{z}/{x}/{y}.png", // Different URL
|
|
323
|
+
credit: "© Example 2",
|
|
324
|
+
},
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const { rerender } = renderHook(
|
|
330
|
+
({ tiles, customProvider }: { tiles: Tile[]; customProvider?: CustomProviderConfig }) => {
|
|
331
|
+
return ImageryLayers({
|
|
332
|
+
tiles,
|
|
333
|
+
cesiumIonAccessToken: undefined,
|
|
334
|
+
customProvider,
|
|
335
|
+
onTilesChange: undefined,
|
|
336
|
+
});
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
initialProps: { tiles, customProvider: customProvider1 },
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
// Wait for initial render
|
|
344
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
345
|
+
|
|
346
|
+
// Clear initial render calls
|
|
347
|
+
mockAdd.mockClear();
|
|
348
|
+
mockRemove.mockClear();
|
|
349
|
+
|
|
350
|
+
// Change the customProvider (this creates a new provider with different URL)
|
|
351
|
+
// Tile properties stay the same
|
|
352
|
+
rerender({ tiles, customProvider: customProvider2 });
|
|
353
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
354
|
+
|
|
355
|
+
// Layer SHOULD be recreated because the provider changed
|
|
356
|
+
// (even though tile properties didn't change)
|
|
242
357
|
expect(mockRemove).toHaveBeenCalled();
|
|
243
358
|
expect(mockAdd).toHaveBeenCalled();
|
|
244
359
|
});
|
|
@@ -75,11 +75,34 @@ export default function ImageryLayers({
|
|
|
75
75
|
presets: tilePresets,
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
// Store layers keyed by tile ID to allow incremental updates
|
|
79
|
+
const layersRef = useRef<
|
|
80
|
+
Map<
|
|
81
|
+
string,
|
|
82
|
+
{
|
|
83
|
+
layer: CesiumImageryLayer;
|
|
84
|
+
tile: Tile;
|
|
85
|
+
provider: Promise<ImageryProvider> | ImageryProvider;
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
>(new Map());
|
|
89
|
+
|
|
78
90
|
useEffect(() => {
|
|
79
91
|
if (!imageryLayerCollection || !scene) return;
|
|
80
92
|
|
|
81
93
|
let cancelled = false;
|
|
82
|
-
const
|
|
94
|
+
const currentTileIds = new Set(stableTiles?.map(t => t.id) || []);
|
|
95
|
+
|
|
96
|
+
// Remove layers for tiles that no longer exist
|
|
97
|
+
layersRef.current.forEach(({ layer }, id) => {
|
|
98
|
+
if (!currentTileIds.has(id)) {
|
|
99
|
+
if (imageryLayerCollection.contains(layer)) {
|
|
100
|
+
imageryLayerCollection.remove(layer);
|
|
101
|
+
}
|
|
102
|
+
layersRef.current.delete(id);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
83
106
|
// Track layers by their intended index to maintain order with async loading
|
|
84
107
|
const layersByIndex: (CesiumImageryLayer | null)[] = new Array(stableTiles?.length || 0).fill(
|
|
85
108
|
null,
|
|
@@ -108,10 +131,50 @@ export default function ImageryLayers({
|
|
|
108
131
|
scene.requestRender();
|
|
109
132
|
};
|
|
110
133
|
|
|
111
|
-
stableTiles?.forEach((
|
|
134
|
+
stableTiles?.forEach((tile, i) => {
|
|
135
|
+
const { id, zoomLevel, opacity, heatmap } = tile;
|
|
136
|
+
const existing = layersRef.current.get(id);
|
|
112
137
|
const providerOrPromise = providers[id]?.[3];
|
|
138
|
+
|
|
113
139
|
if (!providerOrPromise) return;
|
|
114
140
|
|
|
141
|
+
// Check if we can reuse the existing layer with just an opacity update
|
|
142
|
+
if (existing) {
|
|
143
|
+
const prevTile = existing.tile;
|
|
144
|
+
const prevProvider = existing.provider;
|
|
145
|
+
// Must check provider reference - if provider changed (e.g. cesiumIonAccessToken updated),
|
|
146
|
+
// the layer needs to be recreated even if tile properties are the same
|
|
147
|
+
const canReuseLayer =
|
|
148
|
+
prevProvider === providerOrPromise &&
|
|
149
|
+
prevTile.type === tile.type &&
|
|
150
|
+
prevTile.url === tile.url &&
|
|
151
|
+
prevTile.cesiumIonAssetId === tile.cesiumIonAssetId &&
|
|
152
|
+
prevTile.zoomLevel?.[0] === zoomLevel?.[0] &&
|
|
153
|
+
prevTile.zoomLevel?.[1] === zoomLevel?.[1] &&
|
|
154
|
+
prevTile.heatmap === heatmap;
|
|
155
|
+
|
|
156
|
+
if (canReuseLayer) {
|
|
157
|
+
// Only opacity might have changed - update it directly if needed
|
|
158
|
+
const nextAlpha = opacity ?? 1;
|
|
159
|
+
if (existing.layer.alpha !== nextAlpha) {
|
|
160
|
+
existing.layer.alpha = nextAlpha;
|
|
161
|
+
scene.requestRender();
|
|
162
|
+
}
|
|
163
|
+
// Update stored tile and provider for next comparison
|
|
164
|
+
existing.tile = tile;
|
|
165
|
+
existing.provider = providerOrPromise;
|
|
166
|
+
layersByIndex[i] = existing.layer;
|
|
167
|
+
reorderLayers();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Need to recreate the layer - remove the old one
|
|
172
|
+
if (imageryLayerCollection.contains(existing.layer)) {
|
|
173
|
+
imageryLayerCollection.remove(existing.layer);
|
|
174
|
+
}
|
|
175
|
+
layersRef.current.delete(id);
|
|
176
|
+
}
|
|
177
|
+
|
|
115
178
|
const doAdd = (provider: ImageryProvider) => {
|
|
116
179
|
if (!provider || cancelled || scene.isDestroyed()) return;
|
|
117
180
|
const layer = new CesiumImageryLayer(provider, {
|
|
@@ -127,7 +190,8 @@ export default function ImageryLayers({
|
|
|
127
190
|
// Always append to avoid index out of bounds
|
|
128
191
|
imageryLayerCollection.add(layer);
|
|
129
192
|
layersByIndex[i] = layer;
|
|
130
|
-
|
|
193
|
+
// Store the provider reference to detect when provider changes (e.g. token update)
|
|
194
|
+
layersRef.current.set(id, { layer, tile, provider: providerOrPromise });
|
|
131
195
|
|
|
132
196
|
// Reorder all layers after each addition
|
|
133
197
|
reorderLayers();
|
|
@@ -147,15 +211,24 @@ export default function ImageryLayers({
|
|
|
147
211
|
|
|
148
212
|
return () => {
|
|
149
213
|
cancelled = true;
|
|
150
|
-
|
|
151
|
-
|
|
214
|
+
// Don't remove layers on cleanup - they'll be managed by the next render
|
|
215
|
+
// This prevents flickering when tiles change
|
|
216
|
+
};
|
|
217
|
+
}, [providers, stableTiles, imageryLayerCollection, scene, onTilesChange]);
|
|
218
|
+
|
|
219
|
+
// Cleanup all layers on unmount
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
const layers = layersRef.current;
|
|
222
|
+
return () => {
|
|
223
|
+
if (!imageryLayerCollection || !scene || scene.isDestroyed()) return;
|
|
224
|
+
layers.forEach(({ layer }) => {
|
|
225
|
+
if (imageryLayerCollection.contains(layer)) {
|
|
152
226
|
imageryLayerCollection.remove(layer);
|
|
153
227
|
}
|
|
154
|
-
}
|
|
228
|
+
});
|
|
229
|
+
layers.clear();
|
|
155
230
|
};
|
|
156
|
-
|
|
157
|
-
// This also stabilizes `providers` since it depends on tiles in useImageryProviders.
|
|
158
|
-
}, [providers, stableTiles, imageryLayerCollection, scene, onTilesChange]);
|
|
231
|
+
}, [imageryLayerCollection, scene]);
|
|
159
232
|
|
|
160
233
|
return null;
|
|
161
234
|
}
|
|
@@ -253,7 +253,9 @@ const Cesium: React.ForwardRefRenderFunction<EngineRef, EngineProps> = (
|
|
|
253
253
|
useWebVR={!!property?.scene?.vr || undefined} // NOTE: useWebVR={false} will crash Cesium
|
|
254
254
|
debugShowFramesPerSecond={!!property?.debug?.showFramesPerSecond}
|
|
255
255
|
verticalExaggerationRelativeHeight={property?.scene?.verticalExaggerationRelativeHeight}
|
|
256
|
-
verticalExaggeration={
|
|
256
|
+
verticalExaggeration={
|
|
257
|
+
property?.terrain?.enabled ? property?.scene?.verticalExaggeration : 1
|
|
258
|
+
}
|
|
257
259
|
/>
|
|
258
260
|
<SkyBox show={property?.sky?.skyBox?.show ?? true} />
|
|
259
261
|
<Fog enabled={property?.sky?.fog?.enabled ?? true} density={property?.sky?.fog?.density} />
|