@tissuumaps/render 0.1.0-beta.2 → 0.1.0-beta.3

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/index.js CHANGED
@@ -662,8 +662,10 @@ var OpenSeadragonContext = class OpenSeadragonContext {
662
662
  OpenSeadragonUtils.fixTileCacheCounter();
663
663
  this.viewer = new OpenSeadragon.Viewer({
664
664
  ...viewerOptions,
665
+ showNavigator: true,
665
666
  element: viewerElement
666
667
  });
668
+ this.viewer.navigator.setVisible(viewerOptions?.showNavigator !== false);
667
669
  this._backgroundPixelUrl = OpenSeadragonUtils.createPixelUrl(backgroundColor.r, backgroundColor.g, backgroundColor.b, 1);
668
670
  this.viewer.addHandler("canvas-key", (event) => {
669
671
  if ([
@@ -787,11 +789,13 @@ var OpenSeadragonContext = class OpenSeadragonContext {
787
789
  * Applies viewer options to the OpenSeadragon viewer and all existing tiled images
788
790
  *
789
791
  * For each option key, performs a shallow merge (one level deep) of nested objects
790
- * on both the viewer instance and every tiled image in the world.
792
+ * on both the viewer instance and every tiled image in the world. `showNavigator`
793
+ * shows or hides the navigator, as OpenSeadragon reads it only on construction.
791
794
  *
792
795
  * @param viewerOptions - Options to apply
793
796
  */
794
797
  setViewerOptions(viewerOptions) {
798
+ if (viewerOptions.showNavigator !== void 0) this.viewer.navigator.setVisible(viewerOptions.showNavigator);
795
799
  for (const [key, value] of Object.entries(viewerOptions)) {
796
800
  if (key in this.viewer && this.viewer[key] !== value) {
797
801
  if (typeof value === "object" && value !== null) this.viewer[key] = {
@@ -1385,18 +1389,20 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1385
1389
  /**
1386
1390
  * Synchronizes the viewer's tiled images with the current model
1387
1391
  *
1388
- * Loads all objects assigned to the layers of the model set by
1389
- * {@link setModel}, removes the tiled images that are no longer needed, and
1390
- * creates or updates the remaining ones. Resolves once the tiled images have
1391
- * actually been added to the world, i.e. once the viewer reflects the model
1392
- * state that was read. The model may be set again while the synchronization
1393
- * runs; it reads the model once, so it sees one consistent state, while the
1394
- * properties it applies to the tiled images come from the current model (see
1392
+ * Starts loading the objects on the layers of the model (see
1393
+ * {@link setModel}), and right away removes the tiled images that are no
1394
+ * longer needed (see {@link _cleanRenderedObjects}). Each object is then
1395
+ * synchronized as soon as it has loaded, without waiting for the others: its
1396
+ * tiled images are updated, or created at its position in the world (see
1397
+ * {@link _synchronizeRenderedObject}).
1398
+ *
1399
+ * Resolves once the tiled images of all objects are in the world. The model
1400
+ * is read once, so the synchronization sees one consistent state, but the
1401
+ * properties applied to the tiled images come from the current model (see
1395
1402
  * {@link _updateRenderedObject}).
1396
1403
  *
1397
- * Objects whose tiled images cannot be created, e.g. because their data
1398
- * provides no tile sources, are logged and skipped, just like objects whose
1399
- * data failed to load (see {@link _loadObjects}). A synchronization that
1404
+ * Objects whose data fails to load, or whose tiled images cannot be created
1405
+ * (e.g. without tile sources), are logged and dropped. A synchronization that
1400
1406
  * fails or is aborted leaves the model unsynchronized (see
1401
1407
  * {@link needsSynchronization}).
1402
1408
  *
@@ -1413,27 +1419,16 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1413
1419
  const syncState = this.getSyncState();
1414
1420
  this._lastSyncState = syncState;
1415
1421
  try {
1416
- const newRefs = await this._loadObjects(context, { signal });
1417
- const renderedObjectsByNewRef = await this._cleanRenderedObjects(newRefs, { signal });
1418
- this.retainObjects(newRefs.map((newRef) => newRef.object));
1419
- let offset = 0;
1420
- const newRenderedObjects = [];
1421
- for (const newRef of newRefs) {
1422
- let renderedObject = renderedObjectsByNewRef.get(newRef);
1423
- if (renderedObject === void 0) try {
1424
- renderedObject = this._createRenderedObject(offset, newRef, { signal });
1425
- } catch (error) {
1426
- console.error(`Failed to create tiled images for object with ID '${newRef.object.id}'`, error);
1427
- continue;
1428
- }
1429
- else this._updateRenderedObject(renderedObject, newRef);
1430
- newRenderedObjects.push(renderedObject);
1431
- offset += (renderedObject.usesBackdrop ? 1 : 0) + renderedObject.tileSourceCount;
1432
- }
1433
- this._renderedObjects = newRenderedObjects;
1434
- await Promise.allSettled(newRenderedObjects.map((renderedObject) => renderedObject.tiledImagesPromise));
1422
+ const loadings = this._loadObjects(context, { signal });
1423
+ const renderedObjectsByLoading = await this._cleanRenderedObjects(loadings, { signal });
1424
+ this.retainObjects(loadings.map((loading) => loading.object));
1425
+ const syncResults = await Promise.allSettled(loadings.map((loading, index) => this._synchronizeRenderedObject(loadings.slice(0, index), loading.newRefPromise, renderedObjectsByLoading.get(loading), { signal })));
1426
+ signal?.throwIfAborted();
1427
+ const firstSyncFailure = syncResults.find((result) => result.status === "rejected");
1428
+ if (firstSyncFailure !== void 0) throw firstSyncFailure.reason;
1429
+ await Promise.allSettled(this._renderedObjects.map((renderedObject) => renderedObject.tiledImagesPromise));
1435
1430
  signal?.throwIfAborted();
1436
- await this._updateBoundsAndReveal(newRenderedObjects, { signal });
1431
+ await this._updateBoundsAndReveal(this._renderedObjects, { signal });
1437
1432
  } catch (error) {
1438
1433
  if (this._lastSyncState === syncState) this._lastSyncState = void 0;
1439
1434
  throw error;
@@ -1570,13 +1565,12 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1570
1565
  /**
1571
1566
  * Retains what was resolved for the given objects, and discards the rest
1572
1567
  *
1573
- * Called by {@link synchronize} once all objects have loaded and the rendered
1574
- * objects of all others have been deleted, with the objects that are about
1575
- * to be displayed: those assigned to a rendered layer whose data loaded
1576
- * successfully. Does nothing here; subclasses that keep
1577
- * state per object (see {@link resolveObject} and
1568
+ * Called by {@link synchronize} with the objects about to be displayed, i.e.
1569
+ * those assigned to a rendered layer, once the rendered objects of all others
1570
+ * have been deleted and before any object has loaded. Does nothing here;
1571
+ * subclasses that keep state per object (see {@link resolveObject} and
1578
1572
  * {@link resolveTiledImageDataTransfer}) override this to drop the state of
1579
- * every object that is not among the given ones.
1573
+ * all other objects.
1580
1574
  *
1581
1575
  * @param _objects - The objects (images or labels) about to be displayed
1582
1576
  */
@@ -1584,15 +1578,15 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1584
1578
  /**
1585
1579
  * Resolves what a renderer derives from an object, once its data has loaded
1586
1580
  *
1587
- * Called by {@link _loadObjects} for every object on one of the given layers,
1588
- * concurrently, right after
1589
- * its data has loaded and before its tiled images are created or updated. An
1590
- * object that cannot be resolved is logged, but kept: its tiled images are
1591
- * still created or updated, with whatever the synchronous hooks return for
1592
- * it. Does nothing here; subclasses override this to resolve, from the
1593
- * object, its data and the inputs of the synchronization, whatever their
1594
- * synchronous hooks return later (see {@link resolveTiledImageDataTransfer}),
1595
- * and to keep it for as long as its outcome would not change.
1581
+ * Called by {@link _loadObjects} for each object, concurrently, once its data
1582
+ * has loaded and before its tiled images are created or updated. An object
1583
+ * that cannot be resolved is logged, but kept: its tiled images are still
1584
+ * created or updated, with whatever the synchronous hooks return for it.
1585
+ *
1586
+ * Does nothing here. Subclasses override this to resolve what their
1587
+ * synchronous hooks return later (see {@link resolveTiledImageDataTransfer})
1588
+ * from the object, its data and the inputs of the synchronization, and to
1589
+ * keep it for as long as its outcome would not change.
1596
1590
  *
1597
1591
  * @param _object - The object (image or labels) to resolve
1598
1592
  * @param _data - The loaded data of the object
@@ -1679,28 +1673,29 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1679
1673
  */
1680
1674
  resolveTiledImageDataTransfer(_ref, _index) {}
1681
1675
  /**
1682
- * Concurrently loads and resolves all objects assigned to the layers of the current model
1676
+ * Starts loading and resolving all objects assigned to the layers of the current model
1683
1677
  *
1684
- * Each object's data is loaded with the context's `loadObject`, and the object
1685
- * is then resolved (see {@link resolveObject}). The returned references are
1686
- * ordered by layer and then by object, which determines the order of the
1687
- * corresponding tiled images in the world. Objects whose data failed to load
1688
- * are logged and skipped; objects that could not be resolved are logged, but
1689
- * kept. The model is read once, before the first `await`, so one
1690
- * synchronization sees one consistent model state, however often the model
1691
- * is set while it runs.
1678
+ * Loads each object's data with the context's `loadObject`, then resolves the
1679
+ * object (see {@link resolveObject}). Returns right away, with one loading
1680
+ * per object, in world order: by layer, then by object. A loading rejects if
1681
+ * the object's data fails to load, which is logged. An object that cannot be
1682
+ * resolved is logged, but kept.
1683
+ *
1684
+ * The model is read once, right away, so one synchronization sees one
1685
+ * consistent model state.
1692
1686
  *
1693
1687
  * @param context - The inputs of the current synchronization
1694
1688
  * @param options - Optional abort signal
1695
- * @returns A promise that resolves to one object reference per successfully loaded object
1689
+ * @returns One loading per object, whose promise resolves to the object's
1690
+ * reference
1696
1691
  * @throws Error if no model has been set (see {@link setModel})
1697
1692
  */
1698
- async _loadObjects(context, options) {
1693
+ _loadObjects(context, options) {
1699
1694
  const { signal } = options ?? {};
1700
1695
  signal?.throwIfAborted();
1701
1696
  const model = this._model;
1702
1697
  if (model === void 0) throw new Error("Model not set");
1703
- const newRefPromises = [];
1698
+ const loadings = [];
1704
1699
  for (const currentLayer of model.layers) for (const currentObject of model.objects.filter((object) => object.layer === currentLayer.id)) {
1705
1700
  const newRefPromise = context.loadObject(currentObject, { signal }).then(async (data) => {
1706
1701
  try {
@@ -1718,57 +1713,62 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1718
1713
  newRefPromise.catch((error) => {
1719
1714
  if (!signal?.aborted) console.error(`Failed to load object with ID '${currentObject.id}'`, error);
1720
1715
  });
1721
- newRefPromises.push(newRefPromise);
1716
+ loadings.push({
1717
+ layer: currentLayer,
1718
+ object: currentObject,
1719
+ newRefPromise
1720
+ });
1722
1721
  }
1723
- const results = await Promise.allSettled(newRefPromises);
1724
- signal?.throwIfAborted();
1725
- return results.filter((result) => result.status === "fulfilled").map((result) => result.value);
1722
+ return loadings;
1726
1723
  }
1727
1724
  /**
1728
- * Retains the rendered objects that can be reused for the new object references, and deletes the rest
1725
+ * Keeps the rendered objects that can be reused for a synchronization, and deletes the rest
1729
1726
  *
1730
1727
  * A rendered object is matched by the layer and object it references.
1731
- * Unmatched rendered objects are deleted first, so that removing an object
1732
- * does not shift the world indices of the objects behind it. A matched
1733
- * rendered object is reusable if the data source it was loaded for is that of
1734
- * the reference, and if its backdrop, if any, and all of its tiled images
1735
- * already sit at the consecutive world indices expected for its position
1736
- * among the references, counted from the anchor. A partially misplaced object
1737
- * is not reusable. The other matched rendered objects are deleted as well,
1738
- * and are expected to be recreated by the caller via
1739
- * {@link _createRenderedObject}, which is also how the world is reordered.
1740
- *
1741
- * The expected indices account for every matched rendered object, reusable or
1742
- * not, by the world footprint it currently has: a matched object that is
1743
- * recreated, e.g. because its data source changed, is deleted and inserted at
1744
- * the same position, with possibly different tiled images, so the objects
1745
- * behind it keep their indices relative to it and stay reusable. Only the
1746
- * backdrop and tiled images assigned to it count, i.e. none for an object
1747
- * whose tiled images could not be added, which is thereby retried without
1748
- * recreating the objects behind it. The tiled images that an earlier
1749
- * synchronization is still adding are waited for first, so that each
1750
- * rendered object is either fully in the world or not at all. This does not
1751
- * delay the additions of this synchronization, which are queued behind them
1752
- * anyway (see {@link OpenSeadragonContext.addTiledImage}).
1753
- *
1754
- * @param newRefs - The new object references, in the intended world order
1728
+ * Unmatched ones are deleted first, so that their removal does not shift the
1729
+ * world indices of the objects behind them. Only the layers and objects of
1730
+ * the loadings are needed, not their data, so this runs before any of them
1731
+ * has loaded.
1732
+ *
1733
+ * A matched rendered object is reusable if it was loaded for its object's
1734
+ * data source, and if its backdrop, if any, and all of its tiled images sit
1735
+ * at the consecutive world indices expected for its position among the
1736
+ * loadings, counted from the anchor. All other matched rendered objects are
1737
+ * deleted as well, for the caller to recreate them (see
1738
+ * {@link _createRenderedObject}), which is also how the world is reordered.
1739
+ *
1740
+ * The expected indices count every matched rendered object, reusable or not,
1741
+ * by its current footprint. A matched object that is recreated, e.g. because
1742
+ * its data source changed, is reinserted at the same position, so the objects
1743
+ * behind it stay reusable. An object whose tiled images could not be added
1744
+ * has been forgotten (see {@link _createRenderedObject}), so it is created
1745
+ * anew, without recreating the objects behind it.
1746
+ *
1747
+ * The tiled images that an earlier synchronization is still adding are waited
1748
+ * for first, so that each rendered object is either fully in the world or not
1749
+ * at all. This does not delay the additions of this synchronization, which
1750
+ * are queued behind them anyway (see
1751
+ * {@link OpenSeadragonContext.addTiledImage}).
1752
+ *
1753
+ * @param loadings - The loadings of the synchronization (see
1754
+ * {@link _loadObjects}), in world order
1755
1755
  * @param options - Optional abort signal
1756
- * @returns A map of new object references to their reusable rendered objects
1756
+ * @returns A map of loadings to their reusable rendered objects
1757
1757
  * @throws Error if the renderer has no anchor, i.e. it is not initialized or
1758
1758
  * already destroyed
1759
1759
  */
1760
- async _cleanRenderedObjects(newRefs, options) {
1760
+ async _cleanRenderedObjects(loadings, options) {
1761
1761
  const { signal } = options ?? {};
1762
1762
  signal?.throwIfAborted();
1763
1763
  await Promise.allSettled(this._renderedObjects.map((renderedObject) => renderedObject.tiledImagesPromise));
1764
1764
  signal?.throwIfAborted();
1765
1765
  const matchedRenderedObjects = /* @__PURE__ */ new Set();
1766
- const matchedRenderedObjectsByNewRef = /* @__PURE__ */ new Map();
1767
- for (const newRef of newRefs) {
1768
- const renderedObject = this._renderedObjects.find((renderedObject) => renderedObject.ref.layer.id === newRef.layer.id && renderedObject.ref.object.id === newRef.object.id);
1766
+ const matchedRenderedObjectsByLoading = /* @__PURE__ */ new Map();
1767
+ for (const loading of loadings) {
1768
+ const renderedObject = this._renderedObjects.find((renderedObject) => renderedObject.ref.layer.id === loading.layer.id && renderedObject.ref.object.id === loading.object.id);
1769
1769
  if (renderedObject !== void 0) {
1770
1770
  matchedRenderedObjects.add(renderedObject);
1771
- matchedRenderedObjectsByNewRef.set(newRef, renderedObject);
1771
+ matchedRenderedObjectsByLoading.set(loading, renderedObject);
1772
1772
  }
1773
1773
  }
1774
1774
  const unmatchedDeletions = this._renderedObjects.filter((renderedObject) => !matchedRenderedObjects.has(renderedObject)).map((renderedObject) => this._deleteRenderedObject(renderedObject));
@@ -1778,14 +1778,14 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1778
1778
  if (this._anchor === void 0) throw new Error("Anchor not initialized");
1779
1779
  const anchorIndex = this.context.getTiledImageIndex(this._anchor);
1780
1780
  if (anchorIndex === -1) throw new Error("Anchor not found");
1781
- const renderedObjectsByNewRef = /* @__PURE__ */ new Map();
1781
+ const renderedObjectsByLoading = /* @__PURE__ */ new Map();
1782
1782
  const survivors = /* @__PURE__ */ new Set();
1783
1783
  let offset = 1;
1784
- for (const newRef of newRefs) {
1785
- const renderedObject = matchedRenderedObjectsByNewRef.get(newRef);
1784
+ for (const loading of loadings) {
1785
+ const renderedObject = matchedRenderedObjectsByLoading.get(loading);
1786
1786
  if (renderedObject !== void 0) {
1787
- if (deepEqual(renderedObject.ref.object.dataSource, newRef.object.dataSource) && renderedObject.tiledImages !== void 0 && (renderedObject.backdrop === void 0 || this.context.getTiledImageIndex(renderedObject.backdrop) === anchorIndex + offset) && renderedObject.tiledImages.every((tiledImage, c) => this.context.getTiledImageIndex(tiledImage) === anchorIndex + offset + (renderedObject.backdrop !== void 0 ? 1 : 0) + c)) {
1788
- renderedObjectsByNewRef.set(newRef, renderedObject);
1787
+ if (deepEqual(renderedObject.ref.object.dataSource, loading.object.dataSource) && renderedObject.tiledImages !== void 0 && (renderedObject.backdrop === void 0 || this.context.getTiledImageIndex(renderedObject.backdrop) === anchorIndex + offset) && renderedObject.tiledImages.every((tiledImage, c) => this.context.getTiledImageIndex(tiledImage) === anchorIndex + offset + (renderedObject.backdrop !== void 0 ? 1 : 0) + c)) {
1788
+ renderedObjectsByLoading.set(loading, renderedObject);
1789
1789
  survivors.add(renderedObject);
1790
1790
  }
1791
1791
  offset += (renderedObject.backdrop !== void 0 ? 1 : 0) + (renderedObject.tiledImages?.length ?? 0);
@@ -1795,39 +1795,94 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1795
1795
  this._renderedObjects = [...survivors];
1796
1796
  await Promise.all(unreusableDeletions);
1797
1797
  signal?.throwIfAborted();
1798
- return renderedObjectsByNewRef;
1798
+ return renderedObjectsByLoading;
1799
+ }
1800
+ /**
1801
+ * Synchronizes the rendered object of one object, once it has loaded
1802
+ *
1803
+ * A reusable rendered object (see {@link _cleanRenderedObjects}) is updated
1804
+ * with the new reference, or deleted if the object failed to load. Otherwise,
1805
+ * a rendered object is created behind those of the preceding loadings,
1806
+ * whether these are in the world already or still being added. Preceding
1807
+ * objects that arrive later are inserted in front of it, as world mutations
1808
+ * run in request order (see {@link OpenSeadragonContext.addTiledImage}).
1809
+ *
1810
+ * @param precedingLoadings - The loadings that precede the object's in world
1811
+ * order (see {@link _loadObjects})
1812
+ * @param newRefPromise - The promise of the object's reference, from its
1813
+ * loading
1814
+ * @param renderedObject - The object's reusable rendered object, if any
1815
+ * @param options - Optional abort signal
1816
+ * @returns A promise that resolves once the object's tiled images have been
1817
+ * updated, or their creation has been requested
1818
+ */
1819
+ async _synchronizeRenderedObject(precedingLoadings, newRefPromise, renderedObject, options) {
1820
+ const { signal } = options ?? {};
1821
+ signal?.throwIfAborted();
1822
+ let newRef;
1823
+ try {
1824
+ newRef = await newRefPromise;
1825
+ signal?.throwIfAborted();
1826
+ } catch {
1827
+ signal?.throwIfAborted();
1828
+ if (renderedObject !== void 0) {
1829
+ this._renderedObjects = this._renderedObjects.filter((otherRenderedObject) => otherRenderedObject !== renderedObject);
1830
+ await this._deleteRenderedObject(renderedObject);
1831
+ }
1832
+ return;
1833
+ }
1834
+ if (renderedObject !== void 0) {
1835
+ this._updateRenderedObject(renderedObject, newRef);
1836
+ return;
1837
+ }
1838
+ const precedingRenderedObjects = this._renderedObjects.filter((otherRenderedObject) => precedingLoadings.some((precedingLoading) => precedingLoading.layer.id === otherRenderedObject.ref.layer.id && precedingLoading.object.id === otherRenderedObject.ref.object.id));
1839
+ let offset = 0;
1840
+ for (const otherRenderedObject of precedingRenderedObjects) offset += (otherRenderedObject.usesBackdrop ? 1 : 0) + otherRenderedObject.tileSourceCount;
1841
+ let newRenderedObject;
1842
+ try {
1843
+ newRenderedObject = this._createRenderedObject(offset, newRef, { signal });
1844
+ } catch (error) {
1845
+ console.error(`Failed to create tiled images for object with ID '${newRef.object.id}'`, error);
1846
+ return;
1847
+ }
1848
+ this._renderedObjects = [
1849
+ ...precedingRenderedObjects,
1850
+ newRenderedObject,
1851
+ ...this._renderedObjects.slice(precedingRenderedObjects.length)
1852
+ ];
1799
1853
  }
1800
1854
  /**
1801
1855
  * Creates a new rendered object for the given object reference and adds one TiledImage per channel, preceded by a backdrop where required, to the world
1802
1856
  *
1803
- * The TiledImages are inserted at consecutive indices, starting `offset` places
1804
- * after the anchor, which establishes the world layout that
1805
- * {@link _cleanRenderedObjects} expects. `offset` therefore counts TiledImages,
1806
- * not objects, and callers have to advance it by
1807
- * {@link RenderedObject.tileSourceCount} plus the object's backdrop, if it has
1808
- * one. The indices are resolved once each addition is executed, as the anchor
1809
- * may have been replaced, and world indices may have shifted, while it was
1810
- * enqueued.
1811
- *
1812
- * The tile sources are opened here, rather than by the additions themselves,
1813
- * so that the backdrop can be sized like the object's content, which is only
1814
- * known once one of them has been opened. The additions are all requested
1815
- * before this function returns, and in world order, so that the TiledImages of
1816
- * the object that the caller creates next end up behind them.
1817
- *
1818
- * Returns before the TiledImages exist: they are added asynchronously and only
1819
- * then assigned to the rendered object, transformed, and included in the anchor
1820
- * bounds, which is when {@link RenderedObject.tiledImagesPromise} resolves. The
1821
- * backdrop and TiledImages are removed again immediately after they were added
1822
- * if the rendered object is deleted or the operation is aborted in the
1823
- * meantime, or if any of them could not be added at all - a partially added
1824
- * object would shift the world indices of every object after it. For the same
1825
- * reason, a tile source that fails to open is replaced by a transparent
1826
- * placeholder rather than skipped: the indices of the objects created after
1827
- * this one are resolved against the full footprint of this one, so it has to
1828
- * take up all of its indices until it is removed again. If the context
1829
- * is destroyed, nothing is done at all, as the viewer tears down its world
1830
- * itself.
1857
+ * The TiledImages are inserted at consecutive indices, starting `offset`
1858
+ * places after the anchor, which is the world layout that
1859
+ * {@link _cleanRenderedObjects} expects. `offset` thus counts TiledImages,
1860
+ * not objects: the backdrops and TiledImages of the preceding rendered
1861
+ * objects, including those still being added. The indices are resolved when
1862
+ * each addition is executed, as the anchor may have been replaced, and world
1863
+ * indices may have shifted, in the meantime.
1864
+ *
1865
+ * The tile sources are opened here, rather than by the additions, so that the
1866
+ * backdrop can be sized like the object's content, which is only known once
1867
+ * one of them has opened. All additions are requested before this function
1868
+ * returns, in world order, so that the TiledImages of the object created next
1869
+ * end up behind them.
1870
+ *
1871
+ * Returns before the TiledImages exist: they are added asynchronously, and
1872
+ * only then assigned to the rendered object, transformed and included in the
1873
+ * anchor bounds, which is when {@link RenderedObject.tiledImagesPromise}
1874
+ * resolves.
1875
+ *
1876
+ * If the rendered object is deleted or the operation is aborted in the
1877
+ * meantime, or if any TiledImage could not be added, the added backdrop and
1878
+ * TiledImages are removed again right away: a partially added object would
1879
+ * shift the world indices of every object behind it. For the same reason, a
1880
+ * tile source that fails to open is replaced by a transparent placeholder
1881
+ * rather than skipped, as the objects created after this one are placed
1882
+ * against its full footprint. The rendered object is dropped from
1883
+ * {@link _renderedObjects} as its removals are queued, so that objects
1884
+ * created from then on are placed without it. If the context is destroyed,
1885
+ * nothing is done at all, as the viewer tears down its world itself.
1831
1886
  *
1832
1887
  * @param offset - The number of TiledImages between the anchor and the object's first new TiledImage
1833
1888
  * @param newRef - The object reference for which to create a rendered object
@@ -1900,8 +1955,9 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1900
1955
  if (this.context.isDestroyed()) return tiledImages;
1901
1956
  const failure = [...tileSourceResults, ...results].find((result) => result.status === "rejected");
1902
1957
  if (signal?.aborted || failure !== void 0 || newRenderedObject.pendingDelete || this._destroyed) {
1903
- if (backdrop !== void 0) await this.context.removeTiledImage(backdrop);
1904
- for (const tiledImage of tiledImages) await this.context.removeTiledImage(tiledImage);
1958
+ this._renderedObjects = this._renderedObjects.filter((renderedObject) => renderedObject !== newRenderedObject);
1959
+ const addedTiledImages = backdrop !== void 0 ? [backdrop, ...tiledImages] : tiledImages;
1960
+ await Promise.all(addedTiledImages.map((tiledImage) => this.context.removeTiledImage(tiledImage)));
1905
1961
  signal?.throwIfAborted();
1906
1962
  if (failure !== void 0) {
1907
1963
  console.error(`Failed to add tiled images for object with ID '${newRef.object.id}'`, failure.reason);
@@ -3987,7 +4043,6 @@ var WebGLRendererBase = class {
3987
4043
  };
3988
4044
  _model;
3989
4045
  _lastSyncState;
3990
- _hasUnreportedChanges = false;
3991
4046
  _renderedObjects = /* @__PURE__ */ new Map();
3992
4047
  _layerItemsInfosCache = /* @__PURE__ */ new WeakMap();
3993
4048
  /**
@@ -4069,59 +4124,52 @@ var WebGLRendererBase = class {
4069
4124
  /**
4070
4125
  * Synchronizes the rendered objects with the current model and render options
4071
4126
  *
4072
- * Loads the objects of the model (see {@link loadObjects}), drops the
4073
- * rendered objects that no longer match one (see
4074
- * {@link matchOrDestroyRenderedObjects}), and then creates or updates the
4075
- * rest in two passes. The first prepares every object (see
4076
- * {@link prepareRenderedObject}), issuing all requests before the first
4077
- * `await`: requests are shared with, and cancelled once abandoned by, a
4078
- * superseded synchronization, unless they are reclaimed within the same
4079
- * task. The second awaits the preparations in order and uploads each of them
4080
- * in one synchronous block (see {@link createRenderedObject} and
4081
- * {@link updateRenderedObject}), so a draw never sees a half-updated object.
4082
- *
4083
- * An object whose preparation fails, or finds nothing to render, is logged
4084
- * and dropped. A synchronization that fails or is aborted leaves the model
4085
- * unsynchronized (see {@link _discardSyncState}), and the changes it made
4086
- * before it stopped are reported by the next one that completes.
4127
+ * Starts loading the objects of the model (see {@link loadObjects}), and
4128
+ * right away drops the rendered objects that none of them addresses. Each
4129
+ * object is then synchronized on its own, as soon as its data has loaded: the
4130
+ * object is prepared (see {@link prepareRenderedObject}), and the preparation
4131
+ * is uploaded in one synchronous block, either into the reused rendered
4132
+ * object or into one that replaces it (see {@link createRenderedObject} and
4133
+ * {@link updateRenderedObject}). A draw thus never sees a half-updated
4134
+ * object, and a slow or failing object never holds back the others.
4135
+ *
4136
+ * An object issues its requests right after its data has loaded, without
4137
+ * awaiting in between. Requests are shared with a superseded synchronization,
4138
+ * and cancelled once it abandons them, unless they are reclaimed within the
4139
+ * same task. Data that has already loaded allows that.
4140
+ *
4141
+ * An object whose data or preparation fails, or that has nothing to render,
4142
+ * is logged and dropped. A synchronization that fails or is aborted leaves
4143
+ * the model unsynchronized (see {@link _discardSyncState}).
4087
4144
  *
4088
4145
  * @param syncContext - The inputs to synchronize with: the tables and
4089
- * group-to-value maps that the objects resolve their properties from, and
4090
- * the loaders for object and table data. It carries inputs only; what an
4091
- * object was resolved from is captured per object (see {@link ObjectRef})
4092
- * @param options - Optional abort signal
4093
- * @returns A promise that resolves to whether any rendered object changed
4094
- * since the last completed synchronization
4146
+ * group-to-value maps that the objects resolve their properties from, and the
4147
+ * loaders for object and table data. It carries inputs only; what an object
4148
+ * was resolved from is captured per object (see {@link ObjectRef})
4149
+ * @param options - Optional abort signal, and a callback that is called
4150
+ * synchronously whenever a rendered object is added, updated or dropped, so
4151
+ * that the caller can draw each object as soon as it is ready
4152
+ * @returns A promise that resolves once all objects have been synchronized
4095
4153
  * @throws Error if no model has been set (see {@link setModel})
4096
4154
  */
4097
4155
  async synchronize(syncContext, options) {
4098
- const { signal } = options ?? {};
4156
+ const { signal, onChange } = options ?? {};
4099
4157
  signal?.throwIfAborted();
4100
4158
  const syncState = this._recordSyncState();
4101
4159
  try {
4102
- const newRefs = await this.loadObjects(syncContext, { signal });
4103
- const preparations = this.matchOrDestroyRenderedObjects(newRefs).map(({ newRef, renderedObject }) => {
4104
- const preparedPromise = this.prepareRenderedObject(newRef, renderedObject, syncContext, { signal });
4105
- preparedPromise.catch(() => {});
4106
- return {
4107
- newRef,
4108
- renderedObject,
4109
- preparedPromise
4110
- };
4111
- });
4112
- for (const { newRef, renderedObject, preparedPromise } of preparations) {
4113
- const prepared = await this._settleRenderedObjectPreparation(newRef, renderedObject, preparedPromise, { signal });
4114
- if (prepared === void 0) continue;
4115
- if (renderedObject === void 0) this.addRenderedObject(this.createRenderedObject(newRef, prepared));
4116
- else if (this.updateRenderedObject(renderedObject, prepared)) this._hasUnreportedChanges = true;
4117
- }
4160
+ const loadings = this.loadObjects(syncContext, { signal });
4161
+ if (this._cleanRenderedObjects(loadings)) onChange?.();
4162
+ const syncResults = await Promise.allSettled(loadings.map(({ layerId, object, newRefPromise }) => this._synchronizeRenderedObject(layerId, object.id, newRefPromise, syncContext, {
4163
+ signal,
4164
+ onChange
4165
+ })));
4166
+ signal?.throwIfAborted();
4167
+ const firstSyncFailure = syncResults.find((result) => result.status === "rejected");
4168
+ if (firstSyncFailure !== void 0) throw firstSyncFailure.reason;
4118
4169
  } catch (error) {
4119
4170
  this._discardSyncState(syncState);
4120
4171
  throw error;
4121
4172
  }
4122
- const hasUnreportedChanges = this._hasUnreportedChanges;
4123
- this._hasUnreportedChanges = false;
4124
- return hasUnreportedChanges;
4125
4173
  }
4126
4174
  /**
4127
4175
  * Returns the state of the current model and render options that a
@@ -4201,29 +4249,31 @@ var WebGLRendererBase = class {
4201
4249
  */
4202
4250
  getRenderOptionsSyncState() {}
4203
4251
  /**
4204
- * Concurrently loads the data of all objects to be rendered on the layers of the current model
4252
+ * Starts loading the data of all objects to be rendered on the layers of the current model
4205
4253
  *
4206
- * An object assigned to a layer by layer ID is loaded for that layer only, an
4254
+ * An object assigned to a layer by layer ID is loaded for that layer only. An
4207
4255
  * object assigned per item by a table column is loaded for every layer, with
4208
4256
  * the items on each layer resolved from the table (see
4209
4257
  * {@link _getLayerItemsInfos}). The data of an object, and of a table, is
4210
- * loaded once, no matter how many references it is shared by.
4258
+ * loaded once, however many references share it.
4211
4259
  *
4212
- * The returned references are ordered by layer and then by object. Objects
4213
- * whose data or table failed to load are logged and skipped, and so are
4214
- * objects whose items are assigned per item without a table to resolve the
4215
- * assignment from. Objects without items on a layer are skipped silently,
4216
- * which also covers empty objects and objects whose table is empty - those
4217
- * are legitimate states, not failures.
4260
+ * Returns right away, with one loading per object and layer, ordered by layer
4261
+ * and then by object. Each loading settles on its own. It rejects if the
4262
+ * object's data or table fails to load, which is logged. It resolves to
4263
+ * `null` if the object has no items on the layer, which is not logged: empty
4264
+ * objects and empty tables are legitimate states, not failures. An object
4265
+ * whose items are assigned per item, but that has no table to resolve the
4266
+ * assignment from, is logged and gets no loading.
4218
4267
  *
4219
4268
  * @param syncContext - The inputs of the current synchronization: the tables
4220
- * that the objects resolve their item layers from, and the loaders for
4221
- * object and table data
4269
+ * that the objects resolve their item layers from, and the loaders for object
4270
+ * and table data
4222
4271
  * @param options - Optional abort signal
4223
- * @returns A promise that resolves to one reference per loaded object and layer
4272
+ * @returns One loading per object and layer, whose promise resolves to the
4273
+ * object's reference, or to `null` if the object has no items on the layer
4224
4274
  * @throws Error if no model has been set (see {@link setModel})
4225
4275
  */
4226
- async loadObjects(syncContext, options) {
4276
+ loadObjects(syncContext, options) {
4227
4277
  const { signal } = options ?? {};
4228
4278
  signal?.throwIfAborted();
4229
4279
  const model = this._model;
@@ -4232,7 +4282,7 @@ var WebGLRendererBase = class {
4232
4282
  const tableDataPromises = /* @__PURE__ */ new Map();
4233
4283
  const tableLayersPromises = /* @__PURE__ */ new Map();
4234
4284
  const layerItemsInfosPromises = /* @__PURE__ */ new Map();
4235
- const newRefPromises = [];
4285
+ const loadings = [];
4236
4286
  const objectIdsWithoutTable = /* @__PURE__ */ new Set();
4237
4287
  for (const currentLayer of model.layers) for (const currentObject of model.objects) {
4238
4288
  if (typeof currentObject.layer === "string" && currentObject.layer !== currentLayer.id) continue;
@@ -4296,67 +4346,33 @@ var WebGLRendererBase = class {
4296
4346
  }
4297
4347
  const newRefPromise = Promise.all([dataPromise, layerItemsInfosPromise]).then(([data, layerItemsInfos]) => {
4298
4348
  signal?.throwIfAborted();
4349
+ let newRef;
4299
4350
  if (layerItemsInfos !== void 0) {
4300
4351
  const itemsInfo = layerItemsInfos.get(currentLayer.id);
4301
- return {
4352
+ newRef = {
4302
4353
  layerId: currentLayer.id,
4303
4354
  object: currentObject,
4304
4355
  itemIds: itemsInfo?.itemIds ?? [],
4305
4356
  itemsMask: itemsInfo?.itemsMask,
4306
4357
  data
4307
4358
  };
4308
- }
4309
- return {
4359
+ } else newRef = {
4310
4360
  layerId: currentLayer.id,
4311
4361
  object: currentObject,
4312
4362
  itemIds: data.getIds(),
4313
4363
  itemsMask: void 0,
4314
4364
  data
4315
4365
  };
4366
+ return newRef.itemIds.length > 0 ? newRef : null;
4367
+ });
4368
+ newRefPromise.catch(() => {});
4369
+ loadings.push({
4370
+ layerId: currentLayer.id,
4371
+ object: currentObject,
4372
+ newRefPromise
4316
4373
  });
4317
- newRefPromises.push(newRefPromise);
4318
- }
4319
- const results = await Promise.allSettled(newRefPromises);
4320
- signal?.throwIfAborted();
4321
- return results.filter((result) => result.status === "fulfilled").filter((result) => result.value.itemIds.length > 0).map((result) => result.value);
4322
- }
4323
- /**
4324
- * Matches the rendered objects to a new set of references, and destroys the
4325
- * ones left over
4326
- *
4327
- * A rendered object is matched by the layer and object it is kept under, and
4328
- * kept if its contributed items and data source are those of the reference,
4329
- * which it then adopts and is returned for reuse with. Every other rendered
4330
- * object - one that no reference addresses, or one whose items or data source
4331
- * changed - is destroyed and dropped.
4332
- *
4333
- * @param newRefs - The object references to match against
4334
- * @returns One match per reference, in the order of the references, each with
4335
- * the rendered object to reuse for it, or `undefined` if it has none yet
4336
- */
4337
- matchOrDestroyRenderedObjects(newRefs) {
4338
- const matchedRenderedObjects = /* @__PURE__ */ new Set();
4339
- const matches = newRefs.map((newRef) => {
4340
- const renderedObject = this._renderedObjects.get(newRef.layerId)?.get(newRef.object.id);
4341
- if (renderedObject !== void 0 && renderedObject.ref.itemIds === newRef.itemIds && renderedObject.ref.itemsMask === newRef.itemsMask && deepEqual(renderedObject.ref.object.dataSource, newRef.object.dataSource)) {
4342
- renderedObject.ref = newRef;
4343
- matchedRenderedObjects.add(renderedObject);
4344
- return {
4345
- newRef,
4346
- renderedObject
4347
- };
4348
- }
4349
- return {
4350
- newRef,
4351
- renderedObject: void 0
4352
- };
4353
- });
4354
- for (const renderedObjects of this._renderedObjects.values()) for (const [objectId, renderedObject] of renderedObjects) if (!matchedRenderedObjects.has(renderedObject)) {
4355
- renderedObjects.delete(objectId);
4356
- this.destroyRenderedObject(renderedObject);
4357
- this._hasUnreportedChanges = true;
4358
4374
  }
4359
- return matches;
4375
+ return loadings;
4360
4376
  }
4361
4377
  /**
4362
4378
  * Adds a newly created rendered object under its layer and object
@@ -4371,7 +4387,6 @@ var WebGLRendererBase = class {
4371
4387
  this._renderedObjects.set(layerId, renderedObjects);
4372
4388
  }
4373
4389
  renderedObjects.set(object.id, renderedObject);
4374
- this._hasUnreportedChanges = true;
4375
4390
  }
4376
4391
  /**
4377
4392
  * Drops a rendered object and releases its GPU resources
@@ -4381,7 +4396,6 @@ var WebGLRendererBase = class {
4381
4396
  removeRenderedObject(renderedObject) {
4382
4397
  this._renderedObjects.get(renderedObject.ref.layerId)?.delete(renderedObject.ref.object.id);
4383
4398
  this.destroyRenderedObject(renderedObject);
4384
- this._hasUnreportedChanges = true;
4385
4399
  }
4386
4400
  /**
4387
4401
  * Drops all rendered objects and releases their GPU resources
@@ -4389,7 +4403,6 @@ var WebGLRendererBase = class {
4389
4403
  clearRenderedObjects() {
4390
4404
  for (const renderedObjects of this._renderedObjects.values()) for (const renderedObject of renderedObjects.values()) this.destroyRenderedObject(renderedObject);
4391
4405
  this._renderedObjects.clear();
4392
- this._hasUnreportedChanges = true;
4393
4406
  }
4394
4407
  /**
4395
4408
  * Returns one render pass per rendered object to draw, in draw order
@@ -4535,35 +4548,90 @@ var WebGLRendererBase = class {
4535
4548
  return layerItemsInfos;
4536
4549
  }
4537
4550
  /**
4538
- * Awaits the preparation of an object, dropping the object if it fails or
4539
- * finds nothing to render
4551
+ * Drops the rendered objects that none of the given loadings addresses
4540
4552
  *
4541
- * @param newRef - The prepared object
4542
- * @param renderedObject - The object's current rendered state, if any, which
4543
- * is removed if the object is dropped
4544
- * @param preparedPromise - The preparation, see {@link prepareRenderedObject}
4545
- * @param options - Optional abort signal
4546
- * @returns The preparation, or `undefined` if the object was dropped (which
4547
- * is logged)
4553
+ * Their layer or object has left the model, or the object has moved to
4554
+ * another layer, so they are no longer drawn (see {@link getRenderPasses}).
4555
+ * They are dropped right away, rather than once the remaining objects have
4556
+ * loaded. Whether the others can be reused is decided per object, once its
4557
+ * data has loaded (see {@link _synchronizeRenderedObject}).
4558
+ *
4559
+ * @param loadings - The loadings of the current synchronization (see
4560
+ * {@link loadObjects})
4561
+ * @returns Whether any rendered object was dropped
4548
4562
  */
4549
- async _settleRenderedObjectPreparation(newRef, renderedObject, preparedPromise, options) {
4550
- const { signal } = options ?? {};
4563
+ _cleanRenderedObjects(loadings) {
4564
+ let removed = false;
4565
+ for (const [layerId, renderedObjects] of this._renderedObjects) for (const [objectId, renderedObject] of renderedObjects) if (!loadings.some((loading) => loading.layerId === layerId && loading.object.id === objectId)) {
4566
+ this.removeRenderedObject(renderedObject);
4567
+ removed = true;
4568
+ }
4569
+ return removed;
4570
+ }
4571
+ /**
4572
+ * Synchronizes the rendered object of one object and layer, once its data has loaded
4573
+ *
4574
+ * The rendered object kept under the layer and object is reused if its items
4575
+ * and data source match the new reference, which it then adopts. The object
4576
+ * is then prepared and uploaded (see {@link synchronize}). A rendered object
4577
+ * that is not reused stays drawn until its replacement is uploaded, and is
4578
+ * then dropped in the same synchronous block. If the object's data failed to
4579
+ * load, or it has no items on the layer, it is only dropped. So is it if its
4580
+ * preparation fails or finds nothing to render, which is logged.
4581
+ *
4582
+ * @param layerId - The ID of the layer
4583
+ * @param objectId - The ID of the object
4584
+ * @param newRefPromise - The object's loading (see {@link loadObjects})
4585
+ * @param syncContext - The inputs of the current synchronization
4586
+ * @param options - Optional abort signal, and the callback to call whenever
4587
+ * the rendered object is added, updated or dropped
4588
+ * @returns A promise that resolves once the object has been synchronized
4589
+ */
4590
+ async _synchronizeRenderedObject(layerId, objectId, newRefPromise, syncContext, options) {
4591
+ const { signal, onChange } = options ?? {};
4592
+ signal?.throwIfAborted();
4593
+ let newRef;
4594
+ try {
4595
+ newRef = await newRefPromise;
4596
+ signal?.throwIfAborted();
4597
+ } catch {
4598
+ signal?.throwIfAborted();
4599
+ newRef = null;
4600
+ }
4601
+ let renderedObject;
4602
+ const oldRenderedObject = this._renderedObjects.get(layerId)?.get(objectId);
4603
+ if (oldRenderedObject !== void 0 && newRef !== null && oldRenderedObject.ref.itemIds === newRef.itemIds && oldRenderedObject.ref.itemsMask === newRef.itemsMask && deepEqual(oldRenderedObject.ref.object.dataSource, newRef.object.dataSource)) {
4604
+ oldRenderedObject.ref = newRef;
4605
+ renderedObject = oldRenderedObject;
4606
+ }
4607
+ if (newRef === null) {
4608
+ if (oldRenderedObject !== void 0) {
4609
+ this.removeRenderedObject(oldRenderedObject);
4610
+ onChange?.();
4611
+ }
4612
+ return;
4613
+ }
4551
4614
  let prepared;
4552
4615
  try {
4553
- prepared = await preparedPromise;
4616
+ prepared = await this.prepareRenderedObject(newRef, renderedObject, syncContext, { signal });
4617
+ signal?.throwIfAborted();
4554
4618
  } catch (error) {
4555
4619
  signal?.throwIfAborted();
4556
4620
  console.error(`Failed to prepare object with ID '${newRef.object.id}'`, error);
4557
- if (renderedObject !== void 0) this.removeRenderedObject(renderedObject);
4558
- return;
4559
4621
  }
4560
- signal?.throwIfAborted();
4561
- if (prepared === null) {
4562
- console.warn(`Object with ID '${newRef.object.id}' has nothing to render, skipping`);
4563
- if (renderedObject !== void 0) this.removeRenderedObject(renderedObject);
4622
+ if (prepared === null) console.warn(`Object with ID '${newRef.object.id}' has nothing to render, skipping`);
4623
+ if (prepared === void 0 || prepared === null) {
4624
+ if (oldRenderedObject !== void 0) {
4625
+ this.removeRenderedObject(oldRenderedObject);
4626
+ onChange?.();
4627
+ }
4564
4628
  return;
4565
4629
  }
4566
- return prepared;
4630
+ if (renderedObject === void 0) {
4631
+ if (oldRenderedObject !== void 0) this.removeRenderedObject(oldRenderedObject);
4632
+ this.addRenderedObject(this.createRenderedObject(newRef, prepared));
4633
+ onChange?.();
4634
+ } else if (this.updateRenderedObject(renderedObject, prepared)) onChange?.();
4567
4635
  }
4568
4636
  /**
4569
4637
  * Creates the loader for the tables that an object resolves its properties