canvu-react 0.4.63 → 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.js CHANGED
@@ -3146,7 +3146,9 @@ function ToolPluginComponent({
3146
3146
  toolTransform,
3147
3147
  createItem,
3148
3148
  selectAfterCreate,
3149
- onSelectModeItemClick
3149
+ onSelectModeItemClick,
3150
+ onBeforeInteraction,
3151
+ onAfterInteraction
3150
3152
  }) {
3151
3153
  const contribution = useMemo(
3152
3154
  () => ({
@@ -3159,9 +3161,27 @@ function ToolPluginComponent({
3159
3161
  selectAfterCreate,
3160
3162
  onSelectModeItemClick
3161
3163
  }
3162
- ] : void 0
3164
+ ] : void 0,
3165
+ interactionHooks: onBeforeInteraction || onAfterInteraction ? {
3166
+ onBeforeInteraction: onBeforeInteraction ? (detail) => {
3167
+ if (detail.toolId !== tool.id) return void 0;
3168
+ return onBeforeInteraction(detail);
3169
+ } : void 0,
3170
+ onAfterInteraction: onAfterInteraction ? (detail) => {
3171
+ if (detail.toolId !== tool.id) return;
3172
+ onAfterInteraction(detail);
3173
+ } : void 0
3174
+ } : void 0
3163
3175
  }),
3164
- [createItem, onSelectModeItemClick, selectAfterCreate, tool, toolTransform]
3176
+ [
3177
+ createItem,
3178
+ onAfterInteraction,
3179
+ onBeforeInteraction,
3180
+ onSelectModeItemClick,
3181
+ selectAfterCreate,
3182
+ tool,
3183
+ toolTransform
3184
+ ]
3165
3185
  );
3166
3186
  useCanvuPluginContribution(pluginId, contribution);
3167
3187
  return null;
@@ -3172,6 +3192,8 @@ function createToolPlugin(options) {
3172
3192
  toolTransform,
3173
3193
  selectAfterCreate,
3174
3194
  onSelectModeItemClick,
3195
+ onBeforeInteraction,
3196
+ onAfterInteraction,
3175
3197
  ...tool
3176
3198
  } = options;
3177
3199
  const pluginId = `canvu.plugin.tool:${tool.id}`;
@@ -3186,7 +3208,9 @@ function createToolPlugin(options) {
3186
3208
  toolTransform,
3187
3209
  createItem,
3188
3210
  selectAfterCreate,
3189
- onSelectModeItemClick
3211
+ onSelectModeItemClick,
3212
+ onBeforeInteraction,
3213
+ onAfterInteraction
3190
3214
  }
3191
3215
  );
3192
3216
  }
@@ -5622,7 +5646,7 @@ function attachViewportInput(options) {
5622
5646
  if (touchMomentum) {
5623
5647
  touchMomentum.cancel();
5624
5648
  }
5625
- const panOk = allowPrimaryPointerPan();
5649
+ const panOk = allowPrimaryPointerPan(e);
5626
5650
  if (e.pointerType === "mouse" && e.button === 0) {
5627
5651
  if (!panOk) {
5628
5652
  return;
@@ -7646,6 +7670,93 @@ function PresenceRemoteLayer({
7646
7670
  );
7647
7671
  }
7648
7672
 
7673
+ // src/react/read-only-activation.ts
7674
+ function findReadOnlyItemClickPlacement(item, placements) {
7675
+ const toolId = item.customToolId;
7676
+ if (!toolId) return null;
7677
+ return [...placements].reverse().find(
7678
+ (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
7679
+ ) ?? null;
7680
+ }
7681
+ function resolveReadOnlyActivationTarget(input) {
7682
+ const {
7683
+ pointer,
7684
+ camera,
7685
+ container,
7686
+ items,
7687
+ detailItems = items,
7688
+ placements,
7689
+ scope,
7690
+ selectedIds
7691
+ } = input;
7692
+ const rect = container.getBoundingClientRect();
7693
+ const world = camera.screenToWorld(
7694
+ pointer.clientX - rect.left,
7695
+ pointer.clientY - rect.top
7696
+ );
7697
+ const hit = hitTestWorldPoint(items, world.worldX, world.worldY, {
7698
+ lineHitWorld: 10 / camera.zoom,
7699
+ ignoreLocked: true
7700
+ });
7701
+ if (!hit) return null;
7702
+ const customPlacement = findReadOnlyItemClickPlacement(hit, placements);
7703
+ if (customPlacement?.onSelectModeItemClick) {
7704
+ return {
7705
+ item: hit,
7706
+ activation: "custom",
7707
+ worldX: world.worldX,
7708
+ worldY: world.worldY
7709
+ };
7710
+ }
7711
+ if (scope === "all") {
7712
+ return {
7713
+ item: hit,
7714
+ activation: "read-only",
7715
+ worldX: world.worldX,
7716
+ worldY: world.worldY
7717
+ };
7718
+ }
7719
+ if (typeof scope === "function") {
7720
+ const allowed = scope({
7721
+ item: hit,
7722
+ worldX: world.worldX,
7723
+ worldY: world.worldY,
7724
+ clientX: pointer.clientX,
7725
+ clientY: pointer.clientY,
7726
+ pointerType: pointer.pointerType,
7727
+ shiftKey: pointer.shiftKey,
7728
+ altKey: pointer.altKey,
7729
+ metaKey: pointer.metaKey,
7730
+ ctrlKey: pointer.ctrlKey,
7731
+ items: detailItems,
7732
+ selectedIds
7733
+ });
7734
+ if (allowed) {
7735
+ return {
7736
+ item: hit,
7737
+ activation: "read-only",
7738
+ worldX: world.worldX,
7739
+ worldY: world.worldY
7740
+ };
7741
+ }
7742
+ }
7743
+ return null;
7744
+ }
7745
+ function createReadOnlyActivationSession(target, pointer) {
7746
+ return {
7747
+ pointerId: pointer.pointerId,
7748
+ itemId: target.item.id,
7749
+ activation: target.activation,
7750
+ startWorld: { x: target.worldX, y: target.worldY },
7751
+ startScreen: { x: pointer.clientX, y: pointer.clientY }
7752
+ };
7753
+ }
7754
+ function didReadOnlyActivationMovePastTap(session, pointer, tapPx) {
7755
+ const dx = pointer.clientX - session.startScreen.x;
7756
+ const dy = pointer.clientY - session.startScreen.y;
7757
+ return Math.hypot(dx, dy) > tapPx;
7758
+ }
7759
+
7649
7760
  // src/react/stable-selection.ts
7650
7761
  function shallowEqualStringArray(a, b) {
7651
7762
  if (a === b) return true;
@@ -7999,13 +8110,6 @@ function isDefaultMarkerToolStyle(style) {
7999
8110
  function tagCustomPlacementItem(item, toolId) {
8000
8111
  return item.customToolId === toolId ? item : { ...item, customToolId: toolId };
8001
8112
  }
8002
- function findSelectModeItemClickPlacement(item, placements) {
8003
- const toolId = item.customToolId;
8004
- if (!toolId) return null;
8005
- return [...placements].reverse().find(
8006
- (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
8007
- ) ?? null;
8008
- }
8009
8113
  function mergeToolListById(baseTools, pluginTools) {
8010
8114
  const next = [...baseTools];
8011
8115
  for (const tool of pluginTools) {
@@ -8133,9 +8237,12 @@ var VectorViewport = forwardRef(
8133
8237
  toolId = "hand",
8134
8238
  applePencilNav = false,
8135
8239
  interactive = false,
8240
+ readOnlyInteraction,
8136
8241
  selectedIds: selectedIdsProp,
8137
8242
  onSelectionChange,
8138
8243
  onItemsChange: consumerOnItemsChange,
8244
+ onBeforeInteraction: consumerOnBeforeInteraction,
8245
+ onAfterInteraction: consumerOnAfterInteraction,
8139
8246
  onActivateLink,
8140
8247
  onWorldPointerDown: consumerOnWorldPointerDown,
8141
8248
  toolbar,
@@ -8261,6 +8368,24 @@ var VectorViewport = forwardRef(
8261
8368
  (contribution) => contribution.callbacks?.onCameraChange
8262
8369
  )
8263
8370
  );
8371
+ const beforeInteractionHooks = useMemo(
8372
+ () => [
8373
+ consumerOnBeforeInteraction,
8374
+ ...orderedPluginContributions.map(
8375
+ (contribution) => contribution.interactionHooks?.onBeforeInteraction
8376
+ )
8377
+ ].filter((hook) => hook != null),
8378
+ [consumerOnBeforeInteraction, orderedPluginContributions]
8379
+ );
8380
+ const afterInteractionHooks = useMemo(
8381
+ () => [
8382
+ consumerOnAfterInteraction,
8383
+ ...orderedPluginContributions.map(
8384
+ (contribution) => contribution.interactionHooks?.onAfterInteraction
8385
+ )
8386
+ ].filter((hook) => hook != null),
8387
+ [consumerOnAfterInteraction, orderedPluginContributions]
8388
+ );
8264
8389
  const onItemsChange = useMemo(() => {
8265
8390
  const middlewares = orderedPluginContributions.map((contribution) => contribution.wrapOnItemsChange).filter(
8266
8391
  (middleware) => middleware != null
@@ -8315,6 +8440,8 @@ var VectorViewport = forwardRef(
8315
8440
  );
8316
8441
  const toolIdRef = useRef(toolId);
8317
8442
  const interactiveRef = useRef(interactive);
8443
+ const readOnlyInteractionRef = useRef(readOnlyInteraction);
8444
+ readOnlyInteractionRef.current = readOnlyInteraction;
8318
8445
  const reducedMotionRef = useRef(false);
8319
8446
  const itemsRef = useRef(items);
8320
8447
  const onWorldPointerDownRef = useRef(onWorldPointerDown);
@@ -8328,6 +8455,7 @@ var VectorViewport = forwardRef(
8328
8455
  const allCustomPlacementsRef = useRef(allCustomPlacements);
8329
8456
  allCustomPlacementsRef.current = allCustomPlacements;
8330
8457
  const dragStateRef = useRef({ kind: "idle" });
8458
+ const readOnlyItemClickStateRef = useRef(null);
8331
8459
  const clipboardRef = useRef(null);
8332
8460
  const undoStackRef = useRef([]);
8333
8461
  const redoStackRef = useRef([]);
@@ -8404,6 +8532,81 @@ var VectorViewport = forwardRef(
8404
8532
  [items]
8405
8533
  );
8406
8534
  itemsRef.current = normalizedItems;
8535
+ const beforeInteractionHooksRef = useRef(beforeInteractionHooks);
8536
+ beforeInteractionHooksRef.current = beforeInteractionHooks;
8537
+ const afterInteractionHooksRef = useRef(afterInteractionHooks);
8538
+ afterInteractionHooksRef.current = afterInteractionHooks;
8539
+ const canvuInteractionSeqRef = useRef(0);
8540
+ const activeCanvuInteractionRef = useRef(null);
8541
+ const makeInteractionPoint = useCallback(
8542
+ (input) => ({
8543
+ worldX: input.worldX,
8544
+ worldY: input.worldY,
8545
+ clientX: input.clientX,
8546
+ clientY: input.clientY
8547
+ }),
8548
+ []
8549
+ );
8550
+ const startCanvuInteraction = useCallback(
8551
+ (input) => {
8552
+ const start = makeInteractionPoint(input);
8553
+ const detail = {
8554
+ interactionId: `canvu-interaction-${++canvuInteractionSeqRef.current}`,
8555
+ kind: input.kind,
8556
+ toolId: input.toolId,
8557
+ pointerType: input.pointerType,
8558
+ button: input.button,
8559
+ shiftKey: input.shiftKey,
8560
+ altKey: input.altKey,
8561
+ metaKey: input.metaKey,
8562
+ ctrlKey: input.ctrlKey,
8563
+ start,
8564
+ current: start,
8565
+ selectedIds: [...effectiveSelectedIdsRef.current],
8566
+ itemIds: [...input.itemIds ?? []],
8567
+ items: itemsRef.current
8568
+ };
8569
+ for (const hook of beforeInteractionHooksRef.current) {
8570
+ if (hook(detail) === "handled") {
8571
+ return null;
8572
+ }
8573
+ }
8574
+ activeCanvuInteractionRef.current = detail;
8575
+ return detail;
8576
+ },
8577
+ [makeInteractionPoint]
8578
+ );
8579
+ const updateCanvuInteractionCurrent = useCallback(
8580
+ (input) => {
8581
+ const detail = activeCanvuInteractionRef.current;
8582
+ if (!detail) return;
8583
+ activeCanvuInteractionRef.current = {
8584
+ ...detail,
8585
+ current: makeInteractionPoint(input)
8586
+ };
8587
+ },
8588
+ [makeInteractionPoint]
8589
+ );
8590
+ const finishCanvuInteraction = useCallback(
8591
+ (outcome, options) => {
8592
+ const detail = activeCanvuInteractionRef.current;
8593
+ if (!detail) return;
8594
+ activeCanvuInteractionRef.current = null;
8595
+ const current = options?.current ? makeInteractionPoint(options.current) : detail.current;
8596
+ const info = options?.info;
8597
+ const afterDetail = {
8598
+ ...detail,
8599
+ current,
8600
+ itemIds: detail.itemIds.length > 0 ? detail.itemIds : [...info?.itemIds ?? []],
8601
+ outcome,
8602
+ ...info ? { info } : {}
8603
+ };
8604
+ for (const hook of afterInteractionHooksRef.current) {
8605
+ hook(afterDetail);
8606
+ }
8607
+ },
8608
+ [makeInteractionPoint]
8609
+ );
8407
8610
  onWorldPointerDownRef.current = onWorldPointerDown;
8408
8611
  const originalOnItemsChangeRef = useRef(onItemsChange);
8409
8612
  originalOnItemsChangeRef.current = onItemsChange;
@@ -8430,6 +8633,31 @@ var VectorViewport = forwardRef(
8430
8633
  );
8431
8634
  const resolvedItemsRef = useRef(resolvedItems);
8432
8635
  resolvedItemsRef.current = resolvedItems;
8636
+ const readOnlyActivationResolutionCacheRef = useRef(/* @__PURE__ */ new WeakMap());
8637
+ const resolveReadOnlyActivation = useCallback(
8638
+ (pointer) => {
8639
+ const cache = readOnlyActivationResolutionCacheRef.current;
8640
+ if (cache.has(pointer)) return cache.get(pointer) ?? null;
8641
+ let target = null;
8642
+ const cam = cameraRef.current;
8643
+ const container = sceneContainerRef.current;
8644
+ if (!interactiveRef.current && toolIdRef.current === "select" && cam && container) {
8645
+ target = resolveReadOnlyActivationTarget({
8646
+ pointer,
8647
+ camera: cam,
8648
+ container,
8649
+ items: resolvedItemsRef.current,
8650
+ detailItems: itemsRef.current,
8651
+ placements: allCustomPlacementsRef.current,
8652
+ scope: readOnlyInteractionRef.current?.itemClicks ?? "custom",
8653
+ selectedIds: effectiveSelectedIdsRef.current
8654
+ });
8655
+ }
8656
+ cache.set(pointer, target);
8657
+ return target;
8658
+ },
8659
+ []
8660
+ );
8433
8661
  const liveId = useId();
8434
8662
  const reducedMotion = usePrefersReducedMotion();
8435
8663
  reducedMotionRef.current = reducedMotion;
@@ -8644,9 +8872,14 @@ var VectorViewport = forwardRef(
8644
8872
  );
8645
8873
  if (item) {
8646
8874
  const exists = itemsRef.current.some((it) => it.id === id);
8875
+ const info = {
8876
+ motive: "draw",
8877
+ itemIds: [id],
8878
+ toolId: args.tool
8879
+ };
8647
8880
  change(
8648
8881
  exists ? replaceItem(itemsRef.current, id, item) : [...itemsRef.current, item],
8649
- { motive: "draw", itemIds: [id], toolId: args.tool }
8882
+ info
8650
8883
  );
8651
8884
  patchCurrentStrokeStyle({
8652
8885
  stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
@@ -8654,10 +8887,15 @@ var VectorViewport = forwardRef(
8654
8887
  strokeOpacity: item.strokeOpacity,
8655
8888
  strokeDash: item.strokeDash
8656
8889
  });
8890
+ if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8891
+ requestAutoResetTool(args.tool);
8892
+ }
8893
+ return info;
8657
8894
  }
8658
8895
  if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8659
8896
  requestAutoResetTool(args.tool);
8660
8897
  }
8898
+ return void 0;
8661
8899
  },
8662
8900
  [
8663
8901
  patchCurrentStrokeStyle,
@@ -8788,17 +9026,19 @@ var VectorViewport = forwardRef(
8788
9026
  }
8789
9027
  emitRemoteStrokePreviewClear();
8790
9028
  setPlacementPreview(null);
8791
- commitCompletedStroke({
9029
+ const info = commitCompletedStroke({
8792
9030
  tool,
8793
9031
  pointerType,
8794
9032
  points: [...pts],
8795
9033
  style,
8796
9034
  itemId
8797
9035
  });
9036
+ finishCanvuInteraction("completed", { info });
8798
9037
  },
8799
9038
  [
8800
9039
  commitCompletedStroke,
8801
9040
  emitRemoteStrokePreviewClear,
9041
+ finishCanvuInteraction,
8802
9042
  releaseInteractionPointer,
8803
9043
  renderSceneWithLivePenStroke
8804
9044
  ]
@@ -8847,12 +9087,14 @@ var VectorViewport = forwardRef(
8847
9087
  onUpdate: renderFrame,
8848
9088
  wheelElement: wrapperRef.current ?? void 0,
8849
9089
  touchHandledElsewhere: applePencilNav,
8850
- allowPrimaryPointerPan: () => {
9090
+ allowPrimaryPointerPan: (event) => {
8851
9091
  if (interactiveRef.current) {
8852
9092
  return toolIdRef.current === "hand";
8853
9093
  }
8854
9094
  const t = toolIdRef.current;
8855
- return t === "hand" || t === "select";
9095
+ if (t === "hand") return true;
9096
+ if (t !== "select") return false;
9097
+ return resolveReadOnlyActivation(event) === null;
8856
9098
  }
8857
9099
  });
8858
9100
  let detachPencil;
@@ -8873,7 +9115,7 @@ var VectorViewport = forwardRef(
8873
9115
  cameraRef.current = null;
8874
9116
  setCameraForOverlay(null);
8875
9117
  };
8876
- }, [applePencilNav, renderFrame]);
9118
+ }, [applePencilNav, renderFrame, resolveReadOnlyActivation]);
8877
9119
  useEffect(() => {
8878
9120
  rendererRef.current?.setInteractionState({
8879
9121
  selectedIds: effectiveSelectedIds,
@@ -9387,6 +9629,29 @@ var VectorViewport = forwardRef(
9387
9629
  const rect = el.getBoundingClientRect();
9388
9630
  return cam.screenToWorld(clientX - rect.left, clientY - rect.top);
9389
9631
  }, []);
9632
+ const buildSelectModeItemClickDetail = useCallback(
9633
+ (item, world, pointer) => ({
9634
+ item,
9635
+ worldX: world.worldX,
9636
+ worldY: world.worldY,
9637
+ clientX: pointer.clientX,
9638
+ clientY: pointer.clientY,
9639
+ pointerType: pointer.pointerType,
9640
+ shiftKey: pointer.shiftKey,
9641
+ altKey: pointer.altKey,
9642
+ metaKey: pointer.metaKey,
9643
+ ctrlKey: pointer.ctrlKey,
9644
+ items: itemsRef.current,
9645
+ updateItem: (next) => {
9646
+ onItemsChangeRef.current?.(replaceItem(itemsRef.current, item.id, next), {
9647
+ motive: "custom",
9648
+ itemIds: [item.id]
9649
+ });
9650
+ },
9651
+ setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
9652
+ }),
9653
+ []
9654
+ );
9390
9655
  const handleOverlayContextMenu = useCallback(
9391
9656
  (e) => {
9392
9657
  if (!interactiveRef.current || !onItemsChangeRef.current) return;
@@ -9813,6 +10078,118 @@ var VectorViewport = forwardRef(
9813
10078
  },
9814
10079
  [screenToWorld]
9815
10080
  );
10081
+ useEffect(() => {
10082
+ const root = interactionRootRef.current;
10083
+ if (!root) return;
10084
+ const onReadOnlyPointerDownCapture = (e) => {
10085
+ if (e.button !== 0) return;
10086
+ if (readOnlyItemClickStateRef.current) return;
10087
+ const target = resolveReadOnlyActivation(e);
10088
+ if (!target) return;
10089
+ const accepted = startCanvuInteraction({
10090
+ kind: "select-mode-item-click",
10091
+ toolId: "select",
10092
+ pointerType: e.pointerType,
10093
+ button: e.button,
10094
+ worldX: target.worldX,
10095
+ worldY: target.worldY,
10096
+ clientX: e.clientX,
10097
+ clientY: e.clientY,
10098
+ shiftKey: e.shiftKey,
10099
+ altKey: e.altKey,
10100
+ metaKey: e.metaKey,
10101
+ ctrlKey: e.ctrlKey,
10102
+ itemIds: [target.item.id]
10103
+ });
10104
+ if (!accepted) {
10105
+ e.preventDefault();
10106
+ e.stopPropagation();
10107
+ return;
10108
+ }
10109
+ wrapperRef.current?.focus({ preventScroll: true });
10110
+ readOnlyItemClickStateRef.current = createReadOnlyActivationSession(
10111
+ target,
10112
+ e
10113
+ );
10114
+ e.preventDefault();
10115
+ e.stopPropagation();
10116
+ };
10117
+ root.addEventListener("pointerdown", onReadOnlyPointerDownCapture, {
10118
+ capture: true
10119
+ });
10120
+ return () => {
10121
+ root.removeEventListener("pointerdown", onReadOnlyPointerDownCapture, {
10122
+ capture: true
10123
+ });
10124
+ };
10125
+ }, [resolveReadOnlyActivation, startCanvuInteraction]);
10126
+ useEffect(() => {
10127
+ const finishReadOnlyClick = (ev) => {
10128
+ const st = readOnlyItemClickStateRef.current;
10129
+ if (!st || st.pointerId !== ev.pointerId) return;
10130
+ readOnlyItemClickStateRef.current = null;
10131
+ const world = screenToWorld(ev.clientX, ev.clientY);
10132
+ const current = {
10133
+ worldX: world.worldX,
10134
+ worldY: world.worldY,
10135
+ clientX: ev.clientX,
10136
+ clientY: ev.clientY
10137
+ };
10138
+ updateCanvuInteractionCurrent(current);
10139
+ if (ev.type === "pointercancel") {
10140
+ finishCanvuInteraction("cancelled", { current });
10141
+ return;
10142
+ }
10143
+ if (didReadOnlyActivationMovePastTap(st, ev, TAP_PX)) {
10144
+ finishCanvuInteraction("cancelled", { current });
10145
+ return;
10146
+ }
10147
+ const item = itemsRef.current.find((candidate) => candidate.id === st.itemId) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.itemId);
10148
+ if (!item) {
10149
+ finishCanvuInteraction("cancelled", { current });
10150
+ return;
10151
+ }
10152
+ const detail = buildSelectModeItemClickDetail(item, world, ev);
10153
+ if (st.activation === "custom") {
10154
+ const placement = findReadOnlyItemClickPlacement(
10155
+ item,
10156
+ allCustomPlacementsRef.current
10157
+ );
10158
+ const onSelectModeItemClick = placement?.onSelectModeItemClick;
10159
+ if (!onSelectModeItemClick) {
10160
+ finishCanvuInteraction("cancelled", { current });
10161
+ return;
10162
+ }
10163
+ onSelectModeItemClick(detail);
10164
+ finishCanvuInteraction("completed", {
10165
+ current,
10166
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10167
+ });
10168
+ return;
10169
+ }
10170
+ const handled = readOnlyInteractionRef.current?.onItemClick?.(detail) === "handled";
10171
+ if (!handled) {
10172
+ const cur = effectiveSelectedIdsRef.current;
10173
+ const next = ev.shiftKey ? cur.includes(item.id) ? cur.filter((id) => id !== item.id) : [...cur, item.id] : [item.id];
10174
+ setEffectiveSelectedIdsRef.current(next);
10175
+ }
10176
+ finishCanvuInteraction("completed", {
10177
+ current,
10178
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10179
+ });
10180
+ };
10181
+ document.addEventListener("pointerup", finishReadOnlyClick);
10182
+ document.addEventListener("pointercancel", finishReadOnlyClick);
10183
+ return () => {
10184
+ document.removeEventListener("pointerup", finishReadOnlyClick);
10185
+ document.removeEventListener("pointercancel", finishReadOnlyClick);
10186
+ };
10187
+ }, [
10188
+ buildSelectModeItemClickDetail,
10189
+ finishCanvuInteraction,
10190
+ screenToWorld,
10191
+ updateCanvuInteractionCurrent
10192
+ ]);
9816
10193
  const handleOverlayPointerDown = useCallback(
9817
10194
  (e) => {
9818
10195
  let currentDragState = dragStateRef.current;
@@ -9868,12 +10245,26 @@ var VectorViewport = forwardRef(
9868
10245
  if (!raw) return void 0;
9869
10246
  return raw.toolKind === "arrow" && raw.arrowBind ? bakeArrowItemToAbsolute(raw, canonical) : raw;
9870
10247
  };
10248
+ const startInteraction = (kind, itemIds) => startCanvuInteraction({
10249
+ kind,
10250
+ toolId: tool,
10251
+ pointerType: e.pointerType,
10252
+ button: e.button,
10253
+ worldX,
10254
+ worldY,
10255
+ clientX: e.clientX,
10256
+ clientY: e.clientY,
10257
+ shiftKey: e.shiftKey,
10258
+ altKey: e.altKey,
10259
+ metaKey: e.metaKey,
10260
+ ctrlKey: e.ctrlKey,
10261
+ itemIds
10262
+ });
10263
+ const stopHandledInteraction = () => {
10264
+ e.preventDefault();
10265
+ e.stopPropagation();
10266
+ };
9871
10267
  if (tool === "eraser") {
9872
- dragStateRef.current = { kind: "erase" };
9873
- eraserPreviewIdsRef.current = /* @__PURE__ */ new Set();
9874
- setEraserPreviewIds([]);
9875
- setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
9876
- setEraserActive(true);
9877
10268
  const toErase = collectEraserTargetsAtWorldPoint(
9878
10269
  resolved,
9879
10270
  worldX,
@@ -9883,6 +10274,15 @@ var VectorViewport = forwardRef(
9883
10274
  ignoreLocked: true
9884
10275
  }
9885
10276
  );
10277
+ if (!startInteraction("erase", toErase)) {
10278
+ stopHandledInteraction();
10279
+ return;
10280
+ }
10281
+ dragStateRef.current = { kind: "erase" };
10282
+ eraserPreviewIdsRef.current = /* @__PURE__ */ new Set();
10283
+ setEraserPreviewIds([]);
10284
+ setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
10285
+ setEraserActive(true);
9886
10286
  if (toErase.length > 0) {
9887
10287
  for (const id of toErase) {
9888
10288
  eraserPreviewIdsRef.current.add(id);
@@ -9915,6 +10315,10 @@ var VectorViewport = forwardRef(
9915
10315
  const snapSpin = bakedSnapshot(selected.id);
9916
10316
  if (!snapSpin) return;
9917
10317
  const pivot = itemPivotWorld(selected);
10318
+ if (!startInteraction("rotate", [selected.id])) {
10319
+ stopHandledInteraction();
10320
+ return;
10321
+ }
9918
10322
  dragStateRef.current = {
9919
10323
  kind: "rotate",
9920
10324
  id: selected.id,
@@ -9939,6 +10343,10 @@ var VectorViewport = forwardRef(
9939
10343
  if (hb) {
9940
10344
  const snapRs = bakedSnapshot(selected.id);
9941
10345
  if (!snapRs) return;
10346
+ if (!startInteraction("resize", [selected.id])) {
10347
+ stopHandledInteraction();
10348
+ return;
10349
+ }
9942
10350
  dragStateRef.current = {
9943
10351
  kind: "resize",
9944
10352
  id: selected.id,
@@ -9960,7 +10368,7 @@ var VectorViewport = forwardRef(
9960
10368
  ignoreLocked: true
9961
10369
  });
9962
10370
  if (hit) {
9963
- const selectModeClickPlacement = !e.shiftKey ? findSelectModeItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
10371
+ const selectModeClickPlacement = !e.shiftKey ? findReadOnlyItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
9964
10372
  if (selectModeClickPlacement) {
9965
10373
  const isAlreadySelected = cur.includes(hit.id);
9966
10374
  const moveIds = isAlreadySelected ? [...cur] : [hit.id];
@@ -9971,6 +10379,10 @@ var VectorViewport = forwardRef(
9971
10379
  moveSnapshots[id] = snap;
9972
10380
  }
9973
10381
  }
10382
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10383
+ stopHandledInteraction();
10384
+ return;
10385
+ }
9974
10386
  dragStateRef.current = {
9975
10387
  kind: "select-mode-item-click",
9976
10388
  id: hit.id,
@@ -9987,7 +10399,14 @@ var VectorViewport = forwardRef(
9987
10399
  }
9988
10400
  if (e.shiftKey) {
9989
10401
  const next = cur.includes(hit.id) ? cur.filter((id) => id !== hit.id) : [...cur, hit.id];
10402
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10403
+ stopHandledInteraction();
10404
+ return;
10405
+ }
9990
10406
  setEffectiveSelectedIdsRef.current(next);
10407
+ finishCanvuInteraction("completed", {
10408
+ info: { motive: "custom", itemIds: [hit.id], toolId: tool }
10409
+ });
9991
10410
  e.preventDefault();
9992
10411
  e.stopPropagation();
9993
10412
  return;
@@ -9995,7 +10414,6 @@ var VectorViewport = forwardRef(
9995
10414
  let idsToMove;
9996
10415
  if (!cur.includes(hit.id)) {
9997
10416
  idsToMove = [hit.id];
9998
- setEffectiveSelectedIdsRef.current(idsToMove);
9999
10417
  } else {
10000
10418
  idsToMove = [...cur];
10001
10419
  }
@@ -10006,6 +10424,13 @@ var VectorViewport = forwardRef(
10006
10424
  snapshots[id] = snap;
10007
10425
  }
10008
10426
  }
10427
+ if (!startInteraction("move", idsToMove)) {
10428
+ stopHandledInteraction();
10429
+ return;
10430
+ }
10431
+ if (!cur.includes(hit.id)) {
10432
+ setEffectiveSelectedIdsRef.current(idsToMove);
10433
+ }
10009
10434
  dragStateRef.current = {
10010
10435
  kind: "move",
10011
10436
  ids: idsToMove,
@@ -10030,9 +10455,14 @@ var VectorViewport = forwardRef(
10030
10455
  }
10031
10456
  }
10032
10457
  if (Object.keys(snapshots).length > 0) {
10458
+ const moveIds = Object.keys(snapshots);
10459
+ if (!startInteraction("move", moveIds)) {
10460
+ stopHandledInteraction();
10461
+ return;
10462
+ }
10033
10463
  dragStateRef.current = {
10034
10464
  kind: "move",
10035
- ids: Object.keys(snapshots),
10465
+ ids: moveIds,
10036
10466
  snapshots,
10037
10467
  startWorld: { x: worldX, y: worldY }
10038
10468
  };
@@ -10043,6 +10473,10 @@ var VectorViewport = forwardRef(
10043
10473
  }
10044
10474
  }
10045
10475
  }
10476
+ if (!startInteraction("marquee")) {
10477
+ stopHandledInteraction();
10478
+ return;
10479
+ }
10046
10480
  dragStateRef.current = {
10047
10481
  kind: "marquee",
10048
10482
  startWorld: { x: worldX, y: worldY },
@@ -10071,6 +10505,10 @@ var VectorViewport = forwardRef(
10071
10505
  );
10072
10506
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10073
10507
  const directPenStroke = e.pointerType === "pen" && (tool === "draw" || tool === "marker");
10508
+ if (!startInteraction("stroke")) {
10509
+ stopHandledInteraction();
10510
+ return;
10511
+ }
10074
10512
  let itemId;
10075
10513
  if (directPenStroke) {
10076
10514
  itemId = createShapeId();
@@ -10114,6 +10552,10 @@ var VectorViewport = forwardRef(
10114
10552
  return;
10115
10553
  }
10116
10554
  if (tool === "text" || tool === "image") {
10555
+ if (!startInteraction("tap")) {
10556
+ stopHandledInteraction();
10557
+ return;
10558
+ }
10117
10559
  dragStateRef.current = {
10118
10560
  kind: "tap",
10119
10561
  tool,
@@ -10127,6 +10569,10 @@ var VectorViewport = forwardRef(
10127
10569
  }
10128
10570
  const cp = customPlacementRef.current;
10129
10571
  if (tool === "rect" || tool === "ellipse" || tool === "architectural-cloud" || tool === "line" || tool === "arrow" || cp && tool === cp.toolId) {
10572
+ if (!startInteraction("place")) {
10573
+ stopHandledInteraction();
10574
+ return;
10575
+ }
10130
10576
  dragStateRef.current = {
10131
10577
  kind: "place",
10132
10578
  tool,
@@ -10144,8 +10590,10 @@ var VectorViewport = forwardRef(
10144
10590
  captureInteractionPointer,
10145
10591
  emitRemoteStrokePreview,
10146
10592
  finalizeStrokeDragState,
10593
+ finishCanvuInteraction,
10147
10594
  renderSceneWithLivePenStroke,
10148
10595
  screenToWorld,
10596
+ startCanvuInteraction,
10149
10597
  startOrRestartStraightStrokeHoldTimer
10150
10598
  ]
10151
10599
  );
@@ -10174,14 +10622,32 @@ var VectorViewport = forwardRef(
10174
10622
  e.stopImmediatePropagation();
10175
10623
  return;
10176
10624
  }
10177
- wrapperRef.current?.focus({ preventScroll: true });
10178
- setContextMenu(null);
10179
10625
  const startPoint = pointerSampleToWorldPoint(
10180
10626
  screenToWorld,
10181
10627
  e.clientX,
10182
10628
  e.clientY,
10183
10629
  e.pressure
10184
10630
  );
10631
+ if (!startCanvuInteraction({
10632
+ kind: "stroke",
10633
+ toolId: tool,
10634
+ pointerType: e.pointerType,
10635
+ button: e.button,
10636
+ worldX: startPoint.x,
10637
+ worldY: startPoint.y,
10638
+ clientX: e.clientX,
10639
+ clientY: e.clientY,
10640
+ shiftKey: e.shiftKey,
10641
+ altKey: e.altKey,
10642
+ metaKey: e.metaKey,
10643
+ ctrlKey: e.ctrlKey
10644
+ })) {
10645
+ e.preventDefault();
10646
+ e.stopImmediatePropagation();
10647
+ return;
10648
+ }
10649
+ wrapperRef.current?.focus({ preventScroll: true });
10650
+ setContextMenu(null);
10185
10651
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10186
10652
  const itemId = createShapeId();
10187
10653
  const item = createFreehandStrokeItem(
@@ -10229,6 +10695,7 @@ var VectorViewport = forwardRef(
10229
10695
  interactive,
10230
10696
  renderSceneWithLivePenStroke,
10231
10697
  screenToWorld,
10698
+ startCanvuInteraction,
10232
10699
  startOrRestartStraightStrokeHoldTimer
10233
10700
  ]);
10234
10701
  useEffect(() => {
@@ -10279,15 +10746,32 @@ var VectorViewport = forwardRef(
10279
10746
  stopTouchEvent(ev);
10280
10747
  return;
10281
10748
  }
10282
- wrapperRef.current?.focus({ preventScroll: true });
10283
- setContextMenu(null);
10284
- penDetectedRef.current = true;
10285
10749
  const startPoint = pointerSampleToWorldPoint(
10286
10750
  screenToWorld,
10287
10751
  touch.clientX,
10288
10752
  touch.clientY,
10289
10753
  touchPressure(touch)
10290
10754
  );
10755
+ if (!startCanvuInteraction({
10756
+ kind: "stroke",
10757
+ toolId: tool,
10758
+ pointerType: "pen",
10759
+ button: 0,
10760
+ worldX: startPoint.x,
10761
+ worldY: startPoint.y,
10762
+ clientX: touch.clientX,
10763
+ clientY: touch.clientY,
10764
+ shiftKey: ev.shiftKey,
10765
+ altKey: ev.altKey,
10766
+ metaKey: ev.metaKey,
10767
+ ctrlKey: ev.ctrlKey
10768
+ })) {
10769
+ stopTouchEvent(ev);
10770
+ return;
10771
+ }
10772
+ wrapperRef.current?.focus({ preventScroll: true });
10773
+ setContextMenu(null);
10774
+ penDetectedRef.current = true;
10291
10775
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, touch.clientX, touch.clientY) : void 0;
10292
10776
  const itemId = createShapeId();
10293
10777
  const item = createFreehandStrokeItem(
@@ -10333,6 +10817,12 @@ var VectorViewport = forwardRef(
10333
10817
  touch.clientY,
10334
10818
  touchPressure(touch)
10335
10819
  );
10820
+ updateCanvuInteractionCurrent({
10821
+ worldX: endpoint.x,
10822
+ worldY: endpoint.y,
10823
+ clientX: touch.clientX,
10824
+ clientY: touch.clientY
10825
+ });
10336
10826
  if (updateStraightStrokeForMove(st, touch.clientX, touch.clientY, endpoint)) {
10337
10827
  debugApplePencilPointer("touchmove-stroke", {
10338
10828
  touchId: touch.identifier,
@@ -10374,16 +10864,42 @@ var VectorViewport = forwardRef(
10374
10864
  if (st.kind !== "stroke") return;
10375
10865
  const touch = findChangedTouch(ev.changedTouches);
10376
10866
  if (!touch) return;
10867
+ const currentPoint = pointerSampleToWorldPoint(
10868
+ screenToWorld,
10869
+ touch.clientX,
10870
+ touch.clientY,
10871
+ touchPressure(touch)
10872
+ );
10873
+ updateCanvuInteractionCurrent({
10874
+ worldX: currentPoint.x,
10875
+ worldY: currentPoint.y,
10876
+ clientX: touch.clientX,
10877
+ clientY: touch.clientY
10878
+ });
10879
+ if (ev.type === "touchcancel") {
10880
+ clearStraightStrokeHoldTimer(st);
10881
+ dragStateRef.current = { kind: "idle" };
10882
+ releaseInteractionPointer();
10883
+ if (st.itemId) {
10884
+ renderSceneWithLivePenStroke(null);
10885
+ }
10886
+ emitRemoteStrokePreviewClear();
10887
+ setPlacementPreview(null);
10888
+ finishCanvuInteraction("cancelled", {
10889
+ current: {
10890
+ worldX: currentPoint.x,
10891
+ worldY: currentPoint.y,
10892
+ clientX: touch.clientX,
10893
+ clientY: touch.clientY
10894
+ }
10895
+ });
10896
+ stopTouchEvent(ev);
10897
+ return;
10898
+ }
10377
10899
  const cam = cameraRef.current;
10378
10900
  if (cam) {
10379
10901
  if (st.straightLine?.active) {
10380
- const endpoint = pointerSampleToWorldPoint(
10381
- screenToWorld,
10382
- touch.clientX,
10383
- touch.clientY,
10384
- touchPressure(touch)
10385
- );
10386
- setStraightStrokeEndpoint(st, endpoint);
10902
+ setStraightStrokeEndpoint(st, currentPoint);
10387
10903
  } else {
10388
10904
  const completedPoints = appendTouchToStrokePoints(
10389
10905
  st.points,
@@ -10434,12 +10950,17 @@ var VectorViewport = forwardRef(
10434
10950
  }, [
10435
10951
  applePencilNav,
10436
10952
  emitRemoteStrokePreview,
10953
+ emitRemoteStrokePreviewClear,
10437
10954
  finalizeStrokeDragState,
10955
+ finishCanvuInteraction,
10438
10956
  interactive,
10957
+ releaseInteractionPointer,
10439
10958
  renderSceneWithLivePenStroke,
10440
10959
  screenToWorld,
10441
10960
  setStraightStrokeEndpoint,
10961
+ startCanvuInteraction,
10442
10962
  startOrRestartStraightStrokeHoldTimer,
10963
+ updateCanvuInteractionCurrent,
10443
10964
  updateStraightStrokeForMove
10444
10965
  ]);
10445
10966
  useEffect(() => {
@@ -10452,6 +10973,12 @@ var VectorViewport = forwardRef(
10452
10973
  if (st.kind === "tap") return;
10453
10974
  if (st.kind === "marquee") {
10454
10975
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
10976
+ updateCanvuInteractionCurrent({
10977
+ worldX: worldX2,
10978
+ worldY: worldY2,
10979
+ clientX: ev.clientX,
10980
+ clientY: ev.clientY
10981
+ });
10455
10982
  const raw = rectFromCorners(st.startWorld, { x: worldX2, y: worldY2 });
10456
10983
  setPlacementPreview({ kind: "marquee", rect: raw });
10457
10984
  const nextCand = collectItemIdsInRect(
@@ -10479,6 +11006,12 @@ var VectorViewport = forwardRef(
10479
11006
  ev.clientY,
10480
11007
  ev.pointerType === "pen" ? ev.pressure : void 0
10481
11008
  );
11009
+ updateCanvuInteractionCurrent({
11010
+ worldX: endpoint.x,
11011
+ worldY: endpoint.y,
11012
+ clientX: ev.clientX,
11013
+ clientY: ev.clientY
11014
+ });
10482
11015
  if (updateStraightStrokeForMove(st, ev.clientX, ev.clientY, endpoint)) {
10483
11016
  return;
10484
11017
  }
@@ -10527,6 +11060,12 @@ var VectorViewport = forwardRef(
10527
11060
  }
10528
11061
  if (st.kind === "erase") {
10529
11062
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
11063
+ updateCanvuInteractionCurrent({
11064
+ worldX: worldX2,
11065
+ worldY: worldY2,
11066
+ clientX: ev.clientX,
11067
+ clientY: ev.clientY
11068
+ });
10530
11069
  const lineHitWorld = 10 / cam.zoom;
10531
11070
  setEraserTrail(
10532
11071
  (prev) => pruneEraserTrail([...prev, { x: worldX2, y: worldY2, t: Date.now() }])
@@ -10552,6 +11091,12 @@ var VectorViewport = forwardRef(
10552
11091
  const change = onItemsChangeRef.current;
10553
11092
  if (!change) return;
10554
11093
  const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11094
+ updateCanvuInteractionCurrent({
11095
+ worldX,
11096
+ worldY,
11097
+ clientX: ev.clientX,
11098
+ clientY: ev.clientY
11099
+ });
10555
11100
  if (st.kind === "select-mode-item-click") {
10556
11101
  const screenDx = ev.clientX - st.startScreen.x;
10557
11102
  const screenDy = ev.clientY - st.startScreen.y;
@@ -10684,48 +11229,69 @@ var VectorViewport = forwardRef(
10684
11229
  setPlacementPreview(null);
10685
11230
  marqueeCandidateIdsRef.current = [];
10686
11231
  setMarqueeCandidateIds([]);
11232
+ finishCanvuInteraction("cancelled");
10687
11233
  return;
10688
11234
  }
11235
+ const finishOutcome = ev.type === "pointercancel" ? "cancelled" : "completed";
11236
+ const currentWorld = screenToWorld(ev.clientX, ev.clientY);
11237
+ const currentInteractionPoint = {
11238
+ worldX: currentWorld.worldX,
11239
+ worldY: currentWorld.worldY,
11240
+ clientX: ev.clientX,
11241
+ clientY: ev.clientY
11242
+ };
11243
+ updateCanvuInteractionCurrent(currentInteractionPoint);
10689
11244
  if (st.kind === "move" || st.kind === "resize" || st.kind === "rotate") {
11245
+ const info = st.kind === "move" ? { motive: "move", itemIds: [...st.ids] } : st.kind === "resize" ? { motive: "resize", itemIds: [st.id] } : { motive: "rotate", itemIds: [st.id] };
10690
11246
  dragStateRef.current = { kind: "idle" };
10691
11247
  releaseInteractionPointer();
11248
+ finishCanvuInteraction(finishOutcome, {
11249
+ current: currentInteractionPoint,
11250
+ ...finishOutcome === "completed" ? { info } : {}
11251
+ });
10692
11252
  return;
10693
11253
  }
10694
11254
  if (st.kind === "select-mode-item-click") {
10695
11255
  dragStateRef.current = { kind: "idle" };
10696
11256
  releaseInteractionPointer();
10697
- if (ev.type === "pointercancel") return;
11257
+ if (finishOutcome === "cancelled") {
11258
+ finishCanvuInteraction("cancelled", {
11259
+ current: currentInteractionPoint
11260
+ });
11261
+ return;
11262
+ }
10698
11263
  const dx = ev.clientX - st.startScreen.x;
10699
11264
  const dy = ev.clientY - st.startScreen.y;
10700
- if (Math.hypot(dx, dy) > TAP_PX) return;
11265
+ if (Math.hypot(dx, dy) > TAP_PX) {
11266
+ finishCanvuInteraction("cancelled", {
11267
+ current: currentInteractionPoint
11268
+ });
11269
+ return;
11270
+ }
10701
11271
  const item = itemsRef.current.find((candidate) => candidate.id === st.id) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.id);
10702
- if (!item) return;
10703
- const placement = findSelectModeItemClickPlacement(
11272
+ if (!item) {
11273
+ finishCanvuInteraction("cancelled", {
11274
+ current: currentInteractionPoint
11275
+ });
11276
+ return;
11277
+ }
11278
+ const placement = findReadOnlyItemClickPlacement(
10704
11279
  item,
10705
11280
  allCustomPlacementsRef.current
10706
11281
  );
10707
11282
  const onSelectModeItemClick = placement?.onSelectModeItemClick;
10708
- if (!onSelectModeItemClick) return;
10709
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
10710
- onSelectModeItemClick({
10711
- item,
10712
- worldX,
10713
- worldY,
10714
- clientX: ev.clientX,
10715
- clientY: ev.clientY,
10716
- pointerType: ev.pointerType,
10717
- shiftKey: ev.shiftKey,
10718
- altKey: ev.altKey,
10719
- metaKey: ev.metaKey,
10720
- ctrlKey: ev.ctrlKey,
10721
- items: itemsRef.current,
10722
- updateItem: (next) => {
10723
- onItemsChangeRef.current?.(
10724
- replaceItem(itemsRef.current, item.id, next),
10725
- { motive: "custom", itemIds: [item.id] }
10726
- );
10727
- },
10728
- setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
11283
+ if (!onSelectModeItemClick) {
11284
+ finishCanvuInteraction("cancelled", {
11285
+ current: currentInteractionPoint
11286
+ });
11287
+ return;
11288
+ }
11289
+ onSelectModeItemClick(
11290
+ buildSelectModeItemClickDetail(item, currentWorld, ev)
11291
+ );
11292
+ finishCanvuInteraction("completed", {
11293
+ current: currentInteractionPoint,
11294
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10729
11295
  });
10730
11296
  return;
10731
11297
  }
@@ -10735,16 +11301,25 @@ var VectorViewport = forwardRef(
10735
11301
  setPlacementPreview(null);
10736
11302
  marqueeCandidateIdsRef.current = [];
10737
11303
  setMarqueeCandidateIds([]);
10738
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11304
+ const { worldX, worldY } = currentWorld;
10739
11305
  const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
10740
11306
  const br = normalizeRect(raw);
10741
11307
  const screenDx = ev.clientX - st.startScreen.x;
10742
11308
  const screenDy = ev.clientY - st.startScreen.y;
11309
+ if (finishOutcome === "cancelled") {
11310
+ finishCanvuInteraction("cancelled", {
11311
+ current: currentInteractionPoint
11312
+ });
11313
+ return;
11314
+ }
10743
11315
  const tooSmall = Math.hypot(screenDx, screenDy) < TAP_PX || br.width < MIN_MARQUEE_WORLD && br.height < MIN_MARQUEE_WORLD;
10744
11316
  if (tooSmall) {
10745
11317
  if (!st.shiftKey) {
10746
11318
  setEffectiveSelectedIdsRef.current([]);
10747
11319
  }
11320
+ finishCanvuInteraction("cancelled", {
11321
+ current: currentInteractionPoint
11322
+ });
10748
11323
  return;
10749
11324
  }
10750
11325
  const picked = collectItemIdsInRect(resolvedItemsRef.current, br);
@@ -10761,9 +11336,26 @@ var VectorViewport = forwardRef(
10761
11336
  } else {
10762
11337
  setEffectiveSelectedIdsRef.current(picked);
10763
11338
  }
11339
+ finishCanvuInteraction("completed", {
11340
+ current: currentInteractionPoint
11341
+ });
10764
11342
  return;
10765
11343
  }
10766
11344
  if (st.kind === "stroke") {
11345
+ if (finishOutcome === "cancelled") {
11346
+ clearStraightStrokeHoldTimer(st);
11347
+ dragStateRef.current = { kind: "idle" };
11348
+ releaseInteractionPointer();
11349
+ if (st.itemId) {
11350
+ renderSceneWithLivePenStroke(null);
11351
+ }
11352
+ emitRemoteStrokePreviewClear();
11353
+ setPlacementPreview(null);
11354
+ finishCanvuInteraction("cancelled", {
11355
+ current: currentInteractionPoint
11356
+ });
11357
+ return;
11358
+ }
10767
11359
  const completedPoints = (() => {
10768
11360
  if (st.straightLine?.active) {
10769
11361
  const endpoint = pointerSampleToWorldPoint(
@@ -10794,15 +11386,20 @@ var VectorViewport = forwardRef(
10794
11386
  }
10795
11387
  if (st.kind === "erase") {
10796
11388
  const change = onItemsChangeRef.current;
11389
+ const erasedIds = [...eraserPreviewIdsRef.current];
11390
+ const info = erasedIds.length > 0 ? {
11391
+ motive: "erase",
11392
+ itemIds: erasedIds,
11393
+ toolId: "eraser"
11394
+ } : void 0;
10797
11395
  if (change && eraserPreviewIdsRef.current.size > 0) {
10798
11396
  const idSet = new Set(eraserPreviewIdsRef.current);
10799
- change(
10800
- itemsRef.current.filter((i) => !idSet.has(i.id)),
10801
- {
10802
- motive: "erase",
10803
- itemIds: [...idSet]
10804
- }
10805
- );
11397
+ if (finishOutcome === "completed") {
11398
+ change(
11399
+ itemsRef.current.filter((i) => !idSet.has(i.id)),
11400
+ info
11401
+ );
11402
+ }
10806
11403
  }
10807
11404
  eraserPreviewIdsRef.current.clear();
10808
11405
  setEraserPreviewIds([]);
@@ -10810,7 +11407,13 @@ var VectorViewport = forwardRef(
10810
11407
  setEraserActive(false);
10811
11408
  dragStateRef.current = { kind: "idle" };
10812
11409
  releaseInteractionPointer();
10813
- requestAutoResetTool("eraser");
11410
+ if (finishOutcome === "completed") {
11411
+ requestAutoResetTool("eraser");
11412
+ }
11413
+ finishCanvuInteraction(finishOutcome, {
11414
+ current: currentInteractionPoint,
11415
+ ...finishOutcome === "completed" && info ? { info } : {}
11416
+ });
10814
11417
  return;
10815
11418
  }
10816
11419
  if (st.kind === "tap") {
@@ -10818,11 +11421,22 @@ var VectorViewport = forwardRef(
10818
11421
  const dy = ev.clientY - st.startScreen.y;
10819
11422
  dragStateRef.current = { kind: "idle" };
10820
11423
  releaseInteractionPointer();
10821
- if (Math.hypot(dx, dy) > TAP_PX) return;
11424
+ if (finishOutcome === "cancelled" || Math.hypot(dx, dy) > TAP_PX) {
11425
+ finishCanvuInteraction("cancelled", {
11426
+ current: currentInteractionPoint
11427
+ });
11428
+ return;
11429
+ }
10822
11430
  const change = onItemsChangeRef.current;
10823
- if (!change) return;
11431
+ if (!change) {
11432
+ finishCanvuInteraction("cancelled", {
11433
+ current: currentInteractionPoint
11434
+ });
11435
+ return;
11436
+ }
10824
11437
  const id = createShapeId();
10825
11438
  const { x: worldX, y: worldY } = st.startWorld;
11439
+ let info;
10826
11440
  if (st.tool === "text") {
10827
11441
  const fs = strokeStyleRef.current.textFontSize;
10828
11442
  const baseline = textBaselineYFor(fs);
@@ -10845,11 +11459,12 @@ var VectorViewport = forwardRef(
10845
11459
  bounds: { ...newItem.bounds }
10846
11460
  };
10847
11461
  const hidden = applyTextDraftWhileEditing(newItem, "");
10848
- change([...itemsRef.current, hidden], {
11462
+ info = {
10849
11463
  motive: "text-create",
10850
11464
  itemIds: [id],
10851
11465
  toolId: st.tool
10852
- });
11466
+ };
11467
+ change([...itemsRef.current, hidden], info);
10853
11468
  setEffectiveSelectedIdsRef.current([id]);
10854
11469
  setEditingTextId(id);
10855
11470
  setDraftText("");
@@ -10858,6 +11473,10 @@ var VectorViewport = forwardRef(
10858
11473
  imageInputRef.current?.click();
10859
11474
  }
10860
11475
  requestAutoResetTool(st.tool);
11476
+ finishCanvuInteraction("completed", {
11477
+ current: currentInteractionPoint,
11478
+ ...info ? { info } : {}
11479
+ });
10861
11480
  return;
10862
11481
  }
10863
11482
  if (st.kind === "place") {
@@ -10868,11 +11487,19 @@ var VectorViewport = forwardRef(
10868
11487
  dragStateRef.current = { kind: "idle" };
10869
11488
  releaseInteractionPointer();
10870
11489
  setPlacementPreview(null);
10871
- if (!change) return;
11490
+ if (finishOutcome === "cancelled" || !change) {
11491
+ finishCanvuInteraction("cancelled", {
11492
+ current: currentInteractionPoint
11493
+ });
11494
+ return;
11495
+ }
10872
11496
  if (st.tool === "arrow") {
10873
11497
  const screenDx = ev.clientX - st.startScreen.x;
10874
11498
  const screenDy = ev.clientY - st.startScreen.y;
10875
11499
  if (Math.hypot(screenDx, screenDy) < MIN_ARROW_DRAG_PX) {
11500
+ finishCanvuInteraction("cancelled", {
11501
+ current: currentInteractionPoint
11502
+ });
10876
11503
  return;
10877
11504
  }
10878
11505
  const maxDist = ARROW_BIND_SNAP_PX / cam.zoom;
@@ -10904,18 +11531,23 @@ var VectorViewport = forwardRef(
10904
11531
  ...snapB ? { end: snapB.binding } : {}
10905
11532
  };
10906
11533
  }
11534
+ const info2 = {
11535
+ motive: "place",
11536
+ itemIds: [id2],
11537
+ toolId: st.tool
11538
+ };
10907
11539
  change(
10908
11540
  [
10909
11541
  ...itemsRef.current,
10910
11542
  createLineItem(id2, rawArrow, line, "arrow", pen2, arrowBind)
10911
11543
  ],
10912
- {
10913
- motive: "place",
10914
- itemIds: [id2],
10915
- toolId: st.tool
10916
- }
11544
+ info2
10917
11545
  );
10918
11546
  setEffectiveSelectedIdsRef.current([id2]);
11547
+ finishCanvuInteraction("completed", {
11548
+ current: currentInteractionPoint,
11549
+ info: info2
11550
+ });
10919
11551
  return;
10920
11552
  }
10921
11553
  let raw = rectFromCorners(a, b);
@@ -10940,47 +11572,61 @@ var VectorViewport = forwardRef(
10940
11572
  }
10941
11573
  const id = createShapeId();
10942
11574
  const pen = strokeStyleRef.current;
11575
+ let info;
10943
11576
  if (cpUp && st.tool === cpUp.toolId) {
10944
11577
  const item = tagCustomPlacementItem(
10945
11578
  cpUp.createItem({ id, bounds: br }),
10946
11579
  cpUp.toolId
10947
11580
  );
10948
- change(itemsRef.current.concat(item), {
11581
+ info = {
10949
11582
  motive: "place",
10950
11583
  itemIds: [id],
10951
11584
  toolId: st.tool
10952
- });
11585
+ };
11586
+ change(itemsRef.current.concat(item), info);
10953
11587
  if (cpUp.selectAfterCreate !== false) {
10954
11588
  setEffectiveSelectedIdsRef.current([id]);
10955
11589
  }
11590
+ finishCanvuInteraction("completed", {
11591
+ current: currentInteractionPoint,
11592
+ info
11593
+ });
10956
11594
  return;
10957
11595
  }
10958
11596
  if (st.tool === "rect") {
10959
- change([...itemsRef.current, createRectangleItem(id, raw, pen)], {
11597
+ info = {
10960
11598
  motive: "place",
10961
11599
  itemIds: [id],
10962
11600
  toolId: st.tool
10963
- });
11601
+ };
11602
+ change([...itemsRef.current, createRectangleItem(id, raw, pen)], info);
10964
11603
  setEffectiveSelectedIdsRef.current([id]);
10965
11604
  } else if (st.tool === "ellipse") {
10966
- change([...itemsRef.current, createEllipseItem(id, raw, pen)], {
11605
+ info = {
10967
11606
  motive: "place",
10968
11607
  itemIds: [id],
10969
11608
  toolId: st.tool
10970
- });
11609
+ };
11610
+ change([...itemsRef.current, createEllipseItem(id, raw, pen)], info);
10971
11611
  setEffectiveSelectedIdsRef.current([id]);
10972
11612
  } else if (st.tool === "architectural-cloud") {
11613
+ info = {
11614
+ motive: "place",
11615
+ itemIds: [id],
11616
+ toolId: st.tool
11617
+ };
10973
11618
  change(
10974
11619
  [...itemsRef.current, createArchitecturalCloudItem(id, raw, pen)],
10975
- {
10976
- motive: "place",
10977
- itemIds: [id],
10978
- toolId: st.tool
10979
- }
11620
+ info
10980
11621
  );
10981
11622
  setEffectiveSelectedIdsRef.current([id]);
10982
11623
  } else if (st.tool === "line" || st.tool === "arrow") {
10983
11624
  const line = lineEndpointsToLocal(raw, lineA, lineB);
11625
+ info = {
11626
+ motive: "place",
11627
+ itemIds: [id],
11628
+ toolId: st.tool
11629
+ };
10984
11630
  change(
10985
11631
  [
10986
11632
  ...itemsRef.current,
@@ -10992,15 +11638,15 @@ var VectorViewport = forwardRef(
10992
11638
  pen
10993
11639
  )
10994
11640
  ],
10995
- {
10996
- motive: "place",
10997
- itemIds: [id],
10998
- toolId: st.tool
10999
- }
11641
+ info
11000
11642
  );
11001
11643
  setEffectiveSelectedIdsRef.current([id]);
11002
11644
  }
11003
11645
  requestAutoResetTool(st.tool);
11646
+ finishCanvuInteraction("completed", {
11647
+ current: currentInteractionPoint,
11648
+ ...info ? { info } : {}
11649
+ });
11004
11650
  }
11005
11651
  };
11006
11652
  document.addEventListener("pointermove", onMove);
@@ -11012,17 +11658,20 @@ var VectorViewport = forwardRef(
11012
11658
  document.removeEventListener("pointercancel", onUp);
11013
11659
  };
11014
11660
  }, [
11661
+ buildSelectModeItemClickDetail,
11015
11662
  emitRemoteStrokePreview,
11016
11663
  emitRemoteStrokePreviewClear,
11017
11664
  interactive,
11018
11665
  pruneEraserTrail,
11019
11666
  pruneLaserTrail,
11020
11667
  finalizeStrokeDragState,
11668
+ finishCanvuInteraction,
11021
11669
  renderSceneWithLivePenStroke,
11022
11670
  releaseInteractionPointer,
11023
11671
  requestAutoResetTool,
11024
11672
  screenToWorld,
11025
11673
  setStraightStrokeEndpoint,
11674
+ updateCanvuInteractionCurrent,
11026
11675
  updateStraightStrokeForMove
11027
11676
  ]);
11028
11677
  const selectedItemsForOverlay = useMemo(() => {