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.cjs CHANGED
@@ -956,10 +956,32 @@ async function runWithConcurrency(items, concurrency, execute) {
956
956
  async function loadPdfToStore(file, store, options) {
957
957
  const scale = options?.scale ?? 1.5;
958
958
  const pageConcurrency = options?.pageConcurrency ?? 2;
959
+ const storeThumbnails = options?.storeThumbnails ?? false;
959
960
  const pdfjs = await getPdfJs();
960
961
  const arrayBuffer = await file.arrayBuffer();
961
962
  const pdf = await pdfjs.getDocument({ data: arrayBuffer }).promise;
962
963
  const pageNumbers = normalizePdfPageNumbers(options?.pageNumbers, pdf.numPages);
964
+ const bufferedResults = /* @__PURE__ */ new Map();
965
+ let nextEmitIndex = 0;
966
+ let emitChain = Promise.resolve();
967
+ const queuePageEmission = async (pageResult) => {
968
+ bufferedResults.set(pageResult.pageNumber, pageResult);
969
+ const run = async () => {
970
+ while (nextEmitIndex < pageNumbers.length) {
971
+ const nextPageNumber = pageNumbers[nextEmitIndex];
972
+ if (nextPageNumber == null) break;
973
+ const bufferedResult = bufferedResults.get(nextPageNumber);
974
+ if (!bufferedResult) break;
975
+ bufferedResults.delete(nextPageNumber);
976
+ nextEmitIndex += 1;
977
+ await options?.onPageStored?.(bufferedResult);
978
+ }
979
+ };
980
+ const nextChain = emitChain.then(run, run);
981
+ emitChain = nextChain.catch(() => {
982
+ });
983
+ await nextChain;
984
+ };
963
985
  return await runWithConcurrency(
964
986
  pageNumbers,
965
987
  pageConcurrency,
@@ -969,27 +991,31 @@ async function loadPdfToStore(file, store, options) {
969
991
  const mime = "image/png";
970
992
  const pageBlob = await canvasToBlob2(canvas, mime);
971
993
  const blobId = await store.storeOriginal(pageBlob);
972
- const thumbScale = Math.min(1, 256 / Math.max(width, height));
973
- const tw = Math.max(1, Math.round(width * thumbScale));
974
- const th = Math.max(1, Math.round(height * thumbScale));
975
- const thumbCanvas = document.createElement("canvas");
976
- thumbCanvas.width = tw;
977
- thumbCanvas.height = th;
978
- const tCtx = thumbCanvas.getContext("2d");
979
- if (tCtx) {
980
- tCtx.imageSmoothingEnabled = true;
981
- tCtx.imageSmoothingQuality = "high";
982
- tCtx.drawImage(canvas, 0, 0, tw, th);
983
- }
984
- const thumbBlob = await canvasToBlob2(thumbCanvas, mime);
985
- const thumbnailBlobId = await store.storeThumbnail(thumbBlob);
986
- return {
994
+ const thumbnailBlobId = storeThumbnails ? await (async () => {
995
+ const thumbScale = Math.min(1, 256 / Math.max(width, height));
996
+ const tw = Math.max(1, Math.round(width * thumbScale));
997
+ const th = Math.max(1, Math.round(height * thumbScale));
998
+ const thumbCanvas = document.createElement("canvas");
999
+ thumbCanvas.width = tw;
1000
+ thumbCanvas.height = th;
1001
+ const tCtx = thumbCanvas.getContext("2d");
1002
+ if (tCtx) {
1003
+ tCtx.imageSmoothingEnabled = true;
1004
+ tCtx.imageSmoothingQuality = "high";
1005
+ tCtx.drawImage(canvas, 0, 0, tw, th);
1006
+ }
1007
+ const thumbBlob = await canvasToBlob2(thumbCanvas, mime);
1008
+ return await store.storeThumbnail(thumbBlob);
1009
+ })() : "";
1010
+ const pageResult = {
987
1011
  blobId,
988
1012
  thumbnailBlobId,
989
1013
  width,
990
1014
  height,
991
1015
  pageNumber
992
1016
  };
1017
+ await queuePageEmission(pageResult);
1018
+ return pageResult;
993
1019
  }
994
1020
  );
995
1021
  }
@@ -1339,23 +1365,21 @@ async function uploadAssetIfNeeded(assetStore, file, kind) {
1339
1365
  async function ingestAssetFilesToSceneItems(options) {
1340
1366
  const {
1341
1367
  files,
1368
+ existingItems = [],
1342
1369
  worldCenter,
1343
1370
  assetStore,
1344
1371
  imageStore = new IndexedDbImageStore(),
1345
1372
  createId = createShapeId,
1346
1373
  gapWorld = 16,
1347
- stepWorld = 48,
1348
- pdfScale = 1.5,
1374
+ pdfScale = 1.15,
1349
1375
  pdfPageConcurrency = 2,
1350
1376
  decorateItem,
1377
+ onItemsReady,
1351
1378
  onError
1352
1379
  } = options;
1353
1380
  const items = [];
1354
1381
  const errors = [];
1355
- let imagePlacementIndex = 0;
1356
- let occupiedBottomY = null;
1357
- let imageYOffsetAdjustment = 0;
1358
- let hasImagePlacementBase = false;
1382
+ let occupiedBottomY = existingItems.length > 0 ? Math.max(...existingItems.map((item) => item.bounds.y + item.bounds.height)) : null;
1359
1383
  for (const file of files) {
1360
1384
  const kind = getAssetKindForFile(file);
1361
1385
  if (!kind) {
@@ -1369,58 +1393,56 @@ async function ingestAssetFilesToSceneItems(options) {
1369
1393
  }
1370
1394
  try {
1371
1395
  if (kind === "pdf") {
1372
- const [uploadResult2, pages] = await Promise.all([
1373
- uploadAssetIfNeeded(assetStore, file, kind),
1374
- loadPdfToStore(file, imageStore, {
1375
- scale: pdfScale,
1376
- pageConcurrency: pdfPageConcurrency
1377
- })
1378
- ]);
1379
- for (const page of pages) {
1380
- const fullUrl2 = await createBlobUrlFromStore(imageStore, page.blobId);
1381
- const naturalTopY2 = worldCenter.y - page.height / 2;
1382
- const stackedTopY = occupiedBottomY == null ? naturalTopY2 : occupiedBottomY + gapWorld;
1383
- const bounds2 = {
1384
- x: worldCenter.x - page.width / 2,
1385
- y: Math.max(naturalTopY2, stackedTopY),
1386
- width: page.width,
1387
- height: page.height
1388
- };
1389
- const itemContext2 = {
1390
- file,
1391
- kind,
1392
- itemIndex: items.length,
1393
- pageNumber: page.pageNumber
1394
- };
1395
- const item2 = finalizeIngestedItem(
1396
- {
1397
- id: createId(),
1398
- x: bounds2.x,
1399
- y: bounds2.y,
1400
- bounds: { ...bounds2 },
1401
- toolKind: "image",
1402
- imageBlobId: page.blobId,
1403
- imageRasterHref: fullUrl2 ?? void 0,
1404
- imageIntrinsicSize: {
1405
- width: page.width,
1406
- height: page.height
1396
+ const uploadResultPromise = uploadAssetIfNeeded(assetStore, file, kind);
1397
+ await loadPdfToStore(file, imageStore, {
1398
+ scale: pdfScale,
1399
+ pageConcurrency: pdfPageConcurrency,
1400
+ storeThumbnails: false,
1401
+ onPageStored: async (page) => {
1402
+ const uploadResult2 = await uploadResultPromise;
1403
+ const fullUrl2 = await createBlobUrlFromStore(imageStore, page.blobId);
1404
+ const naturalTopY2 = worldCenter.y - page.height / 2;
1405
+ const stackedTopY2 = occupiedBottomY == null ? naturalTopY2 : occupiedBottomY + gapWorld;
1406
+ const bounds2 = {
1407
+ x: worldCenter.x - page.width / 2,
1408
+ y: Math.max(naturalTopY2, stackedTopY2),
1409
+ width: page.width,
1410
+ height: page.height
1411
+ };
1412
+ const itemContext2 = {
1413
+ file,
1414
+ kind,
1415
+ itemIndex: items.length,
1416
+ pageNumber: page.pageNumber
1417
+ };
1418
+ const item2 = finalizeIngestedItem(
1419
+ {
1420
+ id: createId(),
1421
+ x: bounds2.x,
1422
+ y: bounds2.y,
1423
+ bounds: { ...bounds2 },
1424
+ toolKind: "image",
1425
+ imageBlobId: page.blobId,
1426
+ imageRasterHref: fullUrl2 ?? void 0,
1427
+ imageIntrinsicSize: {
1428
+ width: page.width,
1429
+ height: page.height
1430
+ },
1431
+ childrenSvg: fullUrl2 ? buildRasterImageChildrenSvg(
1432
+ fullUrl2,
1433
+ { width: page.width, height: page.height },
1434
+ bounds2
1435
+ ) : ""
1407
1436
  },
1408
- childrenSvg: fullUrl2 ? buildRasterImageChildrenSvg(
1409
- fullUrl2,
1410
- { width: page.width, height: page.height },
1411
- bounds2
1412
- ) : ""
1413
- },
1414
- itemContext2,
1415
- uploadResult2,
1416
- decorateItem
1417
- );
1418
- items.push(item2);
1419
- occupiedBottomY = bounds2.y + page.height;
1420
- }
1421
- hasImagePlacementBase = false;
1422
- imagePlacementIndex = 0;
1423
- imageYOffsetAdjustment = 0;
1437
+ itemContext2,
1438
+ uploadResult2,
1439
+ decorateItem
1440
+ );
1441
+ items.push(item2);
1442
+ occupiedBottomY = bounds2.y + page.height;
1443
+ onItemsReady?.([item2], { file, kind });
1444
+ }
1445
+ });
1424
1446
  continue;
1425
1447
  }
1426
1448
  const [uploadResult, storedImage] = await Promise.all([
@@ -1431,17 +1453,11 @@ async function ingestAssetFilesToSceneItems(options) {
1431
1453
  const fullUrl = await createBlobUrlFromStore(imageStore, blobId);
1432
1454
  const thumbBlob = await imageStore.getThumbnail(thumbnailBlobId);
1433
1455
  const thumbnailHref = thumbBlob ? URL.createObjectURL(thumbBlob) : null;
1434
- const ox = imagePlacementIndex % 8 * stepWorld;
1435
- const oy = Math.floor(imagePlacementIndex / 8) * stepWorld;
1436
- const naturalTopY = worldCenter.y - height / 2 + oy;
1437
- if (!hasImagePlacementBase) {
1438
- const minimumTopY = occupiedBottomY == null ? naturalTopY : occupiedBottomY + gapWorld;
1439
- imageYOffsetAdjustment = Math.max(0, minimumTopY - naturalTopY);
1440
- hasImagePlacementBase = true;
1441
- }
1456
+ const naturalTopY = worldCenter.y - height / 2;
1457
+ const stackedTopY = occupiedBottomY == null ? naturalTopY : occupiedBottomY + gapWorld;
1442
1458
  const bounds = {
1443
- x: worldCenter.x - width / 2 + ox,
1444
- y: naturalTopY + imageYOffsetAdjustment,
1459
+ x: worldCenter.x - width / 2,
1460
+ y: Math.max(naturalTopY, stackedTopY),
1445
1461
  width,
1446
1462
  height
1447
1463
  };
@@ -1470,7 +1486,7 @@ async function ingestAssetFilesToSceneItems(options) {
1470
1486
  decorateItem
1471
1487
  );
1472
1488
  items.push(item);
1473
- imagePlacementIndex++;
1489
+ onItemsReady?.([item], { file, kind });
1474
1490
  occupiedBottomY = occupiedBottomY == null ? bounds.y + height : Math.max(occupiedBottomY, bounds.y + height);
1475
1491
  } catch (error) {
1476
1492
  const fileError = {
@@ -5981,7 +5997,7 @@ body[data-canvu-pen-active="true"] [data-slot="shape-context-menu"] * {
5981
5997
  }
5982
5998
  `;
5983
5999
  function debugApplePencilPointer(phase, detail) {
5984
- console.debug(`[canvu][apple-pencil] ${phase}`, detail);
6000
+ return;
5985
6001
  }
5986
6002
  var MARKER_TOOL_STYLE = {
5987
6003
  stroke: "#fde047",
@@ -6581,7 +6597,6 @@ var VectorViewport = react.forwardRef(
6581
6597
  const captureInteractionPointer = react.useCallback(
6582
6598
  (target, pointerId, options) => {
6583
6599
  const captureNativePointer = options?.captureNativePointer ?? true;
6584
- debugApplePencilPointer("capture", { pointerId, captureNativePointer });
6585
6600
  activeInteractionPointerIdRef.current = pointerId;
6586
6601
  activeInteractionPointerTargetRef.current = captureNativePointer ? target : null;
6587
6602
  if (captureNativePointer) {
@@ -6593,7 +6608,6 @@ var VectorViewport = react.forwardRef(
6593
6608
  const releaseInteractionPointer = react.useCallback(() => {
6594
6609
  const pointerId = activeInteractionPointerIdRef.current;
6595
6610
  const target = activeInteractionPointerTargetRef.current;
6596
- debugApplePencilPointer("release", { pointerId });
6597
6611
  if (pointerId != null && target) {
6598
6612
  try {
6599
6613
  if (target.hasPointerCapture(pointerId)) {
@@ -6626,8 +6640,6 @@ var VectorViewport = react.forwardRef(
6626
6640
  const itemId = strokeState.itemId;
6627
6641
  const style = { ...strokeStyleRef.current };
6628
6642
  debugApplePencilPointer("finalize-stroke", {
6629
- pointerType,
6630
- tool,
6631
6643
  points: pts.length
6632
6644
  });
6633
6645
  dragStateRef.current = { kind: "idle" };
@@ -7377,12 +7389,19 @@ var VectorViewport = react.forwardRef(
7377
7389
  }
7378
7390
  const result = await ingestAssetFilesToSceneItems({
7379
7391
  files,
7392
+ existingItems: itemsRef.current,
7380
7393
  worldCenter: {
7381
7394
  x: worldX,
7382
7395
  y: worldY
7383
7396
  },
7384
7397
  imageStore: store,
7385
- assetStore: assetStoreRef.current ?? void 0
7398
+ assetStore: assetStoreRef.current ?? void 0,
7399
+ onItemsReady: (nextItems) => {
7400
+ if (nextItems.length === 0) return;
7401
+ setLoadingSkeletons([]);
7402
+ change([...itemsRef.current, ...nextItems]);
7403
+ setEffectiveSelectedIdsRef.current(nextItems.map((item) => item.id));
7404
+ }
7386
7405
  });
7387
7406
  if (result.errors.length > 0) {
7388
7407
  for (const error of result.errors) {
@@ -7390,8 +7409,6 @@ var VectorViewport = react.forwardRef(
7390
7409
  }
7391
7410
  }
7392
7411
  if (result.items.length === 0) return;
7393
- change([...itemsRef.current, ...result.items]);
7394
- setEffectiveSelectedIdsRef.current(result.items.map((item) => item.id));
7395
7412
  } finally {
7396
7413
  setLoadingSkeletons([]);
7397
7414
  }
@@ -7400,12 +7417,12 @@ var VectorViewport = react.forwardRef(
7400
7417
  );
7401
7418
  const handleImageFileChange = react.useCallback(
7402
7419
  async (e) => {
7403
- const file = e.target.files?.[0];
7420
+ const files = e.target.files ? Array.from(e.target.files) : [];
7404
7421
  e.target.value = "";
7405
7422
  const pending = pendingImagePlacementRef.current;
7406
7423
  pendingImagePlacementRef.current = null;
7407
- if (!file || !pending) return;
7408
- await placeImageFilesAtWorld([file], pending.worldX, pending.worldY);
7424
+ if (files.length === 0 || !pending) return;
7425
+ await placeImageFilesAtWorld(files, pending.worldX, pending.worldY);
7409
7426
  },
7410
7427
  [placeImageFilesAtWorld]
7411
7428
  );
@@ -7501,14 +7518,12 @@ var VectorViewport = react.forwardRef(
7501
7518
  pointerType: e.pointerType,
7502
7519
  pointerId: e.pointerId,
7503
7520
  pressure: e.pointerType === "pen" ? e.pressure : void 0,
7504
- activePointerId,
7505
7521
  dragKind: currentDragState.kind
7506
7522
  });
7507
7523
  }
7508
7524
  if (e.pointerType === "pen" && currentDragState.kind === "stroke" && currentDragState.pointerType === "pen") {
7509
7525
  debugApplePencilPointer("pen-reentry", {
7510
7526
  pointerId: e.pointerId,
7511
- previousPointerId: activePointerId,
7512
7527
  previousPoints: currentDragState.points.length
7513
7528
  });
7514
7529
  finalizeStrokeDragState(currentDragState);
@@ -7854,9 +7869,7 @@ var VectorViewport = react.forwardRef(
7854
7869
  debugApplePencilPointer("native-pointerdown", {
7855
7870
  pointerType: e.pointerType,
7856
7871
  pointerId: e.pointerId,
7857
- pressure: e.pressure,
7858
- tool
7859
- });
7872
+ pressure: e.pressure});
7860
7873
  e.preventDefault();
7861
7874
  e.stopImmediatePropagation();
7862
7875
  };
@@ -7956,9 +7969,7 @@ var VectorViewport = react.forwardRef(
7956
7969
  debugApplePencilPointer("touchstart-stroke", {
7957
7970
  touchId: touch.identifier,
7958
7971
  touchType: touchKind(touch),
7959
- force: touchPressure(touch),
7960
- tool
7961
- });
7972
+ force: touchPressure(touch)});
7962
7973
  stopTouchEvent(ev);
7963
7974
  };
7964
7975
  const onTouchMove = (ev) => {
@@ -8680,6 +8691,7 @@ var VectorViewport = react.forwardRef(
8680
8691
  {
8681
8692
  ref: imageInputRef,
8682
8693
  type: "file",
8694
+ multiple: true,
8683
8695
  accept: "image/*,application/pdf",
8684
8696
  style: { display: "none" },
8685
8697
  "aria-label": "Select image or PDF to place on canvas",