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.cjs CHANGED
@@ -3153,7 +3153,9 @@ function ToolPluginComponent({
3153
3153
  toolTransform,
3154
3154
  createItem,
3155
3155
  selectAfterCreate,
3156
- onSelectModeItemClick
3156
+ onSelectModeItemClick,
3157
+ onBeforeInteraction,
3158
+ onAfterInteraction
3157
3159
  }) {
3158
3160
  const contribution = react.useMemo(
3159
3161
  () => ({
@@ -3166,9 +3168,27 @@ function ToolPluginComponent({
3166
3168
  selectAfterCreate,
3167
3169
  onSelectModeItemClick
3168
3170
  }
3169
- ] : void 0
3171
+ ] : void 0,
3172
+ interactionHooks: onBeforeInteraction || onAfterInteraction ? {
3173
+ onBeforeInteraction: onBeforeInteraction ? (detail) => {
3174
+ if (detail.toolId !== tool.id) return void 0;
3175
+ return onBeforeInteraction(detail);
3176
+ } : void 0,
3177
+ onAfterInteraction: onAfterInteraction ? (detail) => {
3178
+ if (detail.toolId !== tool.id) return;
3179
+ onAfterInteraction(detail);
3180
+ } : void 0
3181
+ } : void 0
3170
3182
  }),
3171
- [createItem, onSelectModeItemClick, selectAfterCreate, tool, toolTransform]
3183
+ [
3184
+ createItem,
3185
+ onAfterInteraction,
3186
+ onBeforeInteraction,
3187
+ onSelectModeItemClick,
3188
+ selectAfterCreate,
3189
+ tool,
3190
+ toolTransform
3191
+ ]
3172
3192
  );
3173
3193
  useCanvuPluginContribution(pluginId, contribution);
3174
3194
  return null;
@@ -3179,6 +3199,8 @@ function createToolPlugin(options) {
3179
3199
  toolTransform,
3180
3200
  selectAfterCreate,
3181
3201
  onSelectModeItemClick,
3202
+ onBeforeInteraction,
3203
+ onAfterInteraction,
3182
3204
  ...tool
3183
3205
  } = options;
3184
3206
  const pluginId = `canvu.plugin.tool:${tool.id}`;
@@ -3193,7 +3215,9 @@ function createToolPlugin(options) {
3193
3215
  toolTransform,
3194
3216
  createItem,
3195
3217
  selectAfterCreate,
3196
- onSelectModeItemClick
3218
+ onSelectModeItemClick,
3219
+ onBeforeInteraction,
3220
+ onAfterInteraction
3197
3221
  }
3198
3222
  );
3199
3223
  }
@@ -5629,7 +5653,7 @@ function attachViewportInput(options) {
5629
5653
  if (touchMomentum) {
5630
5654
  touchMomentum.cancel();
5631
5655
  }
5632
- const panOk = allowPrimaryPointerPan();
5656
+ const panOk = allowPrimaryPointerPan(e);
5633
5657
  if (e.pointerType === "mouse" && e.button === 0) {
5634
5658
  if (!panOk) {
5635
5659
  return;
@@ -7653,6 +7677,93 @@ function PresenceRemoteLayer({
7653
7677
  );
7654
7678
  }
7655
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
+
7656
7767
  // src/react/stable-selection.ts
7657
7768
  function shallowEqualStringArray(a, b) {
7658
7769
  if (a === b) return true;
@@ -8006,13 +8117,6 @@ function isDefaultMarkerToolStyle(style) {
8006
8117
  function tagCustomPlacementItem(item, toolId) {
8007
8118
  return item.customToolId === toolId ? item : { ...item, customToolId: toolId };
8008
8119
  }
8009
- function findSelectModeItemClickPlacement(item, placements) {
8010
- const toolId = item.customToolId;
8011
- if (!toolId) return null;
8012
- return [...placements].reverse().find(
8013
- (placement) => placement.toolId === toolId && placement.onSelectModeItemClick
8014
- ) ?? null;
8015
- }
8016
8120
  function mergeToolListById(baseTools, pluginTools) {
8017
8121
  const next = [...baseTools];
8018
8122
  for (const tool of pluginTools) {
@@ -8140,9 +8244,12 @@ var VectorViewport = react.forwardRef(
8140
8244
  toolId = "hand",
8141
8245
  applePencilNav = false,
8142
8246
  interactive = false,
8247
+ readOnlyInteraction,
8143
8248
  selectedIds: selectedIdsProp,
8144
8249
  onSelectionChange,
8145
8250
  onItemsChange: consumerOnItemsChange,
8251
+ onBeforeInteraction: consumerOnBeforeInteraction,
8252
+ onAfterInteraction: consumerOnAfterInteraction,
8146
8253
  onActivateLink,
8147
8254
  onWorldPointerDown: consumerOnWorldPointerDown,
8148
8255
  toolbar,
@@ -8268,6 +8375,24 @@ var VectorViewport = react.forwardRef(
8268
8375
  (contribution) => contribution.callbacks?.onCameraChange
8269
8376
  )
8270
8377
  );
8378
+ const beforeInteractionHooks = react.useMemo(
8379
+ () => [
8380
+ consumerOnBeforeInteraction,
8381
+ ...orderedPluginContributions.map(
8382
+ (contribution) => contribution.interactionHooks?.onBeforeInteraction
8383
+ )
8384
+ ].filter((hook) => hook != null),
8385
+ [consumerOnBeforeInteraction, orderedPluginContributions]
8386
+ );
8387
+ const afterInteractionHooks = react.useMemo(
8388
+ () => [
8389
+ consumerOnAfterInteraction,
8390
+ ...orderedPluginContributions.map(
8391
+ (contribution) => contribution.interactionHooks?.onAfterInteraction
8392
+ )
8393
+ ].filter((hook) => hook != null),
8394
+ [consumerOnAfterInteraction, orderedPluginContributions]
8395
+ );
8271
8396
  const onItemsChange = react.useMemo(() => {
8272
8397
  const middlewares = orderedPluginContributions.map((contribution) => contribution.wrapOnItemsChange).filter(
8273
8398
  (middleware) => middleware != null
@@ -8322,6 +8447,8 @@ var VectorViewport = react.forwardRef(
8322
8447
  );
8323
8448
  const toolIdRef = react.useRef(toolId);
8324
8449
  const interactiveRef = react.useRef(interactive);
8450
+ const readOnlyInteractionRef = react.useRef(readOnlyInteraction);
8451
+ readOnlyInteractionRef.current = readOnlyInteraction;
8325
8452
  const reducedMotionRef = react.useRef(false);
8326
8453
  const itemsRef = react.useRef(items);
8327
8454
  const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
@@ -8335,6 +8462,7 @@ var VectorViewport = react.forwardRef(
8335
8462
  const allCustomPlacementsRef = react.useRef(allCustomPlacements);
8336
8463
  allCustomPlacementsRef.current = allCustomPlacements;
8337
8464
  const dragStateRef = react.useRef({ kind: "idle" });
8465
+ const readOnlyItemClickStateRef = react.useRef(null);
8338
8466
  const clipboardRef = react.useRef(null);
8339
8467
  const undoStackRef = react.useRef([]);
8340
8468
  const redoStackRef = react.useRef([]);
@@ -8411,6 +8539,81 @@ var VectorViewport = react.forwardRef(
8411
8539
  [items]
8412
8540
  );
8413
8541
  itemsRef.current = normalizedItems;
8542
+ const beforeInteractionHooksRef = react.useRef(beforeInteractionHooks);
8543
+ beforeInteractionHooksRef.current = beforeInteractionHooks;
8544
+ const afterInteractionHooksRef = react.useRef(afterInteractionHooks);
8545
+ afterInteractionHooksRef.current = afterInteractionHooks;
8546
+ const canvuInteractionSeqRef = react.useRef(0);
8547
+ const activeCanvuInteractionRef = react.useRef(null);
8548
+ const makeInteractionPoint = react.useCallback(
8549
+ (input) => ({
8550
+ worldX: input.worldX,
8551
+ worldY: input.worldY,
8552
+ clientX: input.clientX,
8553
+ clientY: input.clientY
8554
+ }),
8555
+ []
8556
+ );
8557
+ const startCanvuInteraction = react.useCallback(
8558
+ (input) => {
8559
+ const start = makeInteractionPoint(input);
8560
+ const detail = {
8561
+ interactionId: `canvu-interaction-${++canvuInteractionSeqRef.current}`,
8562
+ kind: input.kind,
8563
+ toolId: input.toolId,
8564
+ pointerType: input.pointerType,
8565
+ button: input.button,
8566
+ shiftKey: input.shiftKey,
8567
+ altKey: input.altKey,
8568
+ metaKey: input.metaKey,
8569
+ ctrlKey: input.ctrlKey,
8570
+ start,
8571
+ current: start,
8572
+ selectedIds: [...effectiveSelectedIdsRef.current],
8573
+ itemIds: [...input.itemIds ?? []],
8574
+ items: itemsRef.current
8575
+ };
8576
+ for (const hook of beforeInteractionHooksRef.current) {
8577
+ if (hook(detail) === "handled") {
8578
+ return null;
8579
+ }
8580
+ }
8581
+ activeCanvuInteractionRef.current = detail;
8582
+ return detail;
8583
+ },
8584
+ [makeInteractionPoint]
8585
+ );
8586
+ const updateCanvuInteractionCurrent = react.useCallback(
8587
+ (input) => {
8588
+ const detail = activeCanvuInteractionRef.current;
8589
+ if (!detail) return;
8590
+ activeCanvuInteractionRef.current = {
8591
+ ...detail,
8592
+ current: makeInteractionPoint(input)
8593
+ };
8594
+ },
8595
+ [makeInteractionPoint]
8596
+ );
8597
+ const finishCanvuInteraction = react.useCallback(
8598
+ (outcome, options) => {
8599
+ const detail = activeCanvuInteractionRef.current;
8600
+ if (!detail) return;
8601
+ activeCanvuInteractionRef.current = null;
8602
+ const current = options?.current ? makeInteractionPoint(options.current) : detail.current;
8603
+ const info = options?.info;
8604
+ const afterDetail = {
8605
+ ...detail,
8606
+ current,
8607
+ itemIds: detail.itemIds.length > 0 ? detail.itemIds : [...info?.itemIds ?? []],
8608
+ outcome,
8609
+ ...info ? { info } : {}
8610
+ };
8611
+ for (const hook of afterInteractionHooksRef.current) {
8612
+ hook(afterDetail);
8613
+ }
8614
+ },
8615
+ [makeInteractionPoint]
8616
+ );
8414
8617
  onWorldPointerDownRef.current = onWorldPointerDown;
8415
8618
  const originalOnItemsChangeRef = react.useRef(onItemsChange);
8416
8619
  originalOnItemsChangeRef.current = onItemsChange;
@@ -8437,6 +8640,31 @@ var VectorViewport = react.forwardRef(
8437
8640
  );
8438
8641
  const resolvedItemsRef = react.useRef(resolvedItems);
8439
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
+ );
8440
8668
  const liveId = react.useId();
8441
8669
  const reducedMotion = usePrefersReducedMotion();
8442
8670
  reducedMotionRef.current = reducedMotion;
@@ -8651,9 +8879,14 @@ var VectorViewport = react.forwardRef(
8651
8879
  );
8652
8880
  if (item) {
8653
8881
  const exists = itemsRef.current.some((it) => it.id === id);
8882
+ const info = {
8883
+ motive: "draw",
8884
+ itemIds: [id],
8885
+ toolId: args.tool
8886
+ };
8654
8887
  change(
8655
8888
  exists ? replaceItem(itemsRef.current, id, item) : [...itemsRef.current, item],
8656
- { motive: "draw", itemIds: [id], toolId: args.tool }
8889
+ info
8657
8890
  );
8658
8891
  patchCurrentStrokeStyle({
8659
8892
  stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
@@ -8661,10 +8894,15 @@ var VectorViewport = react.forwardRef(
8661
8894
  strokeOpacity: item.strokeOpacity,
8662
8895
  strokeDash: item.strokeDash
8663
8896
  });
8897
+ if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8898
+ requestAutoResetTool(args.tool);
8899
+ }
8900
+ return info;
8664
8901
  }
8665
8902
  if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8666
8903
  requestAutoResetTool(args.tool);
8667
8904
  }
8905
+ return void 0;
8668
8906
  },
8669
8907
  [
8670
8908
  patchCurrentStrokeStyle,
@@ -8795,17 +9033,19 @@ var VectorViewport = react.forwardRef(
8795
9033
  }
8796
9034
  emitRemoteStrokePreviewClear();
8797
9035
  setPlacementPreview(null);
8798
- commitCompletedStroke({
9036
+ const info = commitCompletedStroke({
8799
9037
  tool,
8800
9038
  pointerType,
8801
9039
  points: [...pts],
8802
9040
  style,
8803
9041
  itemId
8804
9042
  });
9043
+ finishCanvuInteraction("completed", { info });
8805
9044
  },
8806
9045
  [
8807
9046
  commitCompletedStroke,
8808
9047
  emitRemoteStrokePreviewClear,
9048
+ finishCanvuInteraction,
8809
9049
  releaseInteractionPointer,
8810
9050
  renderSceneWithLivePenStroke
8811
9051
  ]
@@ -8854,12 +9094,14 @@ var VectorViewport = react.forwardRef(
8854
9094
  onUpdate: renderFrame,
8855
9095
  wheelElement: wrapperRef.current ?? void 0,
8856
9096
  touchHandledElsewhere: applePencilNav,
8857
- allowPrimaryPointerPan: () => {
9097
+ allowPrimaryPointerPan: (event) => {
8858
9098
  if (interactiveRef.current) {
8859
9099
  return toolIdRef.current === "hand";
8860
9100
  }
8861
9101
  const t = toolIdRef.current;
8862
- return t === "hand" || t === "select";
9102
+ if (t === "hand") return true;
9103
+ if (t !== "select") return false;
9104
+ return resolveReadOnlyActivation(event) === null;
8863
9105
  }
8864
9106
  });
8865
9107
  let detachPencil;
@@ -8880,7 +9122,7 @@ var VectorViewport = react.forwardRef(
8880
9122
  cameraRef.current = null;
8881
9123
  setCameraForOverlay(null);
8882
9124
  };
8883
- }, [applePencilNav, renderFrame]);
9125
+ }, [applePencilNav, renderFrame, resolveReadOnlyActivation]);
8884
9126
  react.useEffect(() => {
8885
9127
  rendererRef.current?.setInteractionState({
8886
9128
  selectedIds: effectiveSelectedIds,
@@ -9394,6 +9636,29 @@ var VectorViewport = react.forwardRef(
9394
9636
  const rect = el.getBoundingClientRect();
9395
9637
  return cam.screenToWorld(clientX - rect.left, clientY - rect.top);
9396
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
+ );
9397
9662
  const handleOverlayContextMenu = react.useCallback(
9398
9663
  (e) => {
9399
9664
  if (!interactiveRef.current || !onItemsChangeRef.current) return;
@@ -9820,6 +10085,118 @@ var VectorViewport = react.forwardRef(
9820
10085
  },
9821
10086
  [screenToWorld]
9822
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
+ ]);
9823
10200
  const handleOverlayPointerDown = react.useCallback(
9824
10201
  (e) => {
9825
10202
  let currentDragState = dragStateRef.current;
@@ -9875,12 +10252,26 @@ var VectorViewport = react.forwardRef(
9875
10252
  if (!raw) return void 0;
9876
10253
  return raw.toolKind === "arrow" && raw.arrowBind ? bakeArrowItemToAbsolute(raw, canonical) : raw;
9877
10254
  };
10255
+ const startInteraction = (kind, itemIds) => startCanvuInteraction({
10256
+ kind,
10257
+ toolId: tool,
10258
+ pointerType: e.pointerType,
10259
+ button: e.button,
10260
+ worldX,
10261
+ worldY,
10262
+ clientX: e.clientX,
10263
+ clientY: e.clientY,
10264
+ shiftKey: e.shiftKey,
10265
+ altKey: e.altKey,
10266
+ metaKey: e.metaKey,
10267
+ ctrlKey: e.ctrlKey,
10268
+ itemIds
10269
+ });
10270
+ const stopHandledInteraction = () => {
10271
+ e.preventDefault();
10272
+ e.stopPropagation();
10273
+ };
9878
10274
  if (tool === "eraser") {
9879
- dragStateRef.current = { kind: "erase" };
9880
- eraserPreviewIdsRef.current = /* @__PURE__ */ new Set();
9881
- setEraserPreviewIds([]);
9882
- setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
9883
- setEraserActive(true);
9884
10275
  const toErase = collectEraserTargetsAtWorldPoint(
9885
10276
  resolved,
9886
10277
  worldX,
@@ -9890,6 +10281,15 @@ var VectorViewport = react.forwardRef(
9890
10281
  ignoreLocked: true
9891
10282
  }
9892
10283
  );
10284
+ if (!startInteraction("erase", toErase)) {
10285
+ stopHandledInteraction();
10286
+ return;
10287
+ }
10288
+ dragStateRef.current = { kind: "erase" };
10289
+ eraserPreviewIdsRef.current = /* @__PURE__ */ new Set();
10290
+ setEraserPreviewIds([]);
10291
+ setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
10292
+ setEraserActive(true);
9893
10293
  if (toErase.length > 0) {
9894
10294
  for (const id of toErase) {
9895
10295
  eraserPreviewIdsRef.current.add(id);
@@ -9922,6 +10322,10 @@ var VectorViewport = react.forwardRef(
9922
10322
  const snapSpin = bakedSnapshot(selected.id);
9923
10323
  if (!snapSpin) return;
9924
10324
  const pivot = itemPivotWorld(selected);
10325
+ if (!startInteraction("rotate", [selected.id])) {
10326
+ stopHandledInteraction();
10327
+ return;
10328
+ }
9925
10329
  dragStateRef.current = {
9926
10330
  kind: "rotate",
9927
10331
  id: selected.id,
@@ -9946,6 +10350,10 @@ var VectorViewport = react.forwardRef(
9946
10350
  if (hb) {
9947
10351
  const snapRs = bakedSnapshot(selected.id);
9948
10352
  if (!snapRs) return;
10353
+ if (!startInteraction("resize", [selected.id])) {
10354
+ stopHandledInteraction();
10355
+ return;
10356
+ }
9949
10357
  dragStateRef.current = {
9950
10358
  kind: "resize",
9951
10359
  id: selected.id,
@@ -9967,7 +10375,7 @@ var VectorViewport = react.forwardRef(
9967
10375
  ignoreLocked: true
9968
10376
  });
9969
10377
  if (hit) {
9970
- const selectModeClickPlacement = !e.shiftKey ? findSelectModeItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
10378
+ const selectModeClickPlacement = !e.shiftKey ? findReadOnlyItemClickPlacement(hit, allCustomPlacementsRef.current) : null;
9971
10379
  if (selectModeClickPlacement) {
9972
10380
  const isAlreadySelected = cur.includes(hit.id);
9973
10381
  const moveIds = isAlreadySelected ? [...cur] : [hit.id];
@@ -9978,6 +10386,10 @@ var VectorViewport = react.forwardRef(
9978
10386
  moveSnapshots[id] = snap;
9979
10387
  }
9980
10388
  }
10389
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10390
+ stopHandledInteraction();
10391
+ return;
10392
+ }
9981
10393
  dragStateRef.current = {
9982
10394
  kind: "select-mode-item-click",
9983
10395
  id: hit.id,
@@ -9994,7 +10406,14 @@ var VectorViewport = react.forwardRef(
9994
10406
  }
9995
10407
  if (e.shiftKey) {
9996
10408
  const next = cur.includes(hit.id) ? cur.filter((id) => id !== hit.id) : [...cur, hit.id];
10409
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10410
+ stopHandledInteraction();
10411
+ return;
10412
+ }
9997
10413
  setEffectiveSelectedIdsRef.current(next);
10414
+ finishCanvuInteraction("completed", {
10415
+ info: { motive: "custom", itemIds: [hit.id], toolId: tool }
10416
+ });
9998
10417
  e.preventDefault();
9999
10418
  e.stopPropagation();
10000
10419
  return;
@@ -10002,7 +10421,6 @@ var VectorViewport = react.forwardRef(
10002
10421
  let idsToMove;
10003
10422
  if (!cur.includes(hit.id)) {
10004
10423
  idsToMove = [hit.id];
10005
- setEffectiveSelectedIdsRef.current(idsToMove);
10006
10424
  } else {
10007
10425
  idsToMove = [...cur];
10008
10426
  }
@@ -10013,6 +10431,13 @@ var VectorViewport = react.forwardRef(
10013
10431
  snapshots[id] = snap;
10014
10432
  }
10015
10433
  }
10434
+ if (!startInteraction("move", idsToMove)) {
10435
+ stopHandledInteraction();
10436
+ return;
10437
+ }
10438
+ if (!cur.includes(hit.id)) {
10439
+ setEffectiveSelectedIdsRef.current(idsToMove);
10440
+ }
10016
10441
  dragStateRef.current = {
10017
10442
  kind: "move",
10018
10443
  ids: idsToMove,
@@ -10037,9 +10462,14 @@ var VectorViewport = react.forwardRef(
10037
10462
  }
10038
10463
  }
10039
10464
  if (Object.keys(snapshots).length > 0) {
10465
+ const moveIds = Object.keys(snapshots);
10466
+ if (!startInteraction("move", moveIds)) {
10467
+ stopHandledInteraction();
10468
+ return;
10469
+ }
10040
10470
  dragStateRef.current = {
10041
10471
  kind: "move",
10042
- ids: Object.keys(snapshots),
10472
+ ids: moveIds,
10043
10473
  snapshots,
10044
10474
  startWorld: { x: worldX, y: worldY }
10045
10475
  };
@@ -10050,6 +10480,10 @@ var VectorViewport = react.forwardRef(
10050
10480
  }
10051
10481
  }
10052
10482
  }
10483
+ if (!startInteraction("marquee")) {
10484
+ stopHandledInteraction();
10485
+ return;
10486
+ }
10053
10487
  dragStateRef.current = {
10054
10488
  kind: "marquee",
10055
10489
  startWorld: { x: worldX, y: worldY },
@@ -10078,6 +10512,10 @@ var VectorViewport = react.forwardRef(
10078
10512
  );
10079
10513
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10080
10514
  const directPenStroke = e.pointerType === "pen" && (tool === "draw" || tool === "marker");
10515
+ if (!startInteraction("stroke")) {
10516
+ stopHandledInteraction();
10517
+ return;
10518
+ }
10081
10519
  let itemId;
10082
10520
  if (directPenStroke) {
10083
10521
  itemId = createShapeId();
@@ -10121,6 +10559,10 @@ var VectorViewport = react.forwardRef(
10121
10559
  return;
10122
10560
  }
10123
10561
  if (tool === "text" || tool === "image") {
10562
+ if (!startInteraction("tap")) {
10563
+ stopHandledInteraction();
10564
+ return;
10565
+ }
10124
10566
  dragStateRef.current = {
10125
10567
  kind: "tap",
10126
10568
  tool,
@@ -10134,6 +10576,10 @@ var VectorViewport = react.forwardRef(
10134
10576
  }
10135
10577
  const cp = customPlacementRef.current;
10136
10578
  if (tool === "rect" || tool === "ellipse" || tool === "architectural-cloud" || tool === "line" || tool === "arrow" || cp && tool === cp.toolId) {
10579
+ if (!startInteraction("place")) {
10580
+ stopHandledInteraction();
10581
+ return;
10582
+ }
10137
10583
  dragStateRef.current = {
10138
10584
  kind: "place",
10139
10585
  tool,
@@ -10151,8 +10597,10 @@ var VectorViewport = react.forwardRef(
10151
10597
  captureInteractionPointer,
10152
10598
  emitRemoteStrokePreview,
10153
10599
  finalizeStrokeDragState,
10600
+ finishCanvuInteraction,
10154
10601
  renderSceneWithLivePenStroke,
10155
10602
  screenToWorld,
10603
+ startCanvuInteraction,
10156
10604
  startOrRestartStraightStrokeHoldTimer
10157
10605
  ]
10158
10606
  );
@@ -10181,14 +10629,32 @@ var VectorViewport = react.forwardRef(
10181
10629
  e.stopImmediatePropagation();
10182
10630
  return;
10183
10631
  }
10184
- wrapperRef.current?.focus({ preventScroll: true });
10185
- setContextMenu(null);
10186
10632
  const startPoint = pointerSampleToWorldPoint(
10187
10633
  screenToWorld,
10188
10634
  e.clientX,
10189
10635
  e.clientY,
10190
10636
  e.pressure
10191
10637
  );
10638
+ if (!startCanvuInteraction({
10639
+ kind: "stroke",
10640
+ toolId: tool,
10641
+ pointerType: e.pointerType,
10642
+ button: e.button,
10643
+ worldX: startPoint.x,
10644
+ worldY: startPoint.y,
10645
+ clientX: e.clientX,
10646
+ clientY: e.clientY,
10647
+ shiftKey: e.shiftKey,
10648
+ altKey: e.altKey,
10649
+ metaKey: e.metaKey,
10650
+ ctrlKey: e.ctrlKey
10651
+ })) {
10652
+ e.preventDefault();
10653
+ e.stopImmediatePropagation();
10654
+ return;
10655
+ }
10656
+ wrapperRef.current?.focus({ preventScroll: true });
10657
+ setContextMenu(null);
10192
10658
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10193
10659
  const itemId = createShapeId();
10194
10660
  const item = createFreehandStrokeItem(
@@ -10236,6 +10702,7 @@ var VectorViewport = react.forwardRef(
10236
10702
  interactive,
10237
10703
  renderSceneWithLivePenStroke,
10238
10704
  screenToWorld,
10705
+ startCanvuInteraction,
10239
10706
  startOrRestartStraightStrokeHoldTimer
10240
10707
  ]);
10241
10708
  react.useEffect(() => {
@@ -10286,15 +10753,32 @@ var VectorViewport = react.forwardRef(
10286
10753
  stopTouchEvent(ev);
10287
10754
  return;
10288
10755
  }
10289
- wrapperRef.current?.focus({ preventScroll: true });
10290
- setContextMenu(null);
10291
- penDetectedRef.current = true;
10292
10756
  const startPoint = pointerSampleToWorldPoint(
10293
10757
  screenToWorld,
10294
10758
  touch.clientX,
10295
10759
  touch.clientY,
10296
10760
  touchPressure(touch)
10297
10761
  );
10762
+ if (!startCanvuInteraction({
10763
+ kind: "stroke",
10764
+ toolId: tool,
10765
+ pointerType: "pen",
10766
+ button: 0,
10767
+ worldX: startPoint.x,
10768
+ worldY: startPoint.y,
10769
+ clientX: touch.clientX,
10770
+ clientY: touch.clientY,
10771
+ shiftKey: ev.shiftKey,
10772
+ altKey: ev.altKey,
10773
+ metaKey: ev.metaKey,
10774
+ ctrlKey: ev.ctrlKey
10775
+ })) {
10776
+ stopTouchEvent(ev);
10777
+ return;
10778
+ }
10779
+ wrapperRef.current?.focus({ preventScroll: true });
10780
+ setContextMenu(null);
10781
+ penDetectedRef.current = true;
10298
10782
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, touch.clientX, touch.clientY) : void 0;
10299
10783
  const itemId = createShapeId();
10300
10784
  const item = createFreehandStrokeItem(
@@ -10340,6 +10824,12 @@ var VectorViewport = react.forwardRef(
10340
10824
  touch.clientY,
10341
10825
  touchPressure(touch)
10342
10826
  );
10827
+ updateCanvuInteractionCurrent({
10828
+ worldX: endpoint.x,
10829
+ worldY: endpoint.y,
10830
+ clientX: touch.clientX,
10831
+ clientY: touch.clientY
10832
+ });
10343
10833
  if (updateStraightStrokeForMove(st, touch.clientX, touch.clientY, endpoint)) {
10344
10834
  debugApplePencilPointer("touchmove-stroke", {
10345
10835
  touchId: touch.identifier,
@@ -10381,16 +10871,42 @@ var VectorViewport = react.forwardRef(
10381
10871
  if (st.kind !== "stroke") return;
10382
10872
  const touch = findChangedTouch(ev.changedTouches);
10383
10873
  if (!touch) return;
10874
+ const currentPoint = pointerSampleToWorldPoint(
10875
+ screenToWorld,
10876
+ touch.clientX,
10877
+ touch.clientY,
10878
+ touchPressure(touch)
10879
+ );
10880
+ updateCanvuInteractionCurrent({
10881
+ worldX: currentPoint.x,
10882
+ worldY: currentPoint.y,
10883
+ clientX: touch.clientX,
10884
+ clientY: touch.clientY
10885
+ });
10886
+ if (ev.type === "touchcancel") {
10887
+ clearStraightStrokeHoldTimer(st);
10888
+ dragStateRef.current = { kind: "idle" };
10889
+ releaseInteractionPointer();
10890
+ if (st.itemId) {
10891
+ renderSceneWithLivePenStroke(null);
10892
+ }
10893
+ emitRemoteStrokePreviewClear();
10894
+ setPlacementPreview(null);
10895
+ finishCanvuInteraction("cancelled", {
10896
+ current: {
10897
+ worldX: currentPoint.x,
10898
+ worldY: currentPoint.y,
10899
+ clientX: touch.clientX,
10900
+ clientY: touch.clientY
10901
+ }
10902
+ });
10903
+ stopTouchEvent(ev);
10904
+ return;
10905
+ }
10384
10906
  const cam = cameraRef.current;
10385
10907
  if (cam) {
10386
10908
  if (st.straightLine?.active) {
10387
- const endpoint = pointerSampleToWorldPoint(
10388
- screenToWorld,
10389
- touch.clientX,
10390
- touch.clientY,
10391
- touchPressure(touch)
10392
- );
10393
- setStraightStrokeEndpoint(st, endpoint);
10909
+ setStraightStrokeEndpoint(st, currentPoint);
10394
10910
  } else {
10395
10911
  const completedPoints = appendTouchToStrokePoints(
10396
10912
  st.points,
@@ -10441,12 +10957,17 @@ var VectorViewport = react.forwardRef(
10441
10957
  }, [
10442
10958
  applePencilNav,
10443
10959
  emitRemoteStrokePreview,
10960
+ emitRemoteStrokePreviewClear,
10444
10961
  finalizeStrokeDragState,
10962
+ finishCanvuInteraction,
10445
10963
  interactive,
10964
+ releaseInteractionPointer,
10446
10965
  renderSceneWithLivePenStroke,
10447
10966
  screenToWorld,
10448
10967
  setStraightStrokeEndpoint,
10968
+ startCanvuInteraction,
10449
10969
  startOrRestartStraightStrokeHoldTimer,
10970
+ updateCanvuInteractionCurrent,
10450
10971
  updateStraightStrokeForMove
10451
10972
  ]);
10452
10973
  react.useEffect(() => {
@@ -10459,6 +10980,12 @@ var VectorViewport = react.forwardRef(
10459
10980
  if (st.kind === "tap") return;
10460
10981
  if (st.kind === "marquee") {
10461
10982
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
10983
+ updateCanvuInteractionCurrent({
10984
+ worldX: worldX2,
10985
+ worldY: worldY2,
10986
+ clientX: ev.clientX,
10987
+ clientY: ev.clientY
10988
+ });
10462
10989
  const raw = rectFromCorners(st.startWorld, { x: worldX2, y: worldY2 });
10463
10990
  setPlacementPreview({ kind: "marquee", rect: raw });
10464
10991
  const nextCand = collectItemIdsInRect(
@@ -10486,6 +11013,12 @@ var VectorViewport = react.forwardRef(
10486
11013
  ev.clientY,
10487
11014
  ev.pointerType === "pen" ? ev.pressure : void 0
10488
11015
  );
11016
+ updateCanvuInteractionCurrent({
11017
+ worldX: endpoint.x,
11018
+ worldY: endpoint.y,
11019
+ clientX: ev.clientX,
11020
+ clientY: ev.clientY
11021
+ });
10489
11022
  if (updateStraightStrokeForMove(st, ev.clientX, ev.clientY, endpoint)) {
10490
11023
  return;
10491
11024
  }
@@ -10534,6 +11067,12 @@ var VectorViewport = react.forwardRef(
10534
11067
  }
10535
11068
  if (st.kind === "erase") {
10536
11069
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
11070
+ updateCanvuInteractionCurrent({
11071
+ worldX: worldX2,
11072
+ worldY: worldY2,
11073
+ clientX: ev.clientX,
11074
+ clientY: ev.clientY
11075
+ });
10537
11076
  const lineHitWorld = 10 / cam.zoom;
10538
11077
  setEraserTrail(
10539
11078
  (prev) => pruneEraserTrail([...prev, { x: worldX2, y: worldY2, t: Date.now() }])
@@ -10559,6 +11098,12 @@ var VectorViewport = react.forwardRef(
10559
11098
  const change = onItemsChangeRef.current;
10560
11099
  if (!change) return;
10561
11100
  const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11101
+ updateCanvuInteractionCurrent({
11102
+ worldX,
11103
+ worldY,
11104
+ clientX: ev.clientX,
11105
+ clientY: ev.clientY
11106
+ });
10562
11107
  if (st.kind === "select-mode-item-click") {
10563
11108
  const screenDx = ev.clientX - st.startScreen.x;
10564
11109
  const screenDy = ev.clientY - st.startScreen.y;
@@ -10691,48 +11236,69 @@ var VectorViewport = react.forwardRef(
10691
11236
  setPlacementPreview(null);
10692
11237
  marqueeCandidateIdsRef.current = [];
10693
11238
  setMarqueeCandidateIds([]);
11239
+ finishCanvuInteraction("cancelled");
10694
11240
  return;
10695
11241
  }
11242
+ const finishOutcome = ev.type === "pointercancel" ? "cancelled" : "completed";
11243
+ const currentWorld = screenToWorld(ev.clientX, ev.clientY);
11244
+ const currentInteractionPoint = {
11245
+ worldX: currentWorld.worldX,
11246
+ worldY: currentWorld.worldY,
11247
+ clientX: ev.clientX,
11248
+ clientY: ev.clientY
11249
+ };
11250
+ updateCanvuInteractionCurrent(currentInteractionPoint);
10696
11251
  if (st.kind === "move" || st.kind === "resize" || st.kind === "rotate") {
11252
+ const info = st.kind === "move" ? { motive: "move", itemIds: [...st.ids] } : st.kind === "resize" ? { motive: "resize", itemIds: [st.id] } : { motive: "rotate", itemIds: [st.id] };
10697
11253
  dragStateRef.current = { kind: "idle" };
10698
11254
  releaseInteractionPointer();
11255
+ finishCanvuInteraction(finishOutcome, {
11256
+ current: currentInteractionPoint,
11257
+ ...finishOutcome === "completed" ? { info } : {}
11258
+ });
10699
11259
  return;
10700
11260
  }
10701
11261
  if (st.kind === "select-mode-item-click") {
10702
11262
  dragStateRef.current = { kind: "idle" };
10703
11263
  releaseInteractionPointer();
10704
- if (ev.type === "pointercancel") return;
11264
+ if (finishOutcome === "cancelled") {
11265
+ finishCanvuInteraction("cancelled", {
11266
+ current: currentInteractionPoint
11267
+ });
11268
+ return;
11269
+ }
10705
11270
  const dx = ev.clientX - st.startScreen.x;
10706
11271
  const dy = ev.clientY - st.startScreen.y;
10707
- if (Math.hypot(dx, dy) > TAP_PX) return;
11272
+ if (Math.hypot(dx, dy) > TAP_PX) {
11273
+ finishCanvuInteraction("cancelled", {
11274
+ current: currentInteractionPoint
11275
+ });
11276
+ return;
11277
+ }
10708
11278
  const item = itemsRef.current.find((candidate) => candidate.id === st.id) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.id);
10709
- if (!item) return;
10710
- const placement = findSelectModeItemClickPlacement(
11279
+ if (!item) {
11280
+ finishCanvuInteraction("cancelled", {
11281
+ current: currentInteractionPoint
11282
+ });
11283
+ return;
11284
+ }
11285
+ const placement = findReadOnlyItemClickPlacement(
10711
11286
  item,
10712
11287
  allCustomPlacementsRef.current
10713
11288
  );
10714
11289
  const onSelectModeItemClick = placement?.onSelectModeItemClick;
10715
- if (!onSelectModeItemClick) return;
10716
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
10717
- onSelectModeItemClick({
10718
- item,
10719
- worldX,
10720
- worldY,
10721
- clientX: ev.clientX,
10722
- clientY: ev.clientY,
10723
- pointerType: ev.pointerType,
10724
- shiftKey: ev.shiftKey,
10725
- altKey: ev.altKey,
10726
- metaKey: ev.metaKey,
10727
- ctrlKey: ev.ctrlKey,
10728
- items: itemsRef.current,
10729
- updateItem: (next) => {
10730
- onItemsChangeRef.current?.(
10731
- replaceItem(itemsRef.current, item.id, next),
10732
- { motive: "custom", itemIds: [item.id] }
10733
- );
10734
- },
10735
- setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
11290
+ if (!onSelectModeItemClick) {
11291
+ finishCanvuInteraction("cancelled", {
11292
+ current: currentInteractionPoint
11293
+ });
11294
+ return;
11295
+ }
11296
+ onSelectModeItemClick(
11297
+ buildSelectModeItemClickDetail(item, currentWorld, ev)
11298
+ );
11299
+ finishCanvuInteraction("completed", {
11300
+ current: currentInteractionPoint,
11301
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
10736
11302
  });
10737
11303
  return;
10738
11304
  }
@@ -10742,16 +11308,25 @@ var VectorViewport = react.forwardRef(
10742
11308
  setPlacementPreview(null);
10743
11309
  marqueeCandidateIdsRef.current = [];
10744
11310
  setMarqueeCandidateIds([]);
10745
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11311
+ const { worldX, worldY } = currentWorld;
10746
11312
  const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
10747
11313
  const br = normalizeRect(raw);
10748
11314
  const screenDx = ev.clientX - st.startScreen.x;
10749
11315
  const screenDy = ev.clientY - st.startScreen.y;
11316
+ if (finishOutcome === "cancelled") {
11317
+ finishCanvuInteraction("cancelled", {
11318
+ current: currentInteractionPoint
11319
+ });
11320
+ return;
11321
+ }
10750
11322
  const tooSmall = Math.hypot(screenDx, screenDy) < TAP_PX || br.width < MIN_MARQUEE_WORLD && br.height < MIN_MARQUEE_WORLD;
10751
11323
  if (tooSmall) {
10752
11324
  if (!st.shiftKey) {
10753
11325
  setEffectiveSelectedIdsRef.current([]);
10754
11326
  }
11327
+ finishCanvuInteraction("cancelled", {
11328
+ current: currentInteractionPoint
11329
+ });
10755
11330
  return;
10756
11331
  }
10757
11332
  const picked = collectItemIdsInRect(resolvedItemsRef.current, br);
@@ -10768,9 +11343,26 @@ var VectorViewport = react.forwardRef(
10768
11343
  } else {
10769
11344
  setEffectiveSelectedIdsRef.current(picked);
10770
11345
  }
11346
+ finishCanvuInteraction("completed", {
11347
+ current: currentInteractionPoint
11348
+ });
10771
11349
  return;
10772
11350
  }
10773
11351
  if (st.kind === "stroke") {
11352
+ if (finishOutcome === "cancelled") {
11353
+ clearStraightStrokeHoldTimer(st);
11354
+ dragStateRef.current = { kind: "idle" };
11355
+ releaseInteractionPointer();
11356
+ if (st.itemId) {
11357
+ renderSceneWithLivePenStroke(null);
11358
+ }
11359
+ emitRemoteStrokePreviewClear();
11360
+ setPlacementPreview(null);
11361
+ finishCanvuInteraction("cancelled", {
11362
+ current: currentInteractionPoint
11363
+ });
11364
+ return;
11365
+ }
10774
11366
  const completedPoints = (() => {
10775
11367
  if (st.straightLine?.active) {
10776
11368
  const endpoint = pointerSampleToWorldPoint(
@@ -10801,15 +11393,20 @@ var VectorViewport = react.forwardRef(
10801
11393
  }
10802
11394
  if (st.kind === "erase") {
10803
11395
  const change = onItemsChangeRef.current;
11396
+ const erasedIds = [...eraserPreviewIdsRef.current];
11397
+ const info = erasedIds.length > 0 ? {
11398
+ motive: "erase",
11399
+ itemIds: erasedIds,
11400
+ toolId: "eraser"
11401
+ } : void 0;
10804
11402
  if (change && eraserPreviewIdsRef.current.size > 0) {
10805
11403
  const idSet = new Set(eraserPreviewIdsRef.current);
10806
- change(
10807
- itemsRef.current.filter((i) => !idSet.has(i.id)),
10808
- {
10809
- motive: "erase",
10810
- itemIds: [...idSet]
10811
- }
10812
- );
11404
+ if (finishOutcome === "completed") {
11405
+ change(
11406
+ itemsRef.current.filter((i) => !idSet.has(i.id)),
11407
+ info
11408
+ );
11409
+ }
10813
11410
  }
10814
11411
  eraserPreviewIdsRef.current.clear();
10815
11412
  setEraserPreviewIds([]);
@@ -10817,7 +11414,13 @@ var VectorViewport = react.forwardRef(
10817
11414
  setEraserActive(false);
10818
11415
  dragStateRef.current = { kind: "idle" };
10819
11416
  releaseInteractionPointer();
10820
- requestAutoResetTool("eraser");
11417
+ if (finishOutcome === "completed") {
11418
+ requestAutoResetTool("eraser");
11419
+ }
11420
+ finishCanvuInteraction(finishOutcome, {
11421
+ current: currentInteractionPoint,
11422
+ ...finishOutcome === "completed" && info ? { info } : {}
11423
+ });
10821
11424
  return;
10822
11425
  }
10823
11426
  if (st.kind === "tap") {
@@ -10825,11 +11428,22 @@ var VectorViewport = react.forwardRef(
10825
11428
  const dy = ev.clientY - st.startScreen.y;
10826
11429
  dragStateRef.current = { kind: "idle" };
10827
11430
  releaseInteractionPointer();
10828
- if (Math.hypot(dx, dy) > TAP_PX) return;
11431
+ if (finishOutcome === "cancelled" || Math.hypot(dx, dy) > TAP_PX) {
11432
+ finishCanvuInteraction("cancelled", {
11433
+ current: currentInteractionPoint
11434
+ });
11435
+ return;
11436
+ }
10829
11437
  const change = onItemsChangeRef.current;
10830
- if (!change) return;
11438
+ if (!change) {
11439
+ finishCanvuInteraction("cancelled", {
11440
+ current: currentInteractionPoint
11441
+ });
11442
+ return;
11443
+ }
10831
11444
  const id = createShapeId();
10832
11445
  const { x: worldX, y: worldY } = st.startWorld;
11446
+ let info;
10833
11447
  if (st.tool === "text") {
10834
11448
  const fs = strokeStyleRef.current.textFontSize;
10835
11449
  const baseline = textBaselineYFor(fs);
@@ -10852,11 +11466,12 @@ var VectorViewport = react.forwardRef(
10852
11466
  bounds: { ...newItem.bounds }
10853
11467
  };
10854
11468
  const hidden = applyTextDraftWhileEditing(newItem, "");
10855
- change([...itemsRef.current, hidden], {
11469
+ info = {
10856
11470
  motive: "text-create",
10857
11471
  itemIds: [id],
10858
11472
  toolId: st.tool
10859
- });
11473
+ };
11474
+ change([...itemsRef.current, hidden], info);
10860
11475
  setEffectiveSelectedIdsRef.current([id]);
10861
11476
  setEditingTextId(id);
10862
11477
  setDraftText("");
@@ -10865,6 +11480,10 @@ var VectorViewport = react.forwardRef(
10865
11480
  imageInputRef.current?.click();
10866
11481
  }
10867
11482
  requestAutoResetTool(st.tool);
11483
+ finishCanvuInteraction("completed", {
11484
+ current: currentInteractionPoint,
11485
+ ...info ? { info } : {}
11486
+ });
10868
11487
  return;
10869
11488
  }
10870
11489
  if (st.kind === "place") {
@@ -10875,11 +11494,19 @@ var VectorViewport = react.forwardRef(
10875
11494
  dragStateRef.current = { kind: "idle" };
10876
11495
  releaseInteractionPointer();
10877
11496
  setPlacementPreview(null);
10878
- if (!change) return;
11497
+ if (finishOutcome === "cancelled" || !change) {
11498
+ finishCanvuInteraction("cancelled", {
11499
+ current: currentInteractionPoint
11500
+ });
11501
+ return;
11502
+ }
10879
11503
  if (st.tool === "arrow") {
10880
11504
  const screenDx = ev.clientX - st.startScreen.x;
10881
11505
  const screenDy = ev.clientY - st.startScreen.y;
10882
11506
  if (Math.hypot(screenDx, screenDy) < MIN_ARROW_DRAG_PX) {
11507
+ finishCanvuInteraction("cancelled", {
11508
+ current: currentInteractionPoint
11509
+ });
10883
11510
  return;
10884
11511
  }
10885
11512
  const maxDist = ARROW_BIND_SNAP_PX / cam.zoom;
@@ -10911,18 +11538,23 @@ var VectorViewport = react.forwardRef(
10911
11538
  ...snapB ? { end: snapB.binding } : {}
10912
11539
  };
10913
11540
  }
11541
+ const info2 = {
11542
+ motive: "place",
11543
+ itemIds: [id2],
11544
+ toolId: st.tool
11545
+ };
10914
11546
  change(
10915
11547
  [
10916
11548
  ...itemsRef.current,
10917
11549
  createLineItem(id2, rawArrow, line, "arrow", pen2, arrowBind)
10918
11550
  ],
10919
- {
10920
- motive: "place",
10921
- itemIds: [id2],
10922
- toolId: st.tool
10923
- }
11551
+ info2
10924
11552
  );
10925
11553
  setEffectiveSelectedIdsRef.current([id2]);
11554
+ finishCanvuInteraction("completed", {
11555
+ current: currentInteractionPoint,
11556
+ info: info2
11557
+ });
10926
11558
  return;
10927
11559
  }
10928
11560
  let raw = rectFromCorners(a, b);
@@ -10947,47 +11579,61 @@ var VectorViewport = react.forwardRef(
10947
11579
  }
10948
11580
  const id = createShapeId();
10949
11581
  const pen = strokeStyleRef.current;
11582
+ let info;
10950
11583
  if (cpUp && st.tool === cpUp.toolId) {
10951
11584
  const item = tagCustomPlacementItem(
10952
11585
  cpUp.createItem({ id, bounds: br }),
10953
11586
  cpUp.toolId
10954
11587
  );
10955
- change(itemsRef.current.concat(item), {
11588
+ info = {
10956
11589
  motive: "place",
10957
11590
  itemIds: [id],
10958
11591
  toolId: st.tool
10959
- });
11592
+ };
11593
+ change(itemsRef.current.concat(item), info);
10960
11594
  if (cpUp.selectAfterCreate !== false) {
10961
11595
  setEffectiveSelectedIdsRef.current([id]);
10962
11596
  }
11597
+ finishCanvuInteraction("completed", {
11598
+ current: currentInteractionPoint,
11599
+ info
11600
+ });
10963
11601
  return;
10964
11602
  }
10965
11603
  if (st.tool === "rect") {
10966
- change([...itemsRef.current, createRectangleItem(id, raw, pen)], {
11604
+ info = {
10967
11605
  motive: "place",
10968
11606
  itemIds: [id],
10969
11607
  toolId: st.tool
10970
- });
11608
+ };
11609
+ change([...itemsRef.current, createRectangleItem(id, raw, pen)], info);
10971
11610
  setEffectiveSelectedIdsRef.current([id]);
10972
11611
  } else if (st.tool === "ellipse") {
10973
- change([...itemsRef.current, createEllipseItem(id, raw, pen)], {
11612
+ info = {
10974
11613
  motive: "place",
10975
11614
  itemIds: [id],
10976
11615
  toolId: st.tool
10977
- });
11616
+ };
11617
+ change([...itemsRef.current, createEllipseItem(id, raw, pen)], info);
10978
11618
  setEffectiveSelectedIdsRef.current([id]);
10979
11619
  } else if (st.tool === "architectural-cloud") {
11620
+ info = {
11621
+ motive: "place",
11622
+ itemIds: [id],
11623
+ toolId: st.tool
11624
+ };
10980
11625
  change(
10981
11626
  [...itemsRef.current, createArchitecturalCloudItem(id, raw, pen)],
10982
- {
10983
- motive: "place",
10984
- itemIds: [id],
10985
- toolId: st.tool
10986
- }
11627
+ info
10987
11628
  );
10988
11629
  setEffectiveSelectedIdsRef.current([id]);
10989
11630
  } else if (st.tool === "line" || st.tool === "arrow") {
10990
11631
  const line = lineEndpointsToLocal(raw, lineA, lineB);
11632
+ info = {
11633
+ motive: "place",
11634
+ itemIds: [id],
11635
+ toolId: st.tool
11636
+ };
10991
11637
  change(
10992
11638
  [
10993
11639
  ...itemsRef.current,
@@ -10999,15 +11645,15 @@ var VectorViewport = react.forwardRef(
10999
11645
  pen
11000
11646
  )
11001
11647
  ],
11002
- {
11003
- motive: "place",
11004
- itemIds: [id],
11005
- toolId: st.tool
11006
- }
11648
+ info
11007
11649
  );
11008
11650
  setEffectiveSelectedIdsRef.current([id]);
11009
11651
  }
11010
11652
  requestAutoResetTool(st.tool);
11653
+ finishCanvuInteraction("completed", {
11654
+ current: currentInteractionPoint,
11655
+ ...info ? { info } : {}
11656
+ });
11011
11657
  }
11012
11658
  };
11013
11659
  document.addEventListener("pointermove", onMove);
@@ -11019,17 +11665,20 @@ var VectorViewport = react.forwardRef(
11019
11665
  document.removeEventListener("pointercancel", onUp);
11020
11666
  };
11021
11667
  }, [
11668
+ buildSelectModeItemClickDetail,
11022
11669
  emitRemoteStrokePreview,
11023
11670
  emitRemoteStrokePreviewClear,
11024
11671
  interactive,
11025
11672
  pruneEraserTrail,
11026
11673
  pruneLaserTrail,
11027
11674
  finalizeStrokeDragState,
11675
+ finishCanvuInteraction,
11028
11676
  renderSceneWithLivePenStroke,
11029
11677
  releaseInteractionPointer,
11030
11678
  requestAutoResetTool,
11031
11679
  screenToWorld,
11032
11680
  setStraightStrokeEndpoint,
11681
+ updateCanvuInteractionCurrent,
11033
11682
  updateStraightStrokeForMove
11034
11683
  ]);
11035
11684
  const selectedItemsForOverlay = react.useMemo(() => {