canvu-react 0.4.64 → 0.4.65

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
@@ -5653,7 +5653,7 @@ function attachViewportInput(options) {
5653
5653
  if (touchMomentum) {
5654
5654
  touchMomentum.cancel();
5655
5655
  }
5656
- const panOk = allowPrimaryPointerPan();
5656
+ const panOk = allowPrimaryPointerPan(e);
5657
5657
  if (e.pointerType === "mouse" && e.button === 0) {
5658
5658
  if (!panOk) {
5659
5659
  return;
@@ -7677,6 +7677,93 @@ function PresenceRemoteLayer({
7677
7677
  );
7678
7678
  }
7679
7679
 
7680
+ // src/react/read-only-activation.ts
7681
+ function findReadOnlyItemClickPlacement(item, placements) {
7682
+ const toolId = item.customToolId;
7683
+ if (!toolId) return null;
7684
+ return [...placements].reverse().find(
7685
+ (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
7686
+ ) ?? null;
7687
+ }
7688
+ function resolveReadOnlyActivationTarget(input) {
7689
+ const {
7690
+ pointer,
7691
+ camera,
7692
+ container,
7693
+ items,
7694
+ detailItems = items,
7695
+ placements,
7696
+ scope,
7697
+ selectedIds
7698
+ } = input;
7699
+ const rect = container.getBoundingClientRect();
7700
+ const world = camera.screenToWorld(
7701
+ pointer.clientX - rect.left,
7702
+ pointer.clientY - rect.top
7703
+ );
7704
+ const hit = hitTestWorldPoint(items, world.worldX, world.worldY, {
7705
+ lineHitWorld: 10 / camera.zoom,
7706
+ ignoreLocked: true
7707
+ });
7708
+ if (!hit) return null;
7709
+ const customPlacement = findReadOnlyItemClickPlacement(hit, placements);
7710
+ if (customPlacement?.onSelectModeItemClick) {
7711
+ return {
7712
+ item: hit,
7713
+ activation: "custom",
7714
+ worldX: world.worldX,
7715
+ worldY: world.worldY
7716
+ };
7717
+ }
7718
+ if (scope === "all") {
7719
+ return {
7720
+ item: hit,
7721
+ activation: "read-only",
7722
+ worldX: world.worldX,
7723
+ worldY: world.worldY
7724
+ };
7725
+ }
7726
+ if (typeof scope === "function") {
7727
+ const allowed = scope({
7728
+ item: hit,
7729
+ worldX: world.worldX,
7730
+ worldY: world.worldY,
7731
+ clientX: pointer.clientX,
7732
+ clientY: pointer.clientY,
7733
+ pointerType: pointer.pointerType,
7734
+ shiftKey: pointer.shiftKey,
7735
+ altKey: pointer.altKey,
7736
+ metaKey: pointer.metaKey,
7737
+ ctrlKey: pointer.ctrlKey,
7738
+ items: detailItems,
7739
+ selectedIds
7740
+ });
7741
+ if (allowed) {
7742
+ return {
7743
+ item: hit,
7744
+ activation: "read-only",
7745
+ worldX: world.worldX,
7746
+ worldY: world.worldY
7747
+ };
7748
+ }
7749
+ }
7750
+ return null;
7751
+ }
7752
+ function createReadOnlyActivationSession(target, pointer) {
7753
+ return {
7754
+ pointerId: pointer.pointerId,
7755
+ itemId: target.item.id,
7756
+ activation: target.activation,
7757
+ startWorld: { x: target.worldX, y: target.worldY },
7758
+ startScreen: { x: pointer.clientX, y: pointer.clientY }
7759
+ };
7760
+ }
7761
+ function didReadOnlyActivationMovePastTap(session, pointer, tapPx) {
7762
+ const dx = pointer.clientX - session.startScreen.x;
7763
+ const dy = pointer.clientY - session.startScreen.y;
7764
+ return Math.hypot(dx, dy) > tapPx;
7765
+ }
7766
+
7680
7767
  // src/react/stable-selection.ts
7681
7768
  function shallowEqualStringArray(a, b) {
7682
7769
  if (a === b) return true;
@@ -8030,13 +8117,6 @@ function isDefaultMarkerToolStyle(style) {
8030
8117
  function tagCustomPlacementItem(item, toolId) {
8031
8118
  return item.customToolId === toolId ? item : { ...item, customToolId: toolId };
8032
8119
  }
8033
- function findSelectModeItemClickPlacement(item, placements) {
8034
- const toolId = item.customToolId;
8035
- if (!toolId) return null;
8036
- return [...placements].reverse().find(
8037
- (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
8038
- ) ?? null;
8039
- }
8040
8120
  function mergeToolListById(baseTools, pluginTools) {
8041
8121
  const next = [...baseTools];
8042
8122
  for (const tool of pluginTools) {
@@ -8164,6 +8244,7 @@ var VectorViewport = react.forwardRef(
8164
8244
  toolId = "hand",
8165
8245
  applePencilNav = false,
8166
8246
  interactive = false,
8247
+ readOnlyInteraction,
8167
8248
  selectedIds: selectedIdsProp,
8168
8249
  onSelectionChange,
8169
8250
  onItemsChange: consumerOnItemsChange,
@@ -8366,6 +8447,8 @@ var VectorViewport = react.forwardRef(
8366
8447
  );
8367
8448
  const toolIdRef = react.useRef(toolId);
8368
8449
  const interactiveRef = react.useRef(interactive);
8450
+ const readOnlyInteractionRef = react.useRef(readOnlyInteraction);
8451
+ readOnlyInteractionRef.current = readOnlyInteraction;
8369
8452
  const reducedMotionRef = react.useRef(false);
8370
8453
  const itemsRef = react.useRef(items);
8371
8454
  const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
@@ -8379,6 +8462,7 @@ var VectorViewport = react.forwardRef(
8379
8462
  const allCustomPlacementsRef = react.useRef(allCustomPlacements);
8380
8463
  allCustomPlacementsRef.current = allCustomPlacements;
8381
8464
  const dragStateRef = react.useRef({ kind: "idle" });
8465
+ const readOnlyItemClickStateRef = react.useRef(null);
8382
8466
  const clipboardRef = react.useRef(null);
8383
8467
  const undoStackRef = react.useRef([]);
8384
8468
  const redoStackRef = react.useRef([]);
@@ -8556,6 +8640,31 @@ var VectorViewport = react.forwardRef(
8556
8640
  );
8557
8641
  const resolvedItemsRef = react.useRef(resolvedItems);
8558
8642
  resolvedItemsRef.current = resolvedItems;
8643
+ const readOnlyActivationResolutionCacheRef = react.useRef(/* @__PURE__ */ new WeakMap());
8644
+ const resolveReadOnlyActivation = react.useCallback(
8645
+ (pointer) => {
8646
+ const cache = readOnlyActivationResolutionCacheRef.current;
8647
+ if (cache.has(pointer)) return cache.get(pointer) ?? null;
8648
+ let target = null;
8649
+ const cam = cameraRef.current;
8650
+ const container = sceneContainerRef.current;
8651
+ if (!interactiveRef.current && toolIdRef.current === "select" && cam && container) {
8652
+ target = resolveReadOnlyActivationTarget({
8653
+ pointer,
8654
+ camera: cam,
8655
+ container,
8656
+ items: resolvedItemsRef.current,
8657
+ detailItems: itemsRef.current,
8658
+ placements: allCustomPlacementsRef.current,
8659
+ scope: readOnlyInteractionRef.current?.itemClicks ?? "custom",
8660
+ selectedIds: effectiveSelectedIdsRef.current
8661
+ });
8662
+ }
8663
+ cache.set(pointer, target);
8664
+ return target;
8665
+ },
8666
+ []
8667
+ );
8559
8668
  const liveId = react.useId();
8560
8669
  const reducedMotion = usePrefersReducedMotion();
8561
8670
  reducedMotionRef.current = reducedMotion;
@@ -8985,12 +9094,14 @@ var VectorViewport = react.forwardRef(
8985
9094
  onUpdate: renderFrame,
8986
9095
  wheelElement: wrapperRef.current ?? void 0,
8987
9096
  touchHandledElsewhere: applePencilNav,
8988
- allowPrimaryPointerPan: () => {
9097
+ allowPrimaryPointerPan: (event) => {
8989
9098
  if (interactiveRef.current) {
8990
9099
  return toolIdRef.current === "hand";
8991
9100
  }
8992
9101
  const t = toolIdRef.current;
8993
- return t === "hand" || t === "select";
9102
+ if (t === "hand") return true;
9103
+ if (t !== "select") return false;
9104
+ return resolveReadOnlyActivation(event) === null;
8994
9105
  }
8995
9106
  });
8996
9107
  let detachPencil;
@@ -9011,7 +9122,7 @@ var VectorViewport = react.forwardRef(
9011
9122
  cameraRef.current = null;
9012
9123
  setCameraForOverlay(null);
9013
9124
  };
9014
- }, [applePencilNav, renderFrame]);
9125
+ }, [applePencilNav, renderFrame, resolveReadOnlyActivation]);
9015
9126
  react.useEffect(() => {
9016
9127
  rendererRef.current?.setInteractionState({
9017
9128
  selectedIds: effectiveSelectedIds,
@@ -9525,6 +9636,29 @@ var VectorViewport = react.forwardRef(
9525
9636
  const rect = el.getBoundingClientRect();
9526
9637
  return cam.screenToWorld(clientX - rect.left, clientY - rect.top);
9527
9638
  }, []);
9639
+ const buildSelectModeItemClickDetail = react.useCallback(
9640
+ (item, world, pointer) => ({
9641
+ item,
9642
+ worldX: world.worldX,
9643
+ worldY: world.worldY,
9644
+ clientX: pointer.clientX,
9645
+ clientY: pointer.clientY,
9646
+ pointerType: pointer.pointerType,
9647
+ shiftKey: pointer.shiftKey,
9648
+ altKey: pointer.altKey,
9649
+ metaKey: pointer.metaKey,
9650
+ ctrlKey: pointer.ctrlKey,
9651
+ items: itemsRef.current,
9652
+ updateItem: (next) => {
9653
+ onItemsChangeRef.current?.(replaceItem(itemsRef.current, item.id, next), {
9654
+ motive: "custom",
9655
+ itemIds: [item.id]
9656
+ });
9657
+ },
9658
+ setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
9659
+ }),
9660
+ []
9661
+ );
9528
9662
  const handleOverlayContextMenu = react.useCallback(
9529
9663
  (e) => {
9530
9664
  if (!interactiveRef.current || !onItemsChangeRef.current) return;
@@ -9951,6 +10085,118 @@ var VectorViewport = react.forwardRef(
9951
10085
  },
9952
10086
  [screenToWorld]
9953
10087
  );
10088
+ react.useEffect(() => {
10089
+ const root = interactionRootRef.current;
10090
+ if (!root) return;
10091
+ const onReadOnlyPointerDownCapture = (e) => {
10092
+ if (e.button !== 0) return;
10093
+ if (readOnlyItemClickStateRef.current) return;
10094
+ const target = resolveReadOnlyActivation(e);
10095
+ if (!target) return;
10096
+ const accepted = startCanvuInteraction({
10097
+ kind: "select-mode-item-click",
10098
+ toolId: "select",
10099
+ pointerType: e.pointerType,
10100
+ button: e.button,
10101
+ worldX: target.worldX,
10102
+ worldY: target.worldY,
10103
+ clientX: e.clientX,
10104
+ clientY: e.clientY,
10105
+ shiftKey: e.shiftKey,
10106
+ altKey: e.altKey,
10107
+ metaKey: e.metaKey,
10108
+ ctrlKey: e.ctrlKey,
10109
+ itemIds: [target.item.id]
10110
+ });
10111
+ if (!accepted) {
10112
+ e.preventDefault();
10113
+ e.stopPropagation();
10114
+ return;
10115
+ }
10116
+ wrapperRef.current?.focus({ preventScroll: true });
10117
+ readOnlyItemClickStateRef.current = createReadOnlyActivationSession(
10118
+ target,
10119
+ e
10120
+ );
10121
+ e.preventDefault();
10122
+ e.stopPropagation();
10123
+ };
10124
+ root.addEventListener("pointerdown", onReadOnlyPointerDownCapture, {
10125
+ capture: true
10126
+ });
10127
+ return () => {
10128
+ root.removeEventListener("pointerdown", onReadOnlyPointerDownCapture, {
10129
+ capture: true
10130
+ });
10131
+ };
10132
+ }, [resolveReadOnlyActivation, startCanvuInteraction]);
10133
+ react.useEffect(() => {
10134
+ const finishReadOnlyClick = (ev) => {
10135
+ const st = readOnlyItemClickStateRef.current;
10136
+ if (!st || st.pointerId !== ev.pointerId) return;
10137
+ readOnlyItemClickStateRef.current = null;
10138
+ const world = screenToWorld(ev.clientX, ev.clientY);
10139
+ const current = {
10140
+ worldX: world.worldX,
10141
+ worldY: world.worldY,
10142
+ clientX: ev.clientX,
10143
+ clientY: ev.clientY
10144
+ };
10145
+ updateCanvuInteractionCurrent(current);
10146
+ if (ev.type === "pointercancel") {
10147
+ finishCanvuInteraction("cancelled", { current });
10148
+ return;
10149
+ }
10150
+ if (didReadOnlyActivationMovePastTap(st, ev, TAP_PX)) {
10151
+ finishCanvuInteraction("cancelled", { current });
10152
+ return;
10153
+ }
10154
+ const item = itemsRef.current.find((candidate) => candidate.id === st.itemId) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.itemId);
10155
+ if (!item) {
10156
+ finishCanvuInteraction("cancelled", { current });
10157
+ return;
10158
+ }
10159
+ const detail = buildSelectModeItemClickDetail(item, world, ev);
10160
+ if (st.activation === "custom") {
10161
+ const placement = findReadOnlyItemClickPlacement(
10162
+ item,
10163
+ allCustomPlacementsRef.current
10164
+ );
10165
+ const onSelectModeItemClick = placement?.onSelectModeItemClick;
10166
+ if (!onSelectModeItemClick) {
10167
+ finishCanvuInteraction("cancelled", { current });
10168
+ return;
10169
+ }
10170
+ onSelectModeItemClick(detail);
10171
+ finishCanvuInteraction("completed", {
10172
+ current,
10173
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10174
+ });
10175
+ return;
10176
+ }
10177
+ const handled = readOnlyInteractionRef.current?.onItemClick?.(detail) === "handled";
10178
+ if (!handled) {
10179
+ const cur = effectiveSelectedIdsRef.current;
10180
+ const next = ev.shiftKey ? cur.includes(item.id) ? cur.filter((id) => id !== item.id) : [...cur, item.id] : [item.id];
10181
+ setEffectiveSelectedIdsRef.current(next);
10182
+ }
10183
+ finishCanvuInteraction("completed", {
10184
+ current,
10185
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10186
+ });
10187
+ };
10188
+ document.addEventListener("pointerup", finishReadOnlyClick);
10189
+ document.addEventListener("pointercancel", finishReadOnlyClick);
10190
+ return () => {
10191
+ document.removeEventListener("pointerup", finishReadOnlyClick);
10192
+ document.removeEventListener("pointercancel", finishReadOnlyClick);
10193
+ };
10194
+ }, [
10195
+ buildSelectModeItemClickDetail,
10196
+ finishCanvuInteraction,
10197
+ screenToWorld,
10198
+ updateCanvuInteractionCurrent
10199
+ ]);
9954
10200
  const handleOverlayPointerDown = react.useCallback(
9955
10201
  (e) => {
9956
10202
  let currentDragState = dragStateRef.current;
@@ -10129,7 +10375,7 @@ var VectorViewport = react.forwardRef(
10129
10375
  ignoreLocked: true
10130
10376
  });
10131
10377
  if (hit) {
10132
- const selectModeClickPlacement = !e.shiftKey ? findSelectModeItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
10378
+ const selectModeClickPlacement = !e.shiftKey ? findReadOnlyItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
10133
10379
  if (selectModeClickPlacement) {
10134
10380
  const isAlreadySelected = cur.includes(hit.id);
10135
10381
  const moveIds = isAlreadySelected ? [...cur] : [hit.id];
@@ -11036,7 +11282,7 @@ var VectorViewport = react.forwardRef(
11036
11282
  });
11037
11283
  return;
11038
11284
  }
11039
- const placement = findSelectModeItemClickPlacement(
11285
+ const placement = findReadOnlyItemClickPlacement(
11040
11286
  item,
11041
11287
  allCustomPlacementsRef.current
11042
11288
  );
@@ -11047,27 +11293,9 @@ var VectorViewport = react.forwardRef(
11047
11293
  });
11048
11294
  return;
11049
11295
  }
11050
- const { worldX, worldY } = currentWorld;
11051
- onSelectModeItemClick({
11052
- item,
11053
- worldX,
11054
- worldY,
11055
- clientX: ev.clientX,
11056
- clientY: ev.clientY,
11057
- pointerType: ev.pointerType,
11058
- shiftKey: ev.shiftKey,
11059
- altKey: ev.altKey,
11060
- metaKey: ev.metaKey,
11061
- ctrlKey: ev.ctrlKey,
11062
- items: itemsRef.current,
11063
- updateItem: (next) => {
11064
- onItemsChangeRef.current?.(
11065
- replaceItem(itemsRef.current, item.id, next),
11066
- { motive: "custom", itemIds: [item.id] }
11067
- );
11068
- },
11069
- setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
11070
- });
11296
+ onSelectModeItemClick(
11297
+ buildSelectModeItemClickDetail(item, currentWorld, ev)
11298
+ );
11071
11299
  finishCanvuInteraction("completed", {
11072
11300
  current: currentInteractionPoint,
11073
11301
  info: { motive: "custom", itemIds: [item.id], toolId: "select" }
@@ -11437,6 +11665,7 @@ var VectorViewport = react.forwardRef(
11437
11665
  document.removeEventListener("pointercancel", onUp);
11438
11666
  };
11439
11667
  }, [
11668
+ buildSelectModeItemClickDetail,
11440
11669
  emitRemoteStrokePreview,
11441
11670
  emitRemoteStrokePreviewClear,
11442
11671
  interactive,