@tissuumaps/render 0.1.0-beta.1 → 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] = {
@@ -1011,8 +1015,12 @@ var OpenSeadragonContext = class OpenSeadragonContext {
1011
1015
  */
1012
1016
  async _openTileSource(tiledImageOptions, options) {
1013
1017
  const { signal } = options ?? {};
1014
- const tileSource = await Promise.resolve(tiledImageOptions.tileSource);
1018
+ let tileSource = await Promise.resolve(tiledImageOptions.tileSource);
1015
1019
  signal?.throwIfAborted();
1020
+ if (typeof tileSource === "object" && tileSource !== null) {
1021
+ const prototype = Object.getPrototypeOf(tileSource);
1022
+ if (prototype === Object.prototype || prototype === null) tileSource = { ...tileSource };
1023
+ }
1016
1024
  try {
1017
1025
  const { source: openedTileSource } = await this.viewer.instantiateTileSourceClass({
1018
1026
  ...tiledImageOptions,
@@ -1381,18 +1389,20 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1381
1389
  /**
1382
1390
  * Synchronizes the viewer's tiled images with the current model
1383
1391
  *
1384
- * Loads all objects assigned to the layers of the model set by
1385
- * {@link setModel}, removes the tiled images that are no longer needed, and
1386
- * creates or updates the remaining ones. Resolves once the tiled images have
1387
- * actually been added to the world, i.e. once the viewer reflects the model
1388
- * state that was read. The model may be set again while the synchronization
1389
- * runs; it reads the model once, so it sees one consistent state, while the
1390
- * 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
1391
1402
  * {@link _updateRenderedObject}).
1392
1403
  *
1393
- * Objects whose tiled images cannot be created, e.g. because their data
1394
- * provides no tile sources, are logged and skipped, just like objects whose
1395
- * 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
1396
1406
  * fails or is aborted leaves the model unsynchronized (see
1397
1407
  * {@link needsSynchronization}).
1398
1408
  *
@@ -1409,27 +1419,16 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1409
1419
  const syncState = this.getSyncState();
1410
1420
  this._lastSyncState = syncState;
1411
1421
  try {
1412
- const newRefs = await this._loadObjects(context, { signal });
1413
- const renderedObjectsByNewRef = await this._cleanRenderedObjects(newRefs, { signal });
1414
- this.retainObjects(newRefs.map((newRef) => newRef.object));
1415
- let offset = 0;
1416
- const newRenderedObjects = [];
1417
- for (const newRef of newRefs) {
1418
- let renderedObject = renderedObjectsByNewRef.get(newRef);
1419
- if (renderedObject === void 0) try {
1420
- renderedObject = this._createRenderedObject(offset, newRef, { signal });
1421
- } catch (error) {
1422
- console.error(`Failed to create tiled images for object with ID '${newRef.object.id}'`, error);
1423
- continue;
1424
- }
1425
- else this._updateRenderedObject(renderedObject, newRef);
1426
- newRenderedObjects.push(renderedObject);
1427
- offset += (renderedObject.usesBackdrop ? 1 : 0) + renderedObject.tileSourceCount;
1428
- }
1429
- this._renderedObjects = newRenderedObjects;
1430
- 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));
1431
1430
  signal?.throwIfAborted();
1432
- await this._updateBoundsAndReveal(newRenderedObjects, { signal });
1431
+ await this._updateBoundsAndReveal(this._renderedObjects, { signal });
1433
1432
  } catch (error) {
1434
1433
  if (this._lastSyncState === syncState) this._lastSyncState = void 0;
1435
1434
  throw error;
@@ -1566,13 +1565,12 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1566
1565
  /**
1567
1566
  * Retains what was resolved for the given objects, and discards the rest
1568
1567
  *
1569
- * Called by {@link synchronize} once all objects have loaded and the rendered
1570
- * objects of all others have been deleted, with the objects that are about
1571
- * to be displayed: those assigned to a rendered layer whose data loaded
1572
- * successfully. Does nothing here; subclasses that keep
1573
- * 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
1574
1572
  * {@link resolveTiledImageDataTransfer}) override this to drop the state of
1575
- * every object that is not among the given ones.
1573
+ * all other objects.
1576
1574
  *
1577
1575
  * @param _objects - The objects (images or labels) about to be displayed
1578
1576
  */
@@ -1580,15 +1578,15 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1580
1578
  /**
1581
1579
  * Resolves what a renderer derives from an object, once its data has loaded
1582
1580
  *
1583
- * Called by {@link _loadObjects} for every object on one of the given layers,
1584
- * concurrently, right after
1585
- * its data has loaded and before its tiled images are created or updated. An
1586
- * object that cannot be resolved is logged, but kept: its tiled images are
1587
- * still created or updated, with whatever the synchronous hooks return for
1588
- * it. Does nothing here; subclasses override this to resolve, from the
1589
- * object, its data and the inputs of the synchronization, whatever their
1590
- * synchronous hooks return later (see {@link resolveTiledImageDataTransfer}),
1591
- * 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.
1592
1590
  *
1593
1591
  * @param _object - The object (image or labels) to resolve
1594
1592
  * @param _data - The loaded data of the object
@@ -1675,28 +1673,29 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1675
1673
  */
1676
1674
  resolveTiledImageDataTransfer(_ref, _index) {}
1677
1675
  /**
1678
- * 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
1677
+ *
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.
1679
1683
  *
1680
- * Each object's data is loaded with the context's `loadObject`, and the object
1681
- * is then resolved (see {@link resolveObject}). The returned references are
1682
- * ordered by layer and then by object, which determines the order of the
1683
- * corresponding tiled images in the world. Objects whose data failed to load
1684
- * are logged and skipped; objects that could not be resolved are logged, but
1685
- * kept. The model is read once, before the first `await`, so one
1686
- * synchronization sees one consistent model state, however often the model
1687
- * is set while it runs.
1684
+ * The model is read once, right away, so one synchronization sees one
1685
+ * consistent model state.
1688
1686
  *
1689
1687
  * @param context - The inputs of the current synchronization
1690
1688
  * @param options - Optional abort signal
1691
- * @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
1692
1691
  * @throws Error if no model has been set (see {@link setModel})
1693
1692
  */
1694
- async _loadObjects(context, options) {
1693
+ _loadObjects(context, options) {
1695
1694
  const { signal } = options ?? {};
1696
1695
  signal?.throwIfAborted();
1697
1696
  const model = this._model;
1698
1697
  if (model === void 0) throw new Error("Model not set");
1699
- const newRefPromises = [];
1698
+ const loadings = [];
1700
1699
  for (const currentLayer of model.layers) for (const currentObject of model.objects.filter((object) => object.layer === currentLayer.id)) {
1701
1700
  const newRefPromise = context.loadObject(currentObject, { signal }).then(async (data) => {
1702
1701
  try {
@@ -1714,57 +1713,62 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1714
1713
  newRefPromise.catch((error) => {
1715
1714
  if (!signal?.aborted) console.error(`Failed to load object with ID '${currentObject.id}'`, error);
1716
1715
  });
1717
- newRefPromises.push(newRefPromise);
1716
+ loadings.push({
1717
+ layer: currentLayer,
1718
+ object: currentObject,
1719
+ newRefPromise
1720
+ });
1718
1721
  }
1719
- const results = await Promise.allSettled(newRefPromises);
1720
- signal?.throwIfAborted();
1721
- return results.filter((result) => result.status === "fulfilled").map((result) => result.value);
1722
+ return loadings;
1722
1723
  }
1723
1724
  /**
1724
- * 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
1725
1726
  *
1726
1727
  * A rendered object is matched by the layer and object it references.
1727
- * Unmatched rendered objects are deleted first, so that removing an object
1728
- * does not shift the world indices of the objects behind it. A matched
1729
- * rendered object is reusable if the data source it was loaded for is that of
1730
- * the reference, and if its backdrop, if any, and all of its tiled images
1731
- * already sit at the consecutive world indices expected for its position
1732
- * among the references, counted from the anchor. A partially misplaced object
1733
- * is not reusable. The other matched rendered objects are deleted as well,
1734
- * and are expected to be recreated by the caller via
1735
- * {@link _createRenderedObject}, which is also how the world is reordered.
1736
- *
1737
- * The expected indices account for every matched rendered object, reusable or
1738
- * not, by the world footprint it currently has: a matched object that is
1739
- * recreated, e.g. because its data source changed, is deleted and inserted at
1740
- * the same position, with possibly different tiled images, so the objects
1741
- * behind it keep their indices relative to it and stay reusable. Only the
1742
- * backdrop and tiled images assigned to it count, i.e. none for an object
1743
- * whose tiled images could not be added, which is thereby retried without
1744
- * recreating the objects behind it. The tiled images that an earlier
1745
- * synchronization is still adding are waited for first, so that each
1746
- * rendered object is either fully in the world or not at all. This does not
1747
- * delay the additions of this synchronization, which are queued behind them
1748
- * anyway (see {@link OpenSeadragonContext.addTiledImage}).
1749
- *
1750
- * @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
1751
1755
  * @param options - Optional abort signal
1752
- * @returns A map of new object references to their reusable rendered objects
1756
+ * @returns A map of loadings to their reusable rendered objects
1753
1757
  * @throws Error if the renderer has no anchor, i.e. it is not initialized or
1754
1758
  * already destroyed
1755
1759
  */
1756
- async _cleanRenderedObjects(newRefs, options) {
1760
+ async _cleanRenderedObjects(loadings, options) {
1757
1761
  const { signal } = options ?? {};
1758
1762
  signal?.throwIfAborted();
1759
1763
  await Promise.allSettled(this._renderedObjects.map((renderedObject) => renderedObject.tiledImagesPromise));
1760
1764
  signal?.throwIfAborted();
1761
1765
  const matchedRenderedObjects = /* @__PURE__ */ new Set();
1762
- const matchedRenderedObjectsByNewRef = /* @__PURE__ */ new Map();
1763
- for (const newRef of newRefs) {
1764
- 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);
1765
1769
  if (renderedObject !== void 0) {
1766
1770
  matchedRenderedObjects.add(renderedObject);
1767
- matchedRenderedObjectsByNewRef.set(newRef, renderedObject);
1771
+ matchedRenderedObjectsByLoading.set(loading, renderedObject);
1768
1772
  }
1769
1773
  }
1770
1774
  const unmatchedDeletions = this._renderedObjects.filter((renderedObject) => !matchedRenderedObjects.has(renderedObject)).map((renderedObject) => this._deleteRenderedObject(renderedObject));
@@ -1774,14 +1778,14 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1774
1778
  if (this._anchor === void 0) throw new Error("Anchor not initialized");
1775
1779
  const anchorIndex = this.context.getTiledImageIndex(this._anchor);
1776
1780
  if (anchorIndex === -1) throw new Error("Anchor not found");
1777
- const renderedObjectsByNewRef = /* @__PURE__ */ new Map();
1781
+ const renderedObjectsByLoading = /* @__PURE__ */ new Map();
1778
1782
  const survivors = /* @__PURE__ */ new Set();
1779
1783
  let offset = 1;
1780
- for (const newRef of newRefs) {
1781
- const renderedObject = matchedRenderedObjectsByNewRef.get(newRef);
1784
+ for (const loading of loadings) {
1785
+ const renderedObject = matchedRenderedObjectsByLoading.get(loading);
1782
1786
  if (renderedObject !== void 0) {
1783
- 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)) {
1784
- 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);
1785
1789
  survivors.add(renderedObject);
1786
1790
  }
1787
1791
  offset += (renderedObject.backdrop !== void 0 ? 1 : 0) + (renderedObject.tiledImages?.length ?? 0);
@@ -1791,39 +1795,94 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1791
1795
  this._renderedObjects = [...survivors];
1792
1796
  await Promise.all(unreusableDeletions);
1793
1797
  signal?.throwIfAborted();
1794
- 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
+ ];
1795
1853
  }
1796
1854
  /**
1797
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
1798
1856
  *
1799
- * The TiledImages are inserted at consecutive indices, starting `offset` places
1800
- * after the anchor, which establishes the world layout that
1801
- * {@link _cleanRenderedObjects} expects. `offset` therefore counts TiledImages,
1802
- * not objects, and callers have to advance it by
1803
- * {@link RenderedObject.tileSourceCount} plus the object's backdrop, if it has
1804
- * one. The indices are resolved once each addition is executed, as the anchor
1805
- * may have been replaced, and world indices may have shifted, while it was
1806
- * enqueued.
1807
- *
1808
- * The tile sources are opened here, rather than by the additions themselves,
1809
- * so that the backdrop can be sized like the object's content, which is only
1810
- * known once one of them has been opened. The additions are all requested
1811
- * before this function returns, and in world order, so that the TiledImages of
1812
- * the object that the caller creates next end up behind them.
1813
- *
1814
- * Returns before the TiledImages exist: they are added asynchronously and only
1815
- * then assigned to the rendered object, transformed, and included in the anchor
1816
- * bounds, which is when {@link RenderedObject.tiledImagesPromise} resolves. The
1817
- * backdrop and TiledImages are removed again immediately after they were added
1818
- * if the rendered object is deleted or the operation is aborted in the
1819
- * meantime, or if any of them could not be added at all - a partially added
1820
- * object would shift the world indices of every object after it. For the same
1821
- * reason, a tile source that fails to open is replaced by a transparent
1822
- * placeholder rather than skipped: the indices of the objects created after
1823
- * this one are resolved against the full footprint of this one, so it has to
1824
- * take up all of its indices until it is removed again. If the context
1825
- * is destroyed, nothing is done at all, as the viewer tears down its world
1826
- * 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.
1827
1886
  *
1828
1887
  * @param offset - The number of TiledImages between the anchor and the object's first new TiledImage
1829
1888
  * @param newRef - The object reference for which to create a rendered object
@@ -1896,8 +1955,9 @@ var OpenSeadragonRendererBase = class OpenSeadragonRendererBase {
1896
1955
  if (this.context.isDestroyed()) return tiledImages;
1897
1956
  const failure = [...tileSourceResults, ...results].find((result) => result.status === "rejected");
1898
1957
  if (signal?.aborted || failure !== void 0 || newRenderedObject.pendingDelete || this._destroyed) {
1899
- if (backdrop !== void 0) await this.context.removeTiledImage(backdrop);
1900
- 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)));
1901
1961
  signal?.throwIfAborted();
1902
1962
  if (failure !== void 0) {
1903
1963
  console.error(`Failed to add tiled images for object with ID '${newRef.object.id}'`, failure.reason);
@@ -3983,7 +4043,6 @@ var WebGLRendererBase = class {
3983
4043
  };
3984
4044
  _model;
3985
4045
  _lastSyncState;
3986
- _hasUnreportedChanges = false;
3987
4046
  _renderedObjects = /* @__PURE__ */ new Map();
3988
4047
  _layerItemsInfosCache = /* @__PURE__ */ new WeakMap();
3989
4048
  /**
@@ -4065,59 +4124,52 @@ var WebGLRendererBase = class {
4065
4124
  /**
4066
4125
  * Synchronizes the rendered objects with the current model and render options
4067
4126
  *
4068
- * Loads the objects of the model (see {@link loadObjects}), drops the
4069
- * rendered objects that no longer match one (see
4070
- * {@link matchOrDestroyRenderedObjects}), and then creates or updates the
4071
- * rest in two passes. The first prepares every object (see
4072
- * {@link prepareRenderedObject}), issuing all requests before the first
4073
- * `await`: requests are shared with, and cancelled once abandoned by, a
4074
- * superseded synchronization, unless they are reclaimed within the same
4075
- * task. The second awaits the preparations in order and uploads each of them
4076
- * in one synchronous block (see {@link createRenderedObject} and
4077
- * {@link updateRenderedObject}), so a draw never sees a half-updated object.
4078
- *
4079
- * An object whose preparation fails, or finds nothing to render, is logged
4080
- * and dropped. A synchronization that fails or is aborted leaves the model
4081
- * unsynchronized (see {@link _discardSyncState}), and the changes it made
4082
- * 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}).
4083
4144
  *
4084
4145
  * @param syncContext - The inputs to synchronize with: the tables and
4085
- * group-to-value maps that the objects resolve their properties from, and
4086
- * the loaders for object and table data. It carries inputs only; what an
4087
- * object was resolved from is captured per object (see {@link ObjectRef})
4088
- * @param options - Optional abort signal
4089
- * @returns A promise that resolves to whether any rendered object changed
4090
- * 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
4091
4153
  * @throws Error if no model has been set (see {@link setModel})
4092
4154
  */
4093
4155
  async synchronize(syncContext, options) {
4094
- const { signal } = options ?? {};
4156
+ const { signal, onChange } = options ?? {};
4095
4157
  signal?.throwIfAborted();
4096
4158
  const syncState = this._recordSyncState();
4097
4159
  try {
4098
- const newRefs = await this.loadObjects(syncContext, { signal });
4099
- const preparations = this.matchOrDestroyRenderedObjects(newRefs).map(({ newRef, renderedObject }) => {
4100
- const preparedPromise = this.prepareRenderedObject(newRef, renderedObject, syncContext, { signal });
4101
- preparedPromise.catch(() => {});
4102
- return {
4103
- newRef,
4104
- renderedObject,
4105
- preparedPromise
4106
- };
4107
- });
4108
- for (const { newRef, renderedObject, preparedPromise } of preparations) {
4109
- const prepared = await this._settleRenderedObjectPreparation(newRef, renderedObject, preparedPromise, { signal });
4110
- if (prepared === void 0) continue;
4111
- if (renderedObject === void 0) this.addRenderedObject(this.createRenderedObject(newRef, prepared));
4112
- else if (this.updateRenderedObject(renderedObject, prepared)) this._hasUnreportedChanges = true;
4113
- }
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;
4114
4169
  } catch (error) {
4115
4170
  this._discardSyncState(syncState);
4116
4171
  throw error;
4117
4172
  }
4118
- const hasUnreportedChanges = this._hasUnreportedChanges;
4119
- this._hasUnreportedChanges = false;
4120
- return hasUnreportedChanges;
4121
4173
  }
4122
4174
  /**
4123
4175
  * Returns the state of the current model and render options that a
@@ -4197,29 +4249,31 @@ var WebGLRendererBase = class {
4197
4249
  */
4198
4250
  getRenderOptionsSyncState() {}
4199
4251
  /**
4200
- * 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
4201
4253
  *
4202
- * 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
4203
4255
  * object assigned per item by a table column is loaded for every layer, with
4204
4256
  * the items on each layer resolved from the table (see
4205
4257
  * {@link _getLayerItemsInfos}). The data of an object, and of a table, is
4206
- * loaded once, no matter how many references it is shared by.
4258
+ * loaded once, however many references share it.
4207
4259
  *
4208
- * The returned references are ordered by layer and then by object. Objects
4209
- * whose data or table failed to load are logged and skipped, and so are
4210
- * objects whose items are assigned per item without a table to resolve the
4211
- * assignment from. Objects without items on a layer are skipped silently,
4212
- * which also covers empty objects and objects whose table is empty - those
4213
- * 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.
4214
4267
  *
4215
4268
  * @param syncContext - The inputs of the current synchronization: the tables
4216
- * that the objects resolve their item layers from, and the loaders for
4217
- * object and table data
4269
+ * that the objects resolve their item layers from, and the loaders for object
4270
+ * and table data
4218
4271
  * @param options - Optional abort signal
4219
- * @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
4220
4274
  * @throws Error if no model has been set (see {@link setModel})
4221
4275
  */
4222
- async loadObjects(syncContext, options) {
4276
+ loadObjects(syncContext, options) {
4223
4277
  const { signal } = options ?? {};
4224
4278
  signal?.throwIfAborted();
4225
4279
  const model = this._model;
@@ -4228,7 +4282,7 @@ var WebGLRendererBase = class {
4228
4282
  const tableDataPromises = /* @__PURE__ */ new Map();
4229
4283
  const tableLayersPromises = /* @__PURE__ */ new Map();
4230
4284
  const layerItemsInfosPromises = /* @__PURE__ */ new Map();
4231
- const newRefPromises = [];
4285
+ const loadings = [];
4232
4286
  const objectIdsWithoutTable = /* @__PURE__ */ new Set();
4233
4287
  for (const currentLayer of model.layers) for (const currentObject of model.objects) {
4234
4288
  if (typeof currentObject.layer === "string" && currentObject.layer !== currentLayer.id) continue;
@@ -4292,67 +4346,33 @@ var WebGLRendererBase = class {
4292
4346
  }
4293
4347
  const newRefPromise = Promise.all([dataPromise, layerItemsInfosPromise]).then(([data, layerItemsInfos]) => {
4294
4348
  signal?.throwIfAborted();
4349
+ let newRef;
4295
4350
  if (layerItemsInfos !== void 0) {
4296
4351
  const itemsInfo = layerItemsInfos.get(currentLayer.id);
4297
- return {
4352
+ newRef = {
4298
4353
  layerId: currentLayer.id,
4299
4354
  object: currentObject,
4300
4355
  itemIds: itemsInfo?.itemIds ?? [],
4301
4356
  itemsMask: itemsInfo?.itemsMask,
4302
4357
  data
4303
4358
  };
4304
- }
4305
- return {
4359
+ } else newRef = {
4306
4360
  layerId: currentLayer.id,
4307
4361
  object: currentObject,
4308
4362
  itemIds: data.getIds(),
4309
4363
  itemsMask: void 0,
4310
4364
  data
4311
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
4312
4373
  });
4313
- newRefPromises.push(newRefPromise);
4314
- }
4315
- const results = await Promise.allSettled(newRefPromises);
4316
- signal?.throwIfAborted();
4317
- return results.filter((result) => result.status === "fulfilled").filter((result) => result.value.itemIds.length > 0).map((result) => result.value);
4318
- }
4319
- /**
4320
- * Matches the rendered objects to a new set of references, and destroys the
4321
- * ones left over
4322
- *
4323
- * A rendered object is matched by the layer and object it is kept under, and
4324
- * kept if its contributed items and data source are those of the reference,
4325
- * which it then adopts and is returned for reuse with. Every other rendered
4326
- * object - one that no reference addresses, or one whose items or data source
4327
- * changed - is destroyed and dropped.
4328
- *
4329
- * @param newRefs - The object references to match against
4330
- * @returns One match per reference, in the order of the references, each with
4331
- * the rendered object to reuse for it, or `undefined` if it has none yet
4332
- */
4333
- matchOrDestroyRenderedObjects(newRefs) {
4334
- const matchedRenderedObjects = /* @__PURE__ */ new Set();
4335
- const matches = newRefs.map((newRef) => {
4336
- const renderedObject = this._renderedObjects.get(newRef.layerId)?.get(newRef.object.id);
4337
- if (renderedObject !== void 0 && renderedObject.ref.itemIds === newRef.itemIds && renderedObject.ref.itemsMask === newRef.itemsMask && deepEqual(renderedObject.ref.object.dataSource, newRef.object.dataSource)) {
4338
- renderedObject.ref = newRef;
4339
- matchedRenderedObjects.add(renderedObject);
4340
- return {
4341
- newRef,
4342
- renderedObject
4343
- };
4344
- }
4345
- return {
4346
- newRef,
4347
- renderedObject: void 0
4348
- };
4349
- });
4350
- for (const renderedObjects of this._renderedObjects.values()) for (const [objectId, renderedObject] of renderedObjects) if (!matchedRenderedObjects.has(renderedObject)) {
4351
- renderedObjects.delete(objectId);
4352
- this.destroyRenderedObject(renderedObject);
4353
- this._hasUnreportedChanges = true;
4354
4374
  }
4355
- return matches;
4375
+ return loadings;
4356
4376
  }
4357
4377
  /**
4358
4378
  * Adds a newly created rendered object under its layer and object
@@ -4367,7 +4387,6 @@ var WebGLRendererBase = class {
4367
4387
  this._renderedObjects.set(layerId, renderedObjects);
4368
4388
  }
4369
4389
  renderedObjects.set(object.id, renderedObject);
4370
- this._hasUnreportedChanges = true;
4371
4390
  }
4372
4391
  /**
4373
4392
  * Drops a rendered object and releases its GPU resources
@@ -4377,7 +4396,6 @@ var WebGLRendererBase = class {
4377
4396
  removeRenderedObject(renderedObject) {
4378
4397
  this._renderedObjects.get(renderedObject.ref.layerId)?.delete(renderedObject.ref.object.id);
4379
4398
  this.destroyRenderedObject(renderedObject);
4380
- this._hasUnreportedChanges = true;
4381
4399
  }
4382
4400
  /**
4383
4401
  * Drops all rendered objects and releases their GPU resources
@@ -4385,7 +4403,6 @@ var WebGLRendererBase = class {
4385
4403
  clearRenderedObjects() {
4386
4404
  for (const renderedObjects of this._renderedObjects.values()) for (const renderedObject of renderedObjects.values()) this.destroyRenderedObject(renderedObject);
4387
4405
  this._renderedObjects.clear();
4388
- this._hasUnreportedChanges = true;
4389
4406
  }
4390
4407
  /**
4391
4408
  * Returns one render pass per rendered object to draw, in draw order
@@ -4531,35 +4548,90 @@ var WebGLRendererBase = class {
4531
4548
  return layerItemsInfos;
4532
4549
  }
4533
4550
  /**
4534
- * Awaits the preparation of an object, dropping the object if it fails or
4535
- * finds nothing to render
4551
+ * Drops the rendered objects that none of the given loadings addresses
4536
4552
  *
4537
- * @param newRef - The prepared object
4538
- * @param renderedObject - The object's current rendered state, if any, which
4539
- * is removed if the object is dropped
4540
- * @param preparedPromise - The preparation, see {@link prepareRenderedObject}
4541
- * @param options - Optional abort signal
4542
- * @returns The preparation, or `undefined` if the object was dropped (which
4543
- * 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
4544
4562
  */
4545
- async _settleRenderedObjectPreparation(newRef, renderedObject, preparedPromise, options) {
4546
- 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
+ }
4547
4614
  let prepared;
4548
4615
  try {
4549
- prepared = await preparedPromise;
4616
+ prepared = await this.prepareRenderedObject(newRef, renderedObject, syncContext, { signal });
4617
+ signal?.throwIfAborted();
4550
4618
  } catch (error) {
4551
4619
  signal?.throwIfAborted();
4552
4620
  console.error(`Failed to prepare object with ID '${newRef.object.id}'`, error);
4553
- if (renderedObject !== void 0) this.removeRenderedObject(renderedObject);
4554
- return;
4555
4621
  }
4556
- signal?.throwIfAborted();
4557
- if (prepared === null) {
4558
- console.warn(`Object with ID '${newRef.object.id}' has nothing to render, skipping`);
4559
- 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
+ }
4560
4628
  return;
4561
4629
  }
4562
- 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?.();
4563
4635
  }
4564
4636
  /**
4565
4637
  * Creates the loader for the tables that an object resolves its properties