From 04fcc531a52b6a2750b9a66c48d0fa9f4276147a Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 3 Jun 2025 18:52:16 +0200 Subject: [PATCH 1/6] Cache rectangle in ClippingPolygon --- .../engine/Source/Scene/ClippingPolygon.js | 93 ++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/packages/engine/Source/Scene/ClippingPolygon.js b/packages/engine/Source/Scene/ClippingPolygon.js index c9497a2a700a..709265497be7 100644 --- a/packages/engine/Source/Scene/ClippingPolygon.js +++ b/packages/engine/Source/Scene/ClippingPolygon.js @@ -46,7 +46,90 @@ function ClippingPolygon(options) { //>>includeEnd('debug'); this._ellipsoid = options.ellipsoid ?? Ellipsoid.default; - this._positions = [...options.positions]; + this._positions = copyArrayCartesian3(options.positions); + + /** + * A copy of the input positions. + * + * This is used to detect modifications of the positions in + * coputeRectangle: The rectangle only has + * to be re-computed when these positions have changed. + * + * @type {Cartesian3[]|undefined} + * @private + */ + this._cachedPositions = undefined; + + /** + * A cached version of the rectangle that is computed in + * computeRectangle. + * + * This is only re-computed when the positions have changed, as + * determined by comparing the _positions to the + * _cachedPositions + * + * @type {Rectangle|undefined} + * @private + */ + this._cachedRectangle = undefined; +} + +/** + * Returns a deep copy of the given array. + * + * If the input is undefined, then undefined is returned. + * + * Otherwise, the result will be a copy of the given array, where + * each element is copied with Cartesian3.clone. + * + * @param {Cartesian3[]|undefined} input The input array + * @returns {Cartesian3[]|undefined} The copy + */ +function copyArrayCartesian3(input) { + if (!defined(input)) { + return undefined; + } + const n = input.length; + const output = Array(n); + for (let i = 0; i < n; i++) { + output[i] = Cartesian3.clone(input[i]); + } + return output; +} + +/** + * Returns whether the given arrays are component-wise equal. + * + * When both arrays are undefined, then true is returned. + * When only one array is defined, or they are both defined but have + * different lengths, then false is returned. + * + * Otherwise, returns whether the corresponding elements of the arrays + * are equal, as of Cartesian3.equals. + * + * @param {Cartesian3[]|undefined} a The first array + * @param {Cartesian3[]|undefined} b The second array + * @returns {boolean} Whether the arrays are equal + */ +function equalsArrayCartesian3(a, b) { + if (!defined(a) && !defined(b)) { + return true; + } + if (defined(a) !== defined(b)) { + return false; + } + if (a.length !== b.length) { + return false; + } + const n = a.length; + for (let i = 0; i < n; i++) { + const ca = a[i]; + const cb = b[i]; + if (!Cartesian3.equals(ca, cb)) { + return false; + } + } + return true; } Object.defineProperties(ClippingPolygon.prototype, { @@ -138,12 +221,18 @@ ClippingPolygon.equals = function (left, right) { * @returns {Rectangle} The result rectangle */ ClippingPolygon.prototype.computeRectangle = function (result) { - return PolygonGeometry.computeRectangleFromPositions( + if (equalsArrayCartesian3(this._positions, this._cachedPositions)) { + return Rectangle.clone(this._cachedRectangle, result); + } + const rectangle = PolygonGeometry.computeRectangleFromPositions( this.positions, this.ellipsoid, undefined, result, ); + this._cachedPositions = copyArrayCartesian3(this._positions); + this._cachedRectangle = Rectangle.clone(rectangle); + return rectangle; }; const scratchRectangle = new Rectangle(); From 2c9215f8c0167d2b682f29e65487d22a9b5769a9 Mon Sep 17 00:00:00 2001 From: rema Date: Tue, 23 Sep 2025 07:47:36 +0200 Subject: [PATCH 2/6] Add declusteredEvent to EntityCluster Fixes #5760 by adding declusteredEvent that provides both clustered and declustered entities, plus new API methods for accessing clustering state. Maintains backward compatibility. --- CONTRIBUTORS.md | 1 + .../Source/DataSources/EntityCluster.js | 162 +++++++++++++++++- 2 files changed, 161 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index be84d97baf2d..bf6315bf59ab 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -431,3 +431,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute to Cesiu - [Easy Mahaffey](https://github.com/easymaahffey) - [Pamela Augustine](https://github.com/pamelaAugustine) - [宋时旺](https://github.com/BlockCnFuture) +- [Alexander Remer](https://github.com/Oko-Tester) diff --git a/packages/engine/Source/DataSources/EntityCluster.js b/packages/engine/Source/DataSources/EntityCluster.js index ff572aaa975e..24b9ca554d6d 100644 --- a/packages/engine/Source/DataSources/EntityCluster.js +++ b/packages/engine/Source/DataSources/EntityCluster.js @@ -67,6 +67,11 @@ function EntityCluster(options) { this._clusterEvent = new Event(); + this._declusteredEvent = new Event(); + this._allProcessedEntities = []; + this._lastClusteredEntities = []; + this._lastDeclusteredEntities = []; + /** * Determines if entities in this collection will be shown. * @@ -127,6 +132,10 @@ function getBoundingBox(item, coord, pixelRange, entityCluster, result) { function addNonClusteredItem(item, entityCluster) { item.clusterShow = true; + if (defined(item.id)) { + entityCluster._lastDeclusteredEntities.push(item.id); + } + if ( !defined(item._labelCollection) && defined(item.id) && @@ -157,7 +166,16 @@ function addCluster(position, numPoints, ids, entityCluster) { cluster.point.position = position; + entityCluster._lastClusteredEntities = + entityCluster._lastClusteredEntities.concat(ids); + entityCluster._clusterEvent.raiseEvent(ids, cluster); + + entityCluster._declusteredEvent.raiseEvent({ + clustered: ids, + declustered: entityCluster._lastDeclusteredEntities.slice(), + cluster: cluster, + }); } function hasLabelIndex(entityCluster, entityId) { @@ -207,6 +225,10 @@ function getScreenSpacePositions( continue; } + if (defined(item.id)) { + entityCluster._allProcessedEntities.push(item.id); + } + points.push({ index: i, collection: collection, @@ -216,7 +238,7 @@ function getScreenSpacePositions( } } -const pointBoundinRectangleScratch = new BoundingRectangle(); +const pointBoundingRectangleScratch = new BoundingRectangle(); const totalBoundingRectangleScratch = new BoundingRectangle(); const neighborBoundingRectangleScratch = new BoundingRectangle(); @@ -226,6 +248,10 @@ function createDeclutterCallback(entityCluster) { return; } + entityCluster._allProcessedEntities = []; + entityCluster._lastClusteredEntities = []; + entityCluster._lastDeclusteredEntities = []; + const scene = entityCluster._scene; const labelCollection = entityCluster._labelCollection; @@ -240,6 +266,11 @@ function createDeclutterCallback(entityCluster) { !entityCluster._clusterLabels && !entityCluster._clusterPoints) ) { + entityCluster._declusteredEvent.raiseEvent({ + clustered: [], + declustered: [], + cluster: null, + }); return; } @@ -414,7 +445,7 @@ function createDeclutterCallback(entityCluster) { point.coord, pixelRange, entityCluster, - pointBoundinRectangleScratch, + pointBoundingRectangleScratch, ); const totalBBox = BoundingRectangle.clone( bbox, @@ -485,6 +516,18 @@ function createDeclutterCallback(entityCluster) { } } + if ( + entityCluster._lastClusteredEntities.length > 0 || + entityCluster._lastDeclusteredEntities.length > 0 + ) { + entityCluster._declusteredEvent.raiseEvent({ + clustered: entityCluster._lastClusteredEntities.slice(), + declustered: entityCluster._lastDeclusteredEntities.slice(), + cluster: null, + allProcessed: entityCluster._allProcessedEntities.slice(), + }); + } + if (clusteredLabelCollection.length === 0) { clusteredLabelCollection.destroy(); entityCluster._clusterLabelCollection = undefined; @@ -567,6 +610,16 @@ Object.defineProperties(EntityCluster.prototype, { return this._clusterEvent; }, }, + /** + * Gets the event that will be raised when clustering is processed, including both clustered and declustered entities. + * @memberof EntityCluster.prototype + * @type {Event} + */ + declusteredEvent: { + get: function () { + return this._declusteredEvent; + }, + }, /** * Gets or sets whether clustering billboard entities is enabled. * @memberof EntityCluster.prototype @@ -852,6 +905,35 @@ function updateEnable(entityCluster) { return; } + const allVisibleEntities = []; + + if (defined(entityCluster._labelCollection)) { + for (let i = 0; i < entityCluster._labelCollection.length; i++) { + const label = entityCluster._labelCollection.get(i); + if (defined(label.id) && label.show) { + allVisibleEntities.push(label.id); + } + } + } + + if (defined(entityCluster._billboardCollection)) { + for (let i = 0; i < entityCluster._billboardCollection.length; i++) { + const billboard = entityCluster._billboardCollection.get(i); + if (defined(billboard.id) && billboard.show) { + allVisibleEntities.push(billboard.id); + } + } + } + + if (defined(entityCluster._pointCollection)) { + for (let i = 0; i < entityCluster._pointCollection.length; i++) { + const point = entityCluster._pointCollection.get(i); + if (defined(point.id) && point.show) { + allVisibleEntities.push(point.id); + } + } + } + if (defined(entityCluster._clusterLabelCollection)) { entityCluster._clusterLabelCollection.destroy(); } @@ -869,6 +951,32 @@ function updateEnable(entityCluster) { disableCollectionClustering(entityCluster._labelCollection); disableCollectionClustering(entityCluster._billboardCollection); disableCollectionClustering(entityCluster._pointCollection); + + if (allVisibleEntities.length > 0) { + const uniqueEntities = [...new Set(allVisibleEntities)]; + + entityCluster._declusteredEvent.raiseEvent({ + clustered: [], + declustered: uniqueEntities, + cluster: null, + allProcessed: uniqueEntities, + }); + + entityCluster._lastClusteredEntities = []; + entityCluster._lastDeclusteredEntities = uniqueEntities.slice(); + entityCluster._allProcessedEntities = uniqueEntities.slice(); + } else { + entityCluster._declusteredEvent.raiseEvent({ + clustered: [], + declustered: [], + cluster: null, + allProcessed: [], + }); + + entityCluster._lastClusteredEntities = []; + entityCluster._lastDeclusteredEntities = []; + entityCluster._allProcessedEntities = []; + } } /** @@ -998,9 +1106,37 @@ EntityCluster.prototype.destroy = function () { this._pixelRangeDirty = false; this._minimumClusterSizeDirty = false; + this._allProcessedEntities = []; + this._lastClusteredEntities = []; + this._lastDeclusteredEntities = []; + return undefined; }; +/** + * Returns the last set of clustered entities from the most recent clustering operation. + * @returns {Entity[]} Array of entities that were clustered + */ +EntityCluster.prototype.getLastClusteredEntities = function () { + return this._lastClusteredEntities.slice(); +}; + +/** + * Returns the last set of declustered entities from the most recent clustering operation. + * @returns {Entity[]} Array of entities that were not clustered + */ +EntityCluster.prototype.getLastDeclusteredEntities = function () { + return this._lastDeclusteredEntities.slice(); +}; + +/** + * Returns all entities that were processed in the most recent clustering operation. + * @returns {Entity[]} Array of all processed entities + */ +EntityCluster.prototype.getAllProcessedEntities = function () { + return this._allProcessedEntities.slice(); +}; + /** * A event listener function used to style clusters. * @callback EntityCluster.newClusterCallback @@ -1019,4 +1155,26 @@ EntityCluster.prototype.destroy = function () { * cluster.label.text = entities.length.toLocaleString(); * }); */ + +/** + * A event listener function for enhanced clustering information. + * @callback EntityCluster.declusteredCallback + * + * @param {object} clusteringData An object containing clustering information. + * @param {Entity[]} clusteringData.clustered An array of entities that were clustered. + * @param {Entity[]} clusteringData.declustered An array of entities that were not clustered. + * @param {object|null} clusteringData.cluster The cluster object (if this event is for a specific cluster) or null for summary events. + * @param {Entity[]} [clusteringData.allProcessed] An array of all entities processed (only in summary events). + * + * @example + * // Using the enhanced declusteredEvent to access both clustered and declustered entities + * dataSource.clustering.declusteredEvent.addEventListener(function(data) { + * console.log('Clustered entities:', data.clustered.length); + * console.log('Declustered entities:', data.declustered.length); + * if (data.allProcessed) { + * console.log('Total processed entities:', data.allProcessed.length); + * } + * }); + */ + export default EntityCluster; From 5d851e7035c9d81b03f4a2fc006ca8f887069730 Mon Sep 17 00:00:00 2001 From: rema Date: Fri, 26 Sep 2025 12:28:36 +0200 Subject: [PATCH 3/6] perf: optimize entity clustering by avoiding spread on large sets and skipping unused declustering computations --- .../Source/DataSources/EntityCluster.js | 100 ++++++++++-------- 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/packages/engine/Source/DataSources/EntityCluster.js b/packages/engine/Source/DataSources/EntityCluster.js index 24b9ca554d6d..6d141a7cb8c9 100644 --- a/packages/engine/Source/DataSources/EntityCluster.js +++ b/packages/engine/Source/DataSources/EntityCluster.js @@ -905,35 +905,6 @@ function updateEnable(entityCluster) { return; } - const allVisibleEntities = []; - - if (defined(entityCluster._labelCollection)) { - for (let i = 0; i < entityCluster._labelCollection.length; i++) { - const label = entityCluster._labelCollection.get(i); - if (defined(label.id) && label.show) { - allVisibleEntities.push(label.id); - } - } - } - - if (defined(entityCluster._billboardCollection)) { - for (let i = 0; i < entityCluster._billboardCollection.length; i++) { - const billboard = entityCluster._billboardCollection.get(i); - if (defined(billboard.id) && billboard.show) { - allVisibleEntities.push(billboard.id); - } - } - } - - if (defined(entityCluster._pointCollection)) { - for (let i = 0; i < entityCluster._pointCollection.length; i++) { - const point = entityCluster._pointCollection.get(i); - if (defined(point.id) && point.show) { - allVisibleEntities.push(point.id); - } - } - } - if (defined(entityCluster._clusterLabelCollection)) { entityCluster._clusterLabelCollection.destroy(); } @@ -952,27 +923,62 @@ function updateEnable(entityCluster) { disableCollectionClustering(entityCluster._billboardCollection); disableCollectionClustering(entityCluster._pointCollection); - if (allVisibleEntities.length > 0) { - const uniqueEntities = [...new Set(allVisibleEntities)]; + if (entityCluster._declusteredEvent.numberOfListeners !== 0) { + const allVisibleEntities = []; - entityCluster._declusteredEvent.raiseEvent({ - clustered: [], - declustered: uniqueEntities, - cluster: null, - allProcessed: uniqueEntities, - }); + if (defined(entityCluster._labelCollection)) { + for (let i = 0; i < entityCluster._labelCollection.length; i++) { + const label = entityCluster._labelCollection.get(i); + if (defined(label.id) && label.show) { + allVisibleEntities.push(label.id); + } + } + } - entityCluster._lastClusteredEntities = []; - entityCluster._lastDeclusteredEntities = uniqueEntities.slice(); - entityCluster._allProcessedEntities = uniqueEntities.slice(); - } else { - entityCluster._declusteredEvent.raiseEvent({ - clustered: [], - declustered: [], - cluster: null, - allProcessed: [], - }); + if (defined(entityCluster._billboardCollection)) { + for (let i = 0; i < entityCluster._billboardCollection.length; i++) { + const billboard = entityCluster._billboardCollection.get(i); + if (defined(billboard.id) && billboard.show) { + allVisibleEntities.push(billboard.id); + } + } + } + + if (defined(entityCluster._pointCollection)) { + for (let i = 0; i < entityCluster._pointCollection.length; i++) { + const point = entityCluster._pointCollection.get(i); + if (defined(point.id) && point.show) { + allVisibleEntities.push(point.id); + } + } + } + if (allVisibleEntities.length > 0) { + const uniqueEntities = Array.from(new Set(allVisibleEntities)); + + entityCluster._declusteredEvent.raiseEvent({ + clustered: [], + declustered: uniqueEntities, + cluster: null, + allProcessed: uniqueEntities, + }); + + entityCluster._lastClusteredEntities = []; + entityCluster._lastDeclusteredEntities = uniqueEntities.slice(); + entityCluster._allProcessedEntities = uniqueEntities.slice(); + } else { + entityCluster._declusteredEvent.raiseEvent({ + clustered: [], + declustered: [], + cluster: null, + allProcessed: [], + }); + + entityCluster._lastClusteredEntities = []; + entityCluster._lastDeclusteredEntities = []; + entityCluster._allProcessedEntities = []; + } + } else { entityCluster._lastClusteredEntities = []; entityCluster._lastDeclusteredEntities = []; entityCluster._allProcessedEntities = []; From 126337fb98db85b47814639fa0ba6bc3b8c3907d Mon Sep 17 00:00:00 2001 From: rema Date: Fri, 26 Sep 2025 12:42:14 +0200 Subject: [PATCH 4/6] docs: updated CHANGES.md --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index ed244a0b0c4a..47c1f2376406 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ #### Additions :tada: - Adds an async factory method for the Material class that allows callers to wait on resource loading. [#10566](https://github.com/CesiumGS/cesium/issues/10566) +- Adds new declusteredEvent: Fires with complete clustering information including both clustered and declustered entities [#5760](https://github.com/CesiumGS/cesium/issues/5760) ## 1.133.1 - 2025-09-08 From 5f406357be127f7f03e869783bc67ea385d30b67 Mon Sep 17 00:00:00 2001 From: rema Date: Fri, 26 Sep 2025 14:06:37 +0200 Subject: [PATCH 5/6] test: extend EntityClusterSpec to cover declusteredEvent --- .../Specs/DataSources/EntityClusterSpec.js | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/packages/engine/Specs/DataSources/EntityClusterSpec.js b/packages/engine/Specs/DataSources/EntityClusterSpec.js index ca13f98058b3..14c0f6b0d92f 100644 --- a/packages/engine/Specs/DataSources/EntityClusterSpec.js +++ b/packages/engine/Specs/DataSources/EntityClusterSpec.js @@ -692,6 +692,187 @@ describe( expect(cluster._billboardCollection).toBeDefined(); expect(cluster._billboardCollection.length).toEqual(2); }); + + it("has declusteredEvent property", function () { + cluster = new EntityCluster(); + expect(cluster.declusteredEvent).toBeDefined(); + expect(typeof cluster.declusteredEvent.addEventListener).toEqual( + "function", + ); + }); + + it("provides access to clustering data via new API methods", function () { + cluster = new EntityCluster(); + cluster._initialize(scene); + + expect(typeof cluster.getLastClusteredEntities).toEqual("function"); + expect(typeof cluster.getLastDeclusteredEntities).toEqual("function"); + expect(typeof cluster.getAllProcessedEntities).toEqual("function"); + + expect(cluster.getLastClusteredEntities()).toEqual([]); + expect(cluster.getLastDeclusteredEntities()).toEqual([]); + expect(cluster.getAllProcessedEntities()).toEqual([]); + }); + + it("fires declusteredEvent with both clustered and declustered entities", function () { + cluster = new EntityCluster(); + cluster._initialize(scene); + + let receivedData = null; + cluster.declusteredEvent.addEventListener(function (data) { + receivedData = data; + }); + + const entity1 = new Entity(); + const point1 = cluster.getPoint(entity1); + point1.id = entity1; + point1.pixelSize = 1; + point1.position = SceneTransforms.drawingBufferToWorldCoordinates( + scene, + new Cartesian2(0.0, 0.0), + depth, + ); + + const entity2 = new Entity(); + const point2 = cluster.getPoint(entity2); + point2.id = entity2; + point2.pixelSize = 1; + point2.position = SceneTransforms.drawingBufferToWorldCoordinates( + scene, + new Cartesian2(1.0, 1.0), + depth, + ); + + const entity3 = new Entity(); + const point3 = cluster.getPoint(entity3); + point3.id = entity3; + point3.pixelSize = 1; + point3.position = SceneTransforms.drawingBufferToWorldCoordinates( + scene, + new Cartesian2(scene.canvas.clientWidth, scene.canvas.clientHeight), + farDepth, + ); + + cluster.enabled = true; + return updateUntilDone(cluster).then(function () { + expect(receivedData).toBeDefined(); + expect(receivedData.clustered).toBeDefined(); + expect(receivedData.declustered).toBeDefined(); + expect(Array.isArray(receivedData.clustered)).toEqual(true); + expect(Array.isArray(receivedData.declustered)).toEqual(true); + }); + }); + + it("maintains backward compatibility - original clusterEvent still works", function () { + cluster = new EntityCluster(); + cluster._initialize(scene); + + let originalEventFired = false; + let newEventFired = false; + + cluster.clusterEvent.addEventListener(function (entities, clusterObj) { + originalEventFired = true; + expect(Array.isArray(entities)).toEqual(true); + expect(clusterObj).toBeDefined(); + }); + + cluster.declusteredEvent.addEventListener(function (data) { + newEventFired = true; + expect(data.clustered).toBeDefined(); + expect(data.declustered).toBeDefined(); + }); + + const entity1 = new Entity(); + const point1 = cluster.getPoint(entity1); + point1.id = entity1; + point1.pixelSize = 1; + point1.position = SceneTransforms.drawingBufferToWorldCoordinates( + scene, + new Cartesian2(0.0, 0.0), + depth, + ); + + const entity2 = new Entity(); + const point2 = cluster.getPoint(entity2); + point2.id = entity2; + point2.pixelSize = 1; + point2.position = SceneTransforms.drawingBufferToWorldCoordinates( + scene, + new Cartesian2(1.0, 1.0), + depth, + ); + + cluster.enabled = true; + return updateUntilDone(cluster).then(function () { + expect(originalEventFired).toEqual(true); + expect(newEventFired).toEqual(true); + }); + }); + + it("tracks processed entities correctly", function () { + cluster = new EntityCluster(); + cluster._initialize(scene); + + const entity1 = new Entity(); + const point1 = cluster.getPoint(entity1); + point1.id = entity1; + point1.pixelSize = 1; + point1.position = SceneTransforms.drawingBufferToWorldCoordinates( + scene, + new Cartesian2(0.0, 0.0), + depth, + ); + + const entity2 = new Entity(); + const point2 = cluster.getPoint(entity2); + point2.id = entity2; + point2.pixelSize = 1; + point2.position = SceneTransforms.drawingBufferToWorldCoordinates( + scene, + new Cartesian2(scene.canvas.clientWidth, scene.canvas.clientHeight), + farDepth, + ); + + cluster.enabled = true; + return updateUntilDone(cluster).then(function () { + const clusteredEntities = cluster.getLastClusteredEntities(); + const declusteredEntities = cluster.getLastDeclusteredEntities(); + const allProcessed = cluster.getAllProcessedEntities(); + + expect(allProcessed.length).toBeGreaterThan(0); + expect( + clusteredEntities.length + declusteredEntities.length, + ).toBeLessThanOrEqual(allProcessed.length); + }); + }); + + it("cleans up tracking arrays on destroy", function () { + cluster = new EntityCluster(); + cluster._initialize(scene); + + const entity = new Entity(); + const point = cluster.getPoint(entity); + point.id = entity; + point.pixelSize = 1; + point.position = SceneTransforms.drawingBufferToWorldCoordinates( + scene, + new Cartesian2(0.0, 0.0), + depth, + ); + + cluster.enabled = true; + return updateUntilDone(cluster).then(function () { + expect(cluster._allProcessedEntities).toBeDefined(); + expect(cluster._lastClusteredEntities).toBeDefined(); + expect(cluster._lastDeclusteredEntities).toBeDefined(); + + cluster.destroy(); + + expect(cluster._allProcessedEntities).toEqual([]); + expect(cluster._lastClusteredEntities).toEqual([]); + expect(cluster._lastDeclusteredEntities).toEqual([]); + }); + }); }, "WebGL", ); From 587e235053cc4c9fa4fb0d05d7f1008c4975d241 Mon Sep 17 00:00:00 2001 From: Oko-Tester Date: Sat, 27 Sep 2025 09:29:42 +0200 Subject: [PATCH 6/6] refactor: reduce nesting and extract duplicate collection logic --- .../Source/DataSources/EntityCluster.js | 113 ++++++++---------- 1 file changed, 53 insertions(+), 60 deletions(-) diff --git a/packages/engine/Source/DataSources/EntityCluster.js b/packages/engine/Source/DataSources/EntityCluster.js index 6d141a7cb8c9..538828f70ed2 100644 --- a/packages/engine/Source/DataSources/EntityCluster.js +++ b/packages/engine/Source/DataSources/EntityCluster.js @@ -900,6 +900,58 @@ function disableCollectionClustering(collection) { } } +function getVisibleEntitiesFromCollection(collection) { + if (!defined(collection)) { + return []; + } + + const visibleEntities = []; + for (let i = 0; i < collection.length; i++) { + const item = collection.get(i); + if (defined(item.id) && item.show) { + visibleEntities.push(item.id); + } + } + return visibleEntities; +} + +function handleDeclusteredEvent(entityCluster) { + if (entityCluster._declusteredEvent.numberOfListeners === 0) { + return; + } + const allVisibleEntities = [ + ...getVisibleEntitiesFromCollection(entityCluster._labelCollection), + ...getVisibleEntitiesFromCollection(entityCluster._billboardCollection), + ...getVisibleEntitiesFromCollection(entityCluster._pointCollection), + ]; + + if (allVisibleEntities.length > 0) { + const uniqueEntities = Array.from(new Set(allVisibleEntities)); + + entityCluster._declusteredEvent.raiseEvent({ + clustered: [], + declustered: uniqueEntities, + cluster: null, + allProcessed: uniqueEntities, + }); + + entityCluster._lastClusteredEntities = []; + entityCluster._lastDeclusteredEntities = uniqueEntities.slice(); + entityCluster._allProcessedEntities = uniqueEntities.slice(); + } else { + entityCluster._declusteredEvent.raiseEvent({ + clustered: [], + declustered: [], + cluster: null, + allProcessed: [], + }); + + entityCluster._lastClusteredEntities = []; + entityCluster._lastDeclusteredEntities = []; + entityCluster._allProcessedEntities = []; + } +} + function updateEnable(entityCluster) { if (entityCluster.enabled) { return; @@ -923,66 +975,7 @@ function updateEnable(entityCluster) { disableCollectionClustering(entityCluster._billboardCollection); disableCollectionClustering(entityCluster._pointCollection); - if (entityCluster._declusteredEvent.numberOfListeners !== 0) { - const allVisibleEntities = []; - - if (defined(entityCluster._labelCollection)) { - for (let i = 0; i < entityCluster._labelCollection.length; i++) { - const label = entityCluster._labelCollection.get(i); - if (defined(label.id) && label.show) { - allVisibleEntities.push(label.id); - } - } - } - - if (defined(entityCluster._billboardCollection)) { - for (let i = 0; i < entityCluster._billboardCollection.length; i++) { - const billboard = entityCluster._billboardCollection.get(i); - if (defined(billboard.id) && billboard.show) { - allVisibleEntities.push(billboard.id); - } - } - } - - if (defined(entityCluster._pointCollection)) { - for (let i = 0; i < entityCluster._pointCollection.length; i++) { - const point = entityCluster._pointCollection.get(i); - if (defined(point.id) && point.show) { - allVisibleEntities.push(point.id); - } - } - } - - if (allVisibleEntities.length > 0) { - const uniqueEntities = Array.from(new Set(allVisibleEntities)); - - entityCluster._declusteredEvent.raiseEvent({ - clustered: [], - declustered: uniqueEntities, - cluster: null, - allProcessed: uniqueEntities, - }); - - entityCluster._lastClusteredEntities = []; - entityCluster._lastDeclusteredEntities = uniqueEntities.slice(); - entityCluster._allProcessedEntities = uniqueEntities.slice(); - } else { - entityCluster._declusteredEvent.raiseEvent({ - clustered: [], - declustered: [], - cluster: null, - allProcessed: [], - }); - - entityCluster._lastClusteredEntities = []; - entityCluster._lastDeclusteredEntities = []; - entityCluster._allProcessedEntities = []; - } - } else { - entityCluster._lastClusteredEntities = []; - entityCluster._lastDeclusteredEntities = []; - entityCluster._allProcessedEntities = []; - } + handleDeclusteredEvent(entityCluster); } /**