canvu-react 0.3.13 → 0.3.16

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
@@ -68,6 +68,7 @@ type IngestAssetFileError = {
68
68
  */
69
69
  type IngestAssetFilesToSceneItemsOptions = {
70
70
  files: readonly File[];
71
+ existingItems?: readonly VectorSceneItem[];
71
72
  worldCenter: {
72
73
  x: number;
73
74
  y: number;
@@ -107,10 +108,18 @@ type IngestAssetFilesToSceneItemsOptions = {
107
108
  /**
108
109
  * PDF rasterization scale forwarded to `pdfjs-dist`.
109
110
  *
110
- * Defaults to `1.5` for crisp zoom while keeping payloads reasonable.
111
+ * Defaults to `1.15` to balance first paint latency with zoom quality.
111
112
  */
112
113
  pdfScale?: number;
113
114
  pdfPageConcurrency?: number;
115
+ /**
116
+ * Called as soon as canvu finishes converting one or more items from the
117
+ * source files.
118
+ */
119
+ onItemsReady?: (items: VectorSceneItem[], context: {
120
+ file: File;
121
+ kind: VectorViewportAssetKind;
122
+ }) => void;
114
123
  /**
115
124
  * Final hook to customize each created item after canvu attaches its own
116
125
  * image metadata and any `assetStore.upload(...)` `pluginData`.
package/dist/react.d.ts CHANGED
@@ -68,6 +68,7 @@ type IngestAssetFileError = {
68
68
  */
69
69
  type IngestAssetFilesToSceneItemsOptions = {
70
70
  files: readonly File[];
71
+ existingItems?: readonly VectorSceneItem[];
71
72
  worldCenter: {
72
73
  x: number;
73
74
  y: number;
@@ -107,10 +108,18 @@ type IngestAssetFilesToSceneItemsOptions = {
107
108
  /**
108
109
  * PDF rasterization scale forwarded to `pdfjs-dist`.
109
110
  *
110
- * Defaults to `1.5` for crisp zoom while keeping payloads reasonable.
111
+ * Defaults to `1.15` to balance first paint latency with zoom quality.
111
112
  */
112
113
  pdfScale?: number;
113
114
  pdfPageConcurrency?: number;
115
+ /**
116
+ * Called as soon as canvu finishes converting one or more items from the
117
+ * source files.
118
+ */
119
+ onItemsReady?: (items: VectorSceneItem[], context: {
120
+ file: File;
121
+ kind: VectorViewportAssetKind;
122
+ }) => void;
114
123
  /**
115
124
  * Final hook to customize each created item after canvu attaches its own
116
125
  * 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
  }
@@ -1332,23 +1358,21 @@ async function uploadAssetIfNeeded(assetStore, file, kind) {
1332
1358
  async function ingestAssetFilesToSceneItems(options) {
1333
1359
  const {
1334
1360
  files,
1361
+ existingItems = [],
1335
1362
  worldCenter,
1336
1363
  assetStore,
1337
1364
  imageStore = new IndexedDbImageStore(),
1338
1365
  createId = createShapeId,
1339
1366
  gapWorld = 16,
1340
- stepWorld = 48,
1341
- pdfScale = 1.5,
1367
+ pdfScale = 1.15,
1342
1368
  pdfPageConcurrency = 2,
1343
1369
  decorateItem,
1370
+ onItemsReady,
1344
1371
  onError
1345
1372
  } = options;
1346
1373
  const items = [];
1347
1374
  const errors = [];
1348
- let imagePlacementIndex = 0;
1349
- let occupiedBottomY = null;
1350
- let imageYOffsetAdjustment = 0;
1351
- let hasImagePlacementBase = false;
1375
+ let occupiedBottomY = existingItems.length > 0 ? Math.max(...existingItems.map((item) => item.bounds.y + item.bounds.height)) : null;
1352
1376
  for (const file of files) {
1353
1377
  const kind = getAssetKindForFile(file);
1354
1378
  if (!kind) {
@@ -1362,58 +1386,56 @@ async function ingestAssetFilesToSceneItems(options) {
1362
1386
  }
1363
1387
  try {
1364
1388
  if (kind === "pdf") {
1365
- const [uploadResult2, pages] = await Promise.all([
1366
- uploadAssetIfNeeded(assetStore, file, kind),
1367
- loadPdfToStore(file, imageStore, {
1368
- scale: pdfScale,
1369
- pageConcurrency: pdfPageConcurrency
1370
- })
1371
- ]);
1372
- for (const page of pages) {
1373
- const fullUrl2 = await createBlobUrlFromStore(imageStore, page.blobId);
1374
- const naturalTopY2 = worldCenter.y - page.height / 2;
1375
- const stackedTopY = occupiedBottomY == null ? naturalTopY2 : occupiedBottomY + gapWorld;
1376
- const bounds2 = {
1377
- x: worldCenter.x - page.width / 2,
1378
- y: Math.max(naturalTopY2, stackedTopY),
1379
- width: page.width,
1380
- height: page.height
1381
- };
1382
- const itemContext2 = {
1383
- file,
1384
- kind,
1385
- itemIndex: items.length,
1386
- pageNumber: page.pageNumber
1387
- };
1388
- const item2 = finalizeIngestedItem(
1389
- {
1390
- id: createId(),
1391
- x: bounds2.x,
1392
- y: bounds2.y,
1393
- bounds: { ...bounds2 },
1394
- toolKind: "image",
1395
- imageBlobId: page.blobId,
1396
- imageRasterHref: fullUrl2 ?? void 0,
1397
- imageIntrinsicSize: {
1398
- width: page.width,
1399
- height: page.height
1389
+ const uploadResultPromise = uploadAssetIfNeeded(assetStore, file, kind);
1390
+ await loadPdfToStore(file, imageStore, {
1391
+ scale: pdfScale,
1392
+ pageConcurrency: pdfPageConcurrency,
1393
+ storeThumbnails: false,
1394
+ onPageStored: async (page) => {
1395
+ const uploadResult2 = await uploadResultPromise;
1396
+ const fullUrl2 = await createBlobUrlFromStore(imageStore, page.blobId);
1397
+ const naturalTopY2 = worldCenter.y - page.height / 2;
1398
+ const stackedTopY2 = occupiedBottomY == null ? naturalTopY2 : occupiedBottomY + gapWorld;
1399
+ const bounds2 = {
1400
+ x: worldCenter.x - page.width / 2,
1401
+ y: Math.max(naturalTopY2, stackedTopY2),
1402
+ width: page.width,
1403
+ height: page.height
1404
+ };
1405
+ const itemContext2 = {
1406
+ file,
1407
+ kind,
1408
+ itemIndex: items.length,
1409
+ pageNumber: page.pageNumber
1410
+ };
1411
+ const item2 = finalizeIngestedItem(
1412
+ {
1413
+ id: createId(),
1414
+ x: bounds2.x,
1415
+ y: bounds2.y,
1416
+ bounds: { ...bounds2 },
1417
+ toolKind: "image",
1418
+ imageBlobId: page.blobId,
1419
+ imageRasterHref: fullUrl2 ?? void 0,
1420
+ imageIntrinsicSize: {
1421
+ width: page.width,
1422
+ height: page.height
1423
+ },
1424
+ childrenSvg: fullUrl2 ? buildRasterImageChildrenSvg(
1425
+ fullUrl2,
1426
+ { width: page.width, height: page.height },
1427
+ bounds2
1428
+ ) : ""
1400
1429
  },
1401
- childrenSvg: fullUrl2 ? buildRasterImageChildrenSvg(
1402
- fullUrl2,
1403
- { width: page.width, height: page.height },
1404
- bounds2
1405
- ) : ""
1406
- },
1407
- itemContext2,
1408
- uploadResult2,
1409
- decorateItem
1410
- );
1411
- items.push(item2);
1412
- occupiedBottomY = bounds2.y + page.height;
1413
- }
1414
- hasImagePlacementBase = false;
1415
- imagePlacementIndex = 0;
1416
- imageYOffsetAdjustment = 0;
1430
+ itemContext2,
1431
+ uploadResult2,
1432
+ decorateItem
1433
+ );
1434
+ items.push(item2);
1435
+ occupiedBottomY = bounds2.y + page.height;
1436
+ onItemsReady?.([item2], { file, kind });
1437
+ }
1438
+ });
1417
1439
  continue;
1418
1440
  }
1419
1441
  const [uploadResult, storedImage] = await Promise.all([
@@ -1424,17 +1446,11 @@ async function ingestAssetFilesToSceneItems(options) {
1424
1446
  const fullUrl = await createBlobUrlFromStore(imageStore, blobId);
1425
1447
  const thumbBlob = await imageStore.getThumbnail(thumbnailBlobId);
1426
1448
  const thumbnailHref = thumbBlob ? URL.createObjectURL(thumbBlob) : null;
1427
- const ox = imagePlacementIndex % 8 * stepWorld;
1428
- const oy = Math.floor(imagePlacementIndex / 8) * stepWorld;
1429
- const naturalTopY = worldCenter.y - height / 2 + oy;
1430
- if (!hasImagePlacementBase) {
1431
- const minimumTopY = occupiedBottomY == null ? naturalTopY : occupiedBottomY + gapWorld;
1432
- imageYOffsetAdjustment = Math.max(0, minimumTopY - naturalTopY);
1433
- hasImagePlacementBase = true;
1434
- }
1449
+ const naturalTopY = worldCenter.y - height / 2;
1450
+ const stackedTopY = occupiedBottomY == null ? naturalTopY : occupiedBottomY + gapWorld;
1435
1451
  const bounds = {
1436
- x: worldCenter.x - width / 2 + ox,
1437
- y: naturalTopY + imageYOffsetAdjustment,
1452
+ x: worldCenter.x - width / 2,
1453
+ y: Math.max(naturalTopY, stackedTopY),
1438
1454
  width,
1439
1455
  height
1440
1456
  };
@@ -1463,7 +1479,7 @@ async function ingestAssetFilesToSceneItems(options) {
1463
1479
  decorateItem
1464
1480
  );
1465
1481
  items.push(item);
1466
- imagePlacementIndex++;
1482
+ onItemsReady?.([item], { file, kind });
1467
1483
  occupiedBottomY = occupiedBottomY == null ? bounds.y + height : Math.max(occupiedBottomY, bounds.y + height);
1468
1484
  } catch (error) {
1469
1485
  const fileError = {
@@ -5974,7 +5990,7 @@ body[data-canvu-pen-active="true"] [data-slot="shape-context-menu"] * {
5974
5990
  }
5975
5991
  `;
5976
5992
  function debugApplePencilPointer(phase, detail) {
5977
- console.debug(`[canvu][apple-pencil] ${phase}`, detail);
5993
+ return;
5978
5994
  }
5979
5995
  var MARKER_TOOL_STYLE = {
5980
5996
  stroke: "#fde047",
@@ -6574,7 +6590,6 @@ var VectorViewport = forwardRef(
6574
6590
  const captureInteractionPointer = useCallback(
6575
6591
  (target, pointerId, options) => {
6576
6592
  const captureNativePointer = options?.captureNativePointer ?? true;
6577
- debugApplePencilPointer("capture", { pointerId, captureNativePointer });
6578
6593
  activeInteractionPointerIdRef.current = pointerId;
6579
6594
  activeInteractionPointerTargetRef.current = captureNativePointer ? target : null;
6580
6595
  if (captureNativePointer) {
@@ -6586,7 +6601,6 @@ var VectorViewport = forwardRef(
6586
6601
  const releaseInteractionPointer = useCallback(() => {
6587
6602
  const pointerId = activeInteractionPointerIdRef.current;
6588
6603
  const target = activeInteractionPointerTargetRef.current;
6589
- debugApplePencilPointer("release", { pointerId });
6590
6604
  if (pointerId != null && target) {
6591
6605
  try {
6592
6606
  if (target.hasPointerCapture(pointerId)) {
@@ -6619,8 +6633,6 @@ var VectorViewport = forwardRef(
6619
6633
  const itemId = strokeState.itemId;
6620
6634
  const style = { ...strokeStyleRef.current };
6621
6635
  debugApplePencilPointer("finalize-stroke", {
6622
- pointerType,
6623
- tool,
6624
6636
  points: pts.length
6625
6637
  });
6626
6638
  dragStateRef.current = { kind: "idle" };
@@ -7370,12 +7382,19 @@ var VectorViewport = forwardRef(
7370
7382
  }
7371
7383
  const result = await ingestAssetFilesToSceneItems({
7372
7384
  files,
7385
+ existingItems: itemsRef.current,
7373
7386
  worldCenter: {
7374
7387
  x: worldX,
7375
7388
  y: worldY
7376
7389
  },
7377
7390
  imageStore: store,
7378
- assetStore: assetStoreRef.current ?? void 0
7391
+ assetStore: assetStoreRef.current ?? void 0,
7392
+ onItemsReady: (nextItems) => {
7393
+ if (nextItems.length === 0) return;
7394
+ setLoadingSkeletons([]);
7395
+ change([...itemsRef.current, ...nextItems]);
7396
+ setEffectiveSelectedIdsRef.current(nextItems.map((item) => item.id));
7397
+ }
7379
7398
  });
7380
7399
  if (result.errors.length > 0) {
7381
7400
  for (const error of result.errors) {
@@ -7383,8 +7402,6 @@ var VectorViewport = forwardRef(
7383
7402
  }
7384
7403
  }
7385
7404
  if (result.items.length === 0) return;
7386
- change([...itemsRef.current, ...result.items]);
7387
- setEffectiveSelectedIdsRef.current(result.items.map((item) => item.id));
7388
7405
  } finally {
7389
7406
  setLoadingSkeletons([]);
7390
7407
  }
@@ -7393,12 +7410,12 @@ var VectorViewport = forwardRef(
7393
7410
  );
7394
7411
  const handleImageFileChange = useCallback(
7395
7412
  async (e) => {
7396
- const file = e.target.files?.[0];
7413
+ const files = e.target.files ? Array.from(e.target.files) : [];
7397
7414
  e.target.value = "";
7398
7415
  const pending = pendingImagePlacementRef.current;
7399
7416
  pendingImagePlacementRef.current = null;
7400
- if (!file || !pending) return;
7401
- await placeImageFilesAtWorld([file], pending.worldX, pending.worldY);
7417
+ if (files.length === 0 || !pending) return;
7418
+ await placeImageFilesAtWorld(files, pending.worldX, pending.worldY);
7402
7419
  },
7403
7420
  [placeImageFilesAtWorld]
7404
7421
  );
@@ -7494,14 +7511,12 @@ var VectorViewport = forwardRef(
7494
7511
  pointerType: e.pointerType,
7495
7512
  pointerId: e.pointerId,
7496
7513
  pressure: e.pointerType === "pen" ? e.pressure : void 0,
7497
- activePointerId,
7498
7514
  dragKind: currentDragState.kind
7499
7515
  });
7500
7516
  }
7501
7517
  if (e.pointerType === "pen" && currentDragState.kind === "stroke" && currentDragState.pointerType === "pen") {
7502
7518
  debugApplePencilPointer("pen-reentry", {
7503
7519
  pointerId: e.pointerId,
7504
- previousPointerId: activePointerId,
7505
7520
  previousPoints: currentDragState.points.length
7506
7521
  });
7507
7522
  finalizeStrokeDragState(currentDragState);
@@ -7847,9 +7862,7 @@ var VectorViewport = forwardRef(
7847
7862
  debugApplePencilPointer("native-pointerdown", {
7848
7863
  pointerType: e.pointerType,
7849
7864
  pointerId: e.pointerId,
7850
- pressure: e.pressure,
7851
- tool
7852
- });
7865
+ pressure: e.pressure});
7853
7866
  e.preventDefault();
7854
7867
  e.stopImmediatePropagation();
7855
7868
  };
@@ -7949,9 +7962,7 @@ var VectorViewport = forwardRef(
7949
7962
  debugApplePencilPointer("touchstart-stroke", {
7950
7963
  touchId: touch.identifier,
7951
7964
  touchType: touchKind(touch),
7952
- force: touchPressure(touch),
7953
- tool
7954
- });
7965
+ force: touchPressure(touch)});
7955
7966
  stopTouchEvent(ev);
7956
7967
  };
7957
7968
  const onTouchMove = (ev) => {
@@ -8673,6 +8684,7 @@ var VectorViewport = forwardRef(
8673
8684
  {
8674
8685
  ref: imageInputRef,
8675
8686
  type: "file",
8687
+ multiple: true,
8676
8688
  accept: "image/*,application/pdf",
8677
8689
  style: { display: "none" },
8678
8690
  "aria-label": "Select image or PDF to place on canvas",