canvu-react 0.3.12 → 0.3.14

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/react.d.cts CHANGED
@@ -107,10 +107,18 @@ type IngestAssetFilesToSceneItemsOptions = {
107
107
  /**
108
108
  * PDF rasterization scale forwarded to `pdfjs-dist`.
109
109
  *
110
- * Defaults to `1.5` for crisp zoom while keeping payloads reasonable.
110
+ * Defaults to `1.15` to balance first paint latency with zoom quality.
111
111
  */
112
112
  pdfScale?: number;
113
113
  pdfPageConcurrency?: number;
114
+ /**
115
+ * Called as soon as canvu finishes converting one or more items from the
116
+ * source files.
117
+ */
118
+ onItemsReady?: (items: VectorSceneItem[], context: {
119
+ file: File;
120
+ kind: VectorViewportAssetKind;
121
+ }) => void;
114
122
  /**
115
123
  * Final hook to customize each created item after canvu attaches its own
116
124
  * image metadata and any `assetStore.upload(...)` `pluginData`.
package/dist/react.d.ts CHANGED
@@ -107,10 +107,18 @@ type IngestAssetFilesToSceneItemsOptions = {
107
107
  /**
108
108
  * PDF rasterization scale forwarded to `pdfjs-dist`.
109
109
  *
110
- * Defaults to `1.5` for crisp zoom while keeping payloads reasonable.
110
+ * Defaults to `1.15` to balance first paint latency with zoom quality.
111
111
  */
112
112
  pdfScale?: number;
113
113
  pdfPageConcurrency?: number;
114
+ /**
115
+ * Called as soon as canvu finishes converting one or more items from the
116
+ * source files.
117
+ */
118
+ onItemsReady?: (items: VectorSceneItem[], context: {
119
+ file: File;
120
+ kind: VectorViewportAssetKind;
121
+ }) => void;
114
122
  /**
115
123
  * Final hook to customize each created item after canvu attaches its own
116
124
  * image metadata and any `assetStore.upload(...)` `pluginData`.
package/dist/react.js CHANGED
@@ -949,10 +949,32 @@ async function runWithConcurrency(items, concurrency, execute) {
949
949
  async function loadPdfToStore(file, store, options) {
950
950
  const scale = options?.scale ?? 1.5;
951
951
  const pageConcurrency = options?.pageConcurrency ?? 2;
952
+ const storeThumbnails = options?.storeThumbnails ?? false;
952
953
  const pdfjs = await getPdfJs();
953
954
  const arrayBuffer = await file.arrayBuffer();
954
955
  const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise;
955
956
  const pageNumbers = normalizePdfPageNumbers(options?.pageNumbers, pdf.numPages);
957
+ const bufferedResults = /* @__PURE__ */ new Map();
958
+ let nextEmitIndex = 0;
959
+ let emitChain = Promise.resolve();
960
+ const queuePageEmission = async (pageResult) => {
961
+ bufferedResults.set(pageResult.pageNumber, pageResult);
962
+ const run = async () => {
963
+ while (nextEmitIndex < pageNumbers.length) {
964
+ const nextPageNumber = pageNumbers[nextEmitIndex];
965
+ if (nextPageNumber == null) break;
966
+ const bufferedResult = bufferedResults.get(nextPageNumber);
967
+ if (!bufferedResult) break;
968
+ bufferedResults.delete(nextPageNumber);
969
+ nextEmitIndex += 1;
970
+ await options?.onPageStored?.(bufferedResult);
971
+ }
972
+ };
973
+ const nextChain = emitChain.then(run, run);
974
+ emitChain = nextChain.catch(() => {
975
+ });
976
+ await nextChain;
977
+ };
956
978
  return await runWithConcurrency(
957
979
  pageNumbers,
958
980
  pageConcurrency,
@@ -962,27 +984,31 @@ async function loadPdfToStore(file, store, options) {
962
984
  const mime = "image/png";
963
985
  const pageBlob = await canvasToBlob2(canvas, mime);
964
986
  const blobId = await store.storeOriginal(pageBlob);
965
- const thumbScale = Math.min(1, 256 / Math.max(width, height));
966
- const tw = Math.max(1, Math.round(width * thumbScale));
967
- const th = Math.max(1, Math.round(height * thumbScale));
968
- const thumbCanvas = document.createElement("canvas");
969
- thumbCanvas.width = tw;
970
- thumbCanvas.height = th;
971
- const tCtx = thumbCanvas.getContext("2d");
972
- if (tCtx) {
973
- tCtx.imageSmoothingEnabled = true;
974
- tCtx.imageSmoothingQuality = "high";
975
- tCtx.drawImage(canvas, 0, 0, tw, th);
976
- }
977
- const thumbBlob = await canvasToBlob2(thumbCanvas, mime);
978
- const thumbnailBlobId = await store.storeThumbnail(thumbBlob);
979
- return {
987
+ const thumbnailBlobId = storeThumbnails ? await (async () => {
988
+ const thumbScale = Math.min(1, 256 / Math.max(width, height));
989
+ const tw = Math.max(1, Math.round(width * thumbScale));
990
+ const th = Math.max(1, Math.round(height * thumbScale));
991
+ const thumbCanvas = document.createElement("canvas");
992
+ thumbCanvas.width = tw;
993
+ thumbCanvas.height = th;
994
+ const tCtx = thumbCanvas.getContext("2d");
995
+ if (tCtx) {
996
+ tCtx.imageSmoothingEnabled = true;
997
+ tCtx.imageSmoothingQuality = "high";
998
+ tCtx.drawImage(canvas, 0, 0, tw, th);
999
+ }
1000
+ const thumbBlob = await canvasToBlob2(thumbCanvas, mime);
1001
+ return await store.storeThumbnail(thumbBlob);
1002
+ })() : "";
1003
+ const pageResult = {
980
1004
  blobId,
981
1005
  thumbnailBlobId,
982
1006
  width,
983
1007
  height,
984
1008
  pageNumber
985
1009
  };
1010
+ await queuePageEmission(pageResult);
1011
+ return pageResult;
986
1012
  }
987
1013
  );
988
1014
  }
@@ -992,7 +1018,8 @@ init_shape_builders();
992
1018
  var HYDRATION_CACHE_NAME = "canvu-asset-hydration-v1";
993
1019
  var DEFAULT_PDF_SCALE = 1.15;
994
1020
  var DEFAULT_PDF_PAGE_CONCURRENCY = 2;
995
- var hydrationBlobPromises = /* @__PURE__ */ new Map();
1021
+ var hydrationSourcePromises = /* @__PURE__ */ new Map();
1022
+ var pdfSourceBlobPromises = /* @__PURE__ */ new Map();
996
1023
  function buildImageHydrationKey(assetId) {
997
1024
  return `image:${assetId}`;
998
1025
  }
@@ -1040,23 +1067,38 @@ async function fetchBlob(url) {
1040
1067
  return null;
1041
1068
  }
1042
1069
  }
1043
- async function getHydrationBlob(key, preferCachedRasters, loader) {
1070
+ async function getHydrationSource(key, preferCachedRasters, loader) {
1044
1071
  const cached = preferCachedRasters ? await readCachedHydrationBlob(key) : null;
1045
- if (cached) return cached;
1046
- const inFlight = hydrationBlobPromises.get(key);
1072
+ if (cached) {
1073
+ return {
1074
+ blob: cached
1075
+ };
1076
+ }
1077
+ const inFlight = hydrationSourcePromises.get(key);
1047
1078
  if (inFlight) return await inFlight;
1048
1079
  const nextPromise = (async () => {
1049
- const blob = await loader();
1050
- if (blob) {
1051
- await writeCachedHydrationBlob(key, blob);
1080
+ const source = await loader();
1081
+ if (source?.blob) {
1082
+ await writeCachedHydrationBlob(key, source.blob);
1052
1083
  }
1053
- return blob;
1084
+ return source;
1054
1085
  })();
1055
- hydrationBlobPromises.set(key, nextPromise);
1086
+ hydrationSourcePromises.set(key, nextPromise);
1087
+ try {
1088
+ return await nextPromise;
1089
+ } finally {
1090
+ hydrationSourcePromises.delete(key);
1091
+ }
1092
+ }
1093
+ async function getPdfSourceBlob(url) {
1094
+ const inFlight = pdfSourceBlobPromises.get(url);
1095
+ if (inFlight) return await inFlight;
1096
+ const nextPromise = fetchBlob(url);
1097
+ pdfSourceBlobPromises.set(url, nextPromise);
1056
1098
  try {
1057
1099
  return await nextPromise;
1058
1100
  } finally {
1059
- hydrationBlobPromises.delete(key);
1101
+ pdfSourceBlobPromises.delete(url);
1060
1102
  }
1061
1103
  }
1062
1104
  function registerObjectUrl(objectUrls, blob) {
@@ -1110,18 +1152,22 @@ async function hydrateImageAssets(requests, resolvedAssetUrls, objectUrls, prefe
1110
1152
  if (!resolvedAsset?.url) {
1111
1153
  return [assetId, null];
1112
1154
  }
1113
- const blob = await getHydrationBlob(
1155
+ const source = await getHydrationSource(
1114
1156
  buildImageHydrationKey(assetId),
1115
1157
  preferCachedRasters,
1116
- async () => await fetchBlob(resolvedAsset.url)
1158
+ async () => {
1159
+ const blob = await fetchBlob(resolvedAsset.url);
1160
+ if (!blob) return null;
1161
+ return { blob };
1162
+ }
1117
1163
  );
1118
- if (!blob) {
1164
+ if (!source?.blob) {
1119
1165
  return [assetId, null];
1120
1166
  }
1121
1167
  return [
1122
1168
  assetId,
1123
1169
  {
1124
- href: registerObjectUrl(objectUrls, blob)
1170
+ href: registerObjectUrl(objectUrls, source.blob)
1125
1171
  }
1126
1172
  ];
1127
1173
  })
@@ -1136,45 +1182,69 @@ async function hydratePdfAssets(requests, resolvedAssetUrls, objectUrls, options
1136
1182
  if (!resolvedAsset?.url) {
1137
1183
  continue;
1138
1184
  }
1139
- const missingPages = [];
1140
- for (const pageNumber of group.pageNumbers) {
1141
- const cacheKey = buildPdfHydrationKey(group.assetId, pageNumber, group.scale);
1142
- const cachedBlob = await readCachedHydrationBlob(cacheKey);
1143
- if (!cachedBlob) {
1144
- missingPages.push(pageNumber);
1185
+ const pageKeys = group.pageNumbers.map((pageNumber) => ({
1186
+ pageNumber,
1187
+ cacheKey: buildPdfHydrationKey(group.assetId, pageNumber, group.scale)
1188
+ }));
1189
+ const pagesToRender = [];
1190
+ for (const { pageNumber, cacheKey } of pageKeys) {
1191
+ const cachedBlob = options.preferCachedRasters ? await readCachedHydrationBlob(cacheKey) : null;
1192
+ if (cachedBlob) {
1193
+ hydratedPages.set(cacheKey, {
1194
+ href: registerObjectUrl(objectUrls, cachedBlob)
1195
+ });
1145
1196
  continue;
1146
1197
  }
1147
- hydratedPages.set(cacheKey, {
1148
- href: registerObjectUrl(objectUrls, cachedBlob)
1149
- });
1150
- }
1151
- if (missingPages.length === 0) {
1152
- continue;
1198
+ if (!hydrationSourcePromises.has(cacheKey)) {
1199
+ pagesToRender.push({ pageNumber, cacheKey });
1200
+ }
1153
1201
  }
1154
- const pdfBlob = await fetchBlob(resolvedAsset.url);
1155
- if (!pdfBlob) {
1156
- continue;
1202
+ if (pagesToRender.length > 0) {
1203
+ const renderPromise = (async () => {
1204
+ const pdfBlob = await getPdfSourceBlob(resolvedAsset.url);
1205
+ if (!pdfBlob) {
1206
+ return /* @__PURE__ */ new Map();
1207
+ }
1208
+ const renderedPages = await loadPdfToStore(pdfBlob, options.imageStore, {
1209
+ scale: group.scale,
1210
+ pageNumbers: pagesToRender.map(({ pageNumber }) => pageNumber),
1211
+ pageConcurrency: options.pdfPageConcurrency
1212
+ });
1213
+ const renderedByPage = /* @__PURE__ */ new Map();
1214
+ for (const renderedPage of renderedPages) {
1215
+ const pageBlob = await options.imageStore.getOriginal(renderedPage.blobId);
1216
+ renderedByPage.set(
1217
+ renderedPage.pageNumber,
1218
+ pageBlob ? {
1219
+ blob: pageBlob,
1220
+ width: renderedPage.width,
1221
+ height: renderedPage.height
1222
+ } : null
1223
+ );
1224
+ }
1225
+ return renderedByPage;
1226
+ })();
1227
+ for (const { pageNumber, cacheKey } of pagesToRender) {
1228
+ const pagePromise = getHydrationSource(
1229
+ cacheKey,
1230
+ options.preferCachedRasters,
1231
+ async () => (await renderPromise).get(pageNumber) ?? null
1232
+ );
1233
+ hydrationSourcePromises.set(cacheKey, pagePromise);
1234
+ }
1157
1235
  }
1158
- const renderedPages = await loadPdfToStore(pdfBlob, options.imageStore, {
1159
- scale: group.scale,
1160
- pageNumbers: missingPages,
1161
- pageConcurrency: options.pdfPageConcurrency
1162
- });
1163
- for (const renderedPage of renderedPages) {
1164
- const cacheKey = buildPdfHydrationKey(
1165
- group.assetId,
1166
- renderedPage.pageNumber,
1167
- group.scale
1236
+ for (const { cacheKey } of pageKeys) {
1237
+ if (hydratedPages.has(cacheKey)) continue;
1238
+ const source = await getHydrationSource(
1239
+ cacheKey,
1240
+ options.preferCachedRasters,
1241
+ async () => null
1168
1242
  );
1169
- const pageBlob = await options.imageStore.getOriginal(renderedPage.blobId);
1170
- if (!pageBlob) {
1171
- continue;
1172
- }
1173
- await writeCachedHydrationBlob(cacheKey, pageBlob);
1243
+ if (!source?.blob) continue;
1174
1244
  hydratedPages.set(cacheKey, {
1175
- href: registerObjectUrl(objectUrls, pageBlob),
1176
- width: renderedPage.width,
1177
- height: renderedPage.height
1245
+ href: registerObjectUrl(objectUrls, source.blob),
1246
+ width: source.width,
1247
+ height: source.height
1178
1248
  });
1179
1249
  }
1180
1250
  }
@@ -1215,7 +1285,9 @@ async function hydrateSceneItemsWithAssets(items, assetStore, options = {}) {
1215
1285
  objectUrls,
1216
1286
  {
1217
1287
  imageStore,
1218
- pdfPageConcurrency}
1288
+ pdfPageConcurrency,
1289
+ preferCachedRasters
1290
+ }
1219
1291
  );
1220
1292
  return {
1221
1293
  items: items.map((item) => {
@@ -1292,9 +1364,10 @@ async function ingestAssetFilesToSceneItems(options) {
1292
1364
  createId = createShapeId,
1293
1365
  gapWorld = 16,
1294
1366
  stepWorld = 48,
1295
- pdfScale = 1.5,
1367
+ pdfScale = 1.15,
1296
1368
  pdfPageConcurrency = 2,
1297
1369
  decorateItem,
1370
+ onItemsReady,
1298
1371
  onError
1299
1372
  } = options;
1300
1373
  const items = [];
@@ -1316,55 +1389,56 @@ async function ingestAssetFilesToSceneItems(options) {
1316
1389
  }
1317
1390
  try {
1318
1391
  if (kind === "pdf") {
1319
- const [uploadResult2, pages] = await Promise.all([
1320
- uploadAssetIfNeeded(assetStore, file, kind),
1321
- loadPdfToStore(file, imageStore, {
1322
- scale: pdfScale,
1323
- pageConcurrency: pdfPageConcurrency
1324
- })
1325
- ]);
1326
- for (const page of pages) {
1327
- const fullUrl2 = await createBlobUrlFromStore(imageStore, page.blobId);
1328
- const naturalTopY2 = worldCenter.y - page.height / 2;
1329
- const stackedTopY = occupiedBottomY == null ? naturalTopY2 : occupiedBottomY + gapWorld;
1330
- const bounds2 = {
1331
- x: worldCenter.x - page.width / 2,
1332
- y: Math.max(naturalTopY2, stackedTopY),
1333
- width: page.width,
1334
- height: page.height
1335
- };
1336
- const itemContext2 = {
1337
- file,
1338
- kind,
1339
- itemIndex: items.length,
1340
- pageNumber: page.pageNumber
1341
- };
1342
- const item2 = finalizeIngestedItem(
1343
- {
1344
- id: createId(),
1345
- x: bounds2.x,
1346
- y: bounds2.y,
1347
- bounds: { ...bounds2 },
1348
- toolKind: "image",
1349
- imageBlobId: page.blobId,
1350
- imageRasterHref: fullUrl2 ?? void 0,
1351
- imageIntrinsicSize: {
1352
- width: page.width,
1353
- height: page.height
1392
+ const uploadResultPromise = uploadAssetIfNeeded(assetStore, file, kind);
1393
+ await loadPdfToStore(file, imageStore, {
1394
+ scale: pdfScale,
1395
+ pageConcurrency: pdfPageConcurrency,
1396
+ storeThumbnails: false,
1397
+ onPageStored: async (page) => {
1398
+ const uploadResult2 = await uploadResultPromise;
1399
+ const fullUrl2 = await createBlobUrlFromStore(imageStore, page.blobId);
1400
+ const naturalTopY2 = worldCenter.y - page.height / 2;
1401
+ const stackedTopY = occupiedBottomY == null ? naturalTopY2 : occupiedBottomY + gapWorld;
1402
+ const bounds2 = {
1403
+ x: worldCenter.x - page.width / 2,
1404
+ y: Math.max(naturalTopY2, stackedTopY),
1405
+ width: page.width,
1406
+ height: page.height
1407
+ };
1408
+ const itemContext2 = {
1409
+ file,
1410
+ kind,
1411
+ itemIndex: items.length,
1412
+ pageNumber: page.pageNumber
1413
+ };
1414
+ const item2 = finalizeIngestedItem(
1415
+ {
1416
+ id: createId(),
1417
+ x: bounds2.x,
1418
+ y: bounds2.y,
1419
+ bounds: { ...bounds2 },
1420
+ toolKind: "image",
1421
+ imageBlobId: page.blobId,
1422
+ imageRasterHref: fullUrl2 ?? void 0,
1423
+ imageIntrinsicSize: {
1424
+ width: page.width,
1425
+ height: page.height
1426
+ },
1427
+ childrenSvg: fullUrl2 ? buildRasterImageChildrenSvg(
1428
+ fullUrl2,
1429
+ { width: page.width, height: page.height },
1430
+ bounds2
1431
+ ) : ""
1354
1432
  },
1355
- childrenSvg: fullUrl2 ? buildRasterImageChildrenSvg(
1356
- fullUrl2,
1357
- { width: page.width, height: page.height },
1358
- bounds2
1359
- ) : ""
1360
- },
1361
- itemContext2,
1362
- uploadResult2,
1363
- decorateItem
1364
- );
1365
- items.push(item2);
1366
- occupiedBottomY = bounds2.y + page.height;
1367
- }
1433
+ itemContext2,
1434
+ uploadResult2,
1435
+ decorateItem
1436
+ );
1437
+ items.push(item2);
1438
+ occupiedBottomY = bounds2.y + page.height;
1439
+ onItemsReady?.([item2], { file, kind });
1440
+ }
1441
+ });
1368
1442
  hasImagePlacementBase = false;
1369
1443
  imagePlacementIndex = 0;
1370
1444
  imageYOffsetAdjustment = 0;
@@ -1417,6 +1491,7 @@ async function ingestAssetFilesToSceneItems(options) {
1417
1491
  decorateItem
1418
1492
  );
1419
1493
  items.push(item);
1494
+ onItemsReady?.([item], { file, kind });
1420
1495
  imagePlacementIndex++;
1421
1496
  occupiedBottomY = occupiedBottomY == null ? bounds.y + height : Math.max(occupiedBottomY, bounds.y + height);
1422
1497
  } catch (error) {
@@ -7329,7 +7404,13 @@ var VectorViewport = forwardRef(
7329
7404
  y: worldY
7330
7405
  },
7331
7406
  imageStore: store,
7332
- assetStore: assetStoreRef.current ?? void 0
7407
+ assetStore: assetStoreRef.current ?? void 0,
7408
+ onItemsReady: (nextItems) => {
7409
+ if (nextItems.length === 0) return;
7410
+ setLoadingSkeletons([]);
7411
+ change([...itemsRef.current, ...nextItems]);
7412
+ setEffectiveSelectedIdsRef.current(nextItems.map((item) => item.id));
7413
+ }
7333
7414
  });
7334
7415
  if (result.errors.length > 0) {
7335
7416
  for (const error of result.errors) {
@@ -7337,8 +7418,6 @@ var VectorViewport = forwardRef(
7337
7418
  }
7338
7419
  }
7339
7420
  if (result.items.length === 0) return;
7340
- change([...itemsRef.current, ...result.items]);
7341
- setEffectiveSelectedIdsRef.current(result.items.map((item) => item.id));
7342
7421
  } finally {
7343
7422
  setLoadingSkeletons([]);
7344
7423
  }