canvu-react 0.4.62 → 0.4.64

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
@@ -2119,7 +2119,7 @@ var collapseButtonStyle = {
2119
2119
  cursor: "pointer",
2120
2120
  padding: 0
2121
2121
  };
2122
- var collapsedButtonStyle = {
2122
+ var defaultCollapsedButtonStyle = {
2123
2123
  display: "inline-flex",
2124
2124
  alignItems: "center",
2125
2125
  justifyContent: "center",
@@ -2148,7 +2148,10 @@ function ImagesMenu({
2148
2148
  onItemsChange,
2149
2149
  onFocusItem,
2150
2150
  labels,
2151
- defaultOpen = false
2151
+ defaultOpen = false,
2152
+ collapsedButtonClassName,
2153
+ collapsedButtonStyle,
2154
+ renderCollapsedButton
2152
2155
  }) {
2153
2156
  const managed = react.useMemo(() => items.filter(isManagedImage), [items]);
2154
2157
  const sensors = core.useSensors(
@@ -2169,18 +2172,26 @@ function ImagesMenu({
2169
2172
  duplicate: labels?.duplicate ?? labels?.copy ?? defaultLabels.duplicate
2170
2173
  };
2171
2174
  if (collapsed) {
2172
- return /* @__PURE__ */ jsxRuntime.jsx(
2173
- "button",
2174
- {
2175
- type: "button",
2176
- "data-slot": "images-menu-collapsed",
2177
- style: collapsedButtonStyle,
2178
- "aria-label": resolvedLabels.expand,
2179
- title: resolvedLabels.expand,
2180
- onClick: () => setCollapsed(false),
2181
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Images, { size: 20 })
2182
- }
2183
- );
2175
+ const buttonProps = {
2176
+ type: "button",
2177
+ "data-slot": "images-menu-collapsed",
2178
+ className: collapsedButtonClassName,
2179
+ style: {
2180
+ ...defaultCollapsedButtonStyle,
2181
+ ...collapsedButtonStyle ?? {}
2182
+ },
2183
+ "aria-label": resolvedLabels.expand,
2184
+ title: resolvedLabels.expand,
2185
+ onClick: () => setCollapsed(false)
2186
+ };
2187
+ if (renderCollapsedButton) {
2188
+ return renderCollapsedButton({
2189
+ count: managed.length,
2190
+ label: resolvedLabels.expand,
2191
+ buttonProps
2192
+ });
2193
+ }
2194
+ return /* @__PURE__ */ jsxRuntime.jsx("button", { ...buttonProps, children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Images, { size: 20 }) });
2184
2195
  }
2185
2196
  const onDragEnd = (event) => {
2186
2197
  const { active, over } = event;
@@ -3142,7 +3153,9 @@ function ToolPluginComponent({
3142
3153
  toolTransform,
3143
3154
  createItem,
3144
3155
  selectAfterCreate,
3145
- onSelectModeItemClick
3156
+ onSelectModeItemClick,
3157
+ onBeforeInteraction,
3158
+ onAfterInteraction
3146
3159
  }) {
3147
3160
  const contribution = react.useMemo(
3148
3161
  () => ({
@@ -3155,9 +3168,27 @@ function ToolPluginComponent({
3155
3168
  selectAfterCreate,
3156
3169
  onSelectModeItemClick
3157
3170
  }
3158
- ] : 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
3159
3182
  }),
3160
- [createItem, onSelectModeItemClick, selectAfterCreate, tool, toolTransform]
3183
+ [
3184
+ createItem,
3185
+ onAfterInteraction,
3186
+ onBeforeInteraction,
3187
+ onSelectModeItemClick,
3188
+ selectAfterCreate,
3189
+ tool,
3190
+ toolTransform
3191
+ ]
3161
3192
  );
3162
3193
  useCanvuPluginContribution(pluginId, contribution);
3163
3194
  return null;
@@ -3168,6 +3199,8 @@ function createToolPlugin(options) {
3168
3199
  toolTransform,
3169
3200
  selectAfterCreate,
3170
3201
  onSelectModeItemClick,
3202
+ onBeforeInteraction,
3203
+ onAfterInteraction,
3171
3204
  ...tool
3172
3205
  } = options;
3173
3206
  const pluginId = `canvu.plugin.tool:${tool.id}`;
@@ -3182,7 +3215,9 @@ function createToolPlugin(options) {
3182
3215
  toolTransform,
3183
3216
  createItem,
3184
3217
  selectAfterCreate,
3185
- onSelectModeItemClick
3218
+ onSelectModeItemClick,
3219
+ onBeforeInteraction,
3220
+ onAfterInteraction
3186
3221
  }
3187
3222
  );
3188
3223
  }
@@ -8132,6 +8167,8 @@ var VectorViewport = react.forwardRef(
8132
8167
  selectedIds: selectedIdsProp,
8133
8168
  onSelectionChange,
8134
8169
  onItemsChange: consumerOnItemsChange,
8170
+ onBeforeInteraction: consumerOnBeforeInteraction,
8171
+ onAfterInteraction: consumerOnAfterInteraction,
8135
8172
  onActivateLink,
8136
8173
  onWorldPointerDown: consumerOnWorldPointerDown,
8137
8174
  toolbar,
@@ -8257,6 +8294,24 @@ var VectorViewport = react.forwardRef(
8257
8294
  (contribution) => contribution.callbacks?.onCameraChange
8258
8295
  )
8259
8296
  );
8297
+ const beforeInteractionHooks = react.useMemo(
8298
+ () => [
8299
+ consumerOnBeforeInteraction,
8300
+ ...orderedPluginContributions.map(
8301
+ (contribution) => contribution.interactionHooks?.onBeforeInteraction
8302
+ )
8303
+ ].filter((hook) => hook != null),
8304
+ [consumerOnBeforeInteraction, orderedPluginContributions]
8305
+ );
8306
+ const afterInteractionHooks = react.useMemo(
8307
+ () => [
8308
+ consumerOnAfterInteraction,
8309
+ ...orderedPluginContributions.map(
8310
+ (contribution) => contribution.interactionHooks?.onAfterInteraction
8311
+ )
8312
+ ].filter((hook) => hook != null),
8313
+ [consumerOnAfterInteraction, orderedPluginContributions]
8314
+ );
8260
8315
  const onItemsChange = react.useMemo(() => {
8261
8316
  const middlewares = orderedPluginContributions.map((contribution) => contribution.wrapOnItemsChange).filter(
8262
8317
  (middleware) => middleware != null
@@ -8400,6 +8455,81 @@ var VectorViewport = react.forwardRef(
8400
8455
  [items]
8401
8456
  );
8402
8457
  itemsRef.current = normalizedItems;
8458
+ const beforeInteractionHooksRef = react.useRef(beforeInteractionHooks);
8459
+ beforeInteractionHooksRef.current = beforeInteractionHooks;
8460
+ const afterInteractionHooksRef = react.useRef(afterInteractionHooks);
8461
+ afterInteractionHooksRef.current = afterInteractionHooks;
8462
+ const canvuInteractionSeqRef = react.useRef(0);
8463
+ const activeCanvuInteractionRef = react.useRef(null);
8464
+ const makeInteractionPoint = react.useCallback(
8465
+ (input) => ({
8466
+ worldX: input.worldX,
8467
+ worldY: input.worldY,
8468
+ clientX: input.clientX,
8469
+ clientY: input.clientY
8470
+ }),
8471
+ []
8472
+ );
8473
+ const startCanvuInteraction = react.useCallback(
8474
+ (input) => {
8475
+ const start = makeInteractionPoint(input);
8476
+ const detail = {
8477
+ interactionId: `canvu-interaction-${++canvuInteractionSeqRef.current}`,
8478
+ kind: input.kind,
8479
+ toolId: input.toolId,
8480
+ pointerType: input.pointerType,
8481
+ button: input.button,
8482
+ shiftKey: input.shiftKey,
8483
+ altKey: input.altKey,
8484
+ metaKey: input.metaKey,
8485
+ ctrlKey: input.ctrlKey,
8486
+ start,
8487
+ current: start,
8488
+ selectedIds: [...effectiveSelectedIdsRef.current],
8489
+ itemIds: [...input.itemIds ?? []],
8490
+ items: itemsRef.current
8491
+ };
8492
+ for (const hook of beforeInteractionHooksRef.current) {
8493
+ if (hook(detail) === "handled") {
8494
+ return null;
8495
+ }
8496
+ }
8497
+ activeCanvuInteractionRef.current = detail;
8498
+ return detail;
8499
+ },
8500
+ [makeInteractionPoint]
8501
+ );
8502
+ const updateCanvuInteractionCurrent = react.useCallback(
8503
+ (input) => {
8504
+ const detail = activeCanvuInteractionRef.current;
8505
+ if (!detail) return;
8506
+ activeCanvuInteractionRef.current = {
8507
+ ...detail,
8508
+ current: makeInteractionPoint(input)
8509
+ };
8510
+ },
8511
+ [makeInteractionPoint]
8512
+ );
8513
+ const finishCanvuInteraction = react.useCallback(
8514
+ (outcome, options) => {
8515
+ const detail = activeCanvuInteractionRef.current;
8516
+ if (!detail) return;
8517
+ activeCanvuInteractionRef.current = null;
8518
+ const current = options?.current ? makeInteractionPoint(options.current) : detail.current;
8519
+ const info = options?.info;
8520
+ const afterDetail = {
8521
+ ...detail,
8522
+ current,
8523
+ itemIds: detail.itemIds.length > 0 ? detail.itemIds : [...info?.itemIds ?? []],
8524
+ outcome,
8525
+ ...info ? { info } : {}
8526
+ };
8527
+ for (const hook of afterInteractionHooksRef.current) {
8528
+ hook(afterDetail);
8529
+ }
8530
+ },
8531
+ [makeInteractionPoint]
8532
+ );
8403
8533
  onWorldPointerDownRef.current = onWorldPointerDown;
8404
8534
  const originalOnItemsChangeRef = react.useRef(onItemsChange);
8405
8535
  originalOnItemsChangeRef.current = onItemsChange;
@@ -8640,9 +8770,14 @@ var VectorViewport = react.forwardRef(
8640
8770
  );
8641
8771
  if (item) {
8642
8772
  const exists = itemsRef.current.some((it) => it.id === id);
8773
+ const info = {
8774
+ motive: "draw",
8775
+ itemIds: [id],
8776
+ toolId: args.tool
8777
+ };
8643
8778
  change(
8644
8779
  exists ? replaceItem(itemsRef.current, id, item) : [...itemsRef.current, item],
8645
- { motive: "draw", itemIds: [id], toolId: args.tool }
8780
+ info
8646
8781
  );
8647
8782
  patchCurrentStrokeStyle({
8648
8783
  stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
@@ -8650,10 +8785,15 @@ var VectorViewport = react.forwardRef(
8650
8785
  strokeOpacity: item.strokeOpacity,
8651
8786
  strokeDash: item.strokeDash
8652
8787
  });
8788
+ if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8789
+ requestAutoResetTool(args.tool);
8790
+ }
8791
+ return info;
8653
8792
  }
8654
8793
  if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8655
8794
  requestAutoResetTool(args.tool);
8656
8795
  }
8796
+ return void 0;
8657
8797
  },
8658
8798
  [
8659
8799
  patchCurrentStrokeStyle,
@@ -8784,17 +8924,19 @@ var VectorViewport = react.forwardRef(
8784
8924
  }
8785
8925
  emitRemoteStrokePreviewClear();
8786
8926
  setPlacementPreview(null);
8787
- commitCompletedStroke({
8927
+ const info = commitCompletedStroke({
8788
8928
  tool,
8789
8929
  pointerType,
8790
8930
  points: [...pts],
8791
8931
  style,
8792
8932
  itemId
8793
8933
  });
8934
+ finishCanvuInteraction("completed", { info });
8794
8935
  },
8795
8936
  [
8796
8937
  commitCompletedStroke,
8797
8938
  emitRemoteStrokePreviewClear,
8939
+ finishCanvuInteraction,
8798
8940
  releaseInteractionPointer,
8799
8941
  renderSceneWithLivePenStroke
8800
8942
  ]
@@ -9864,12 +10006,26 @@ var VectorViewport = react.forwardRef(
9864
10006
  if (!raw) return void 0;
9865
10007
  return raw.toolKind === "arrow" && raw.arrowBind ? bakeArrowItemToAbsolute(raw, canonical) : raw;
9866
10008
  };
10009
+ const startInteraction = (kind, itemIds) => startCanvuInteraction({
10010
+ kind,
10011
+ toolId: tool,
10012
+ pointerType: e.pointerType,
10013
+ button: e.button,
10014
+ worldX,
10015
+ worldY,
10016
+ clientX: e.clientX,
10017
+ clientY: e.clientY,
10018
+ shiftKey: e.shiftKey,
10019
+ altKey: e.altKey,
10020
+ metaKey: e.metaKey,
10021
+ ctrlKey: e.ctrlKey,
10022
+ itemIds
10023
+ });
10024
+ const stopHandledInteraction = () => {
10025
+ e.preventDefault();
10026
+ e.stopPropagation();
10027
+ };
9867
10028
  if (tool === "eraser") {
9868
- dragStateRef.current = { kind: "erase" };
9869
- eraserPreviewIdsRef.current = /* @__PURE__ */ new Set();
9870
- setEraserPreviewIds([]);
9871
- setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
9872
- setEraserActive(true);
9873
10029
  const toErase = collectEraserTargetsAtWorldPoint(
9874
10030
  resolved,
9875
10031
  worldX,
@@ -9879,6 +10035,15 @@ var VectorViewport = react.forwardRef(
9879
10035
  ignoreLocked: true
9880
10036
  }
9881
10037
  );
10038
+ if (!startInteraction("erase", toErase)) {
10039
+ stopHandledInteraction();
10040
+ return;
10041
+ }
10042
+ dragStateRef.current = { kind: "erase" };
10043
+ eraserPreviewIdsRef.current = /* @__PURE__ */ new Set();
10044
+ setEraserPreviewIds([]);
10045
+ setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
10046
+ setEraserActive(true);
9882
10047
  if (toErase.length > 0) {
9883
10048
  for (const id of toErase) {
9884
10049
  eraserPreviewIdsRef.current.add(id);
@@ -9911,6 +10076,10 @@ var VectorViewport = react.forwardRef(
9911
10076
  const snapSpin = bakedSnapshot(selected.id);
9912
10077
  if (!snapSpin) return;
9913
10078
  const pivot = itemPivotWorld(selected);
10079
+ if (!startInteraction("rotate", [selected.id])) {
10080
+ stopHandledInteraction();
10081
+ return;
10082
+ }
9914
10083
  dragStateRef.current = {
9915
10084
  kind: "rotate",
9916
10085
  id: selected.id,
@@ -9935,6 +10104,10 @@ var VectorViewport = react.forwardRef(
9935
10104
  if (hb) {
9936
10105
  const snapRs = bakedSnapshot(selected.id);
9937
10106
  if (!snapRs) return;
10107
+ if (!startInteraction("resize", [selected.id])) {
10108
+ stopHandledInteraction();
10109
+ return;
10110
+ }
9938
10111
  dragStateRef.current = {
9939
10112
  kind: "resize",
9940
10113
  id: selected.id,
@@ -9967,6 +10140,10 @@ var VectorViewport = react.forwardRef(
9967
10140
  moveSnapshots[id] = snap;
9968
10141
  }
9969
10142
  }
10143
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10144
+ stopHandledInteraction();
10145
+ return;
10146
+ }
9970
10147
  dragStateRef.current = {
9971
10148
  kind: "select-mode-item-click",
9972
10149
  id: hit.id,
@@ -9983,7 +10160,14 @@ var VectorViewport = react.forwardRef(
9983
10160
  }
9984
10161
  if (e.shiftKey) {
9985
10162
  const next = cur.includes(hit.id) ? cur.filter((id) => id !== hit.id) : [...cur, hit.id];
10163
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10164
+ stopHandledInteraction();
10165
+ return;
10166
+ }
9986
10167
  setEffectiveSelectedIdsRef.current(next);
10168
+ finishCanvuInteraction("completed", {
10169
+ info: { motive: "custom", itemIds: [hit.id], toolId: tool }
10170
+ });
9987
10171
  e.preventDefault();
9988
10172
  e.stopPropagation();
9989
10173
  return;
@@ -9991,7 +10175,6 @@ var VectorViewport = react.forwardRef(
9991
10175
  let idsToMove;
9992
10176
  if (!cur.includes(hit.id)) {
9993
10177
  idsToMove = [hit.id];
9994
- setEffectiveSelectedIdsRef.current(idsToMove);
9995
10178
  } else {
9996
10179
  idsToMove = [...cur];
9997
10180
  }
@@ -10002,6 +10185,13 @@ var VectorViewport = react.forwardRef(
10002
10185
  snapshots[id] = snap;
10003
10186
  }
10004
10187
  }
10188
+ if (!startInteraction("move", idsToMove)) {
10189
+ stopHandledInteraction();
10190
+ return;
10191
+ }
10192
+ if (!cur.includes(hit.id)) {
10193
+ setEffectiveSelectedIdsRef.current(idsToMove);
10194
+ }
10005
10195
  dragStateRef.current = {
10006
10196
  kind: "move",
10007
10197
  ids: idsToMove,
@@ -10026,9 +10216,14 @@ var VectorViewport = react.forwardRef(
10026
10216
  }
10027
10217
  }
10028
10218
  if (Object.keys(snapshots).length > 0) {
10219
+ const moveIds = Object.keys(snapshots);
10220
+ if (!startInteraction("move", moveIds)) {
10221
+ stopHandledInteraction();
10222
+ return;
10223
+ }
10029
10224
  dragStateRef.current = {
10030
10225
  kind: "move",
10031
- ids: Object.keys(snapshots),
10226
+ ids: moveIds,
10032
10227
  snapshots,
10033
10228
  startWorld: { x: worldX, y: worldY }
10034
10229
  };
@@ -10039,6 +10234,10 @@ var VectorViewport = react.forwardRef(
10039
10234
  }
10040
10235
  }
10041
10236
  }
10237
+ if (!startInteraction("marquee")) {
10238
+ stopHandledInteraction();
10239
+ return;
10240
+ }
10042
10241
  dragStateRef.current = {
10043
10242
  kind: "marquee",
10044
10243
  startWorld: { x: worldX, y: worldY },
@@ -10067,6 +10266,10 @@ var VectorViewport = react.forwardRef(
10067
10266
  );
10068
10267
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10069
10268
  const directPenStroke = e.pointerType === "pen" && (tool === "draw" || tool === "marker");
10269
+ if (!startInteraction("stroke")) {
10270
+ stopHandledInteraction();
10271
+ return;
10272
+ }
10070
10273
  let itemId;
10071
10274
  if (directPenStroke) {
10072
10275
  itemId = createShapeId();
@@ -10110,6 +10313,10 @@ var VectorViewport = react.forwardRef(
10110
10313
  return;
10111
10314
  }
10112
10315
  if (tool === "text" || tool === "image") {
10316
+ if (!startInteraction("tap")) {
10317
+ stopHandledInteraction();
10318
+ return;
10319
+ }
10113
10320
  dragStateRef.current = {
10114
10321
  kind: "tap",
10115
10322
  tool,
@@ -10123,6 +10330,10 @@ var VectorViewport = react.forwardRef(
10123
10330
  }
10124
10331
  const cp = customPlacementRef.current;
10125
10332
  if (tool === "rect" || tool === "ellipse" || tool === "architectural-cloud" || tool === "line" || tool === "arrow" || cp && tool === cp.toolId) {
10333
+ if (!startInteraction("place")) {
10334
+ stopHandledInteraction();
10335
+ return;
10336
+ }
10126
10337
  dragStateRef.current = {
10127
10338
  kind: "place",
10128
10339
  tool,
@@ -10140,8 +10351,10 @@ var VectorViewport = react.forwardRef(
10140
10351
  captureInteractionPointer,
10141
10352
  emitRemoteStrokePreview,
10142
10353
  finalizeStrokeDragState,
10354
+ finishCanvuInteraction,
10143
10355
  renderSceneWithLivePenStroke,
10144
10356
  screenToWorld,
10357
+ startCanvuInteraction,
10145
10358
  startOrRestartStraightStrokeHoldTimer
10146
10359
  ]
10147
10360
  );
@@ -10170,14 +10383,32 @@ var VectorViewport = react.forwardRef(
10170
10383
  e.stopImmediatePropagation();
10171
10384
  return;
10172
10385
  }
10173
- wrapperRef.current?.focus({ preventScroll: true });
10174
- setContextMenu(null);
10175
10386
  const startPoint = pointerSampleToWorldPoint(
10176
10387
  screenToWorld,
10177
10388
  e.clientX,
10178
10389
  e.clientY,
10179
10390
  e.pressure
10180
10391
  );
10392
+ if (!startCanvuInteraction({
10393
+ kind: "stroke",
10394
+ toolId: tool,
10395
+ pointerType: e.pointerType,
10396
+ button: e.button,
10397
+ worldX: startPoint.x,
10398
+ worldY: startPoint.y,
10399
+ clientX: e.clientX,
10400
+ clientY: e.clientY,
10401
+ shiftKey: e.shiftKey,
10402
+ altKey: e.altKey,
10403
+ metaKey: e.metaKey,
10404
+ ctrlKey: e.ctrlKey
10405
+ })) {
10406
+ e.preventDefault();
10407
+ e.stopImmediatePropagation();
10408
+ return;
10409
+ }
10410
+ wrapperRef.current?.focus({ preventScroll: true });
10411
+ setContextMenu(null);
10181
10412
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10182
10413
  const itemId = createShapeId();
10183
10414
  const item = createFreehandStrokeItem(
@@ -10225,6 +10456,7 @@ var VectorViewport = react.forwardRef(
10225
10456
  interactive,
10226
10457
  renderSceneWithLivePenStroke,
10227
10458
  screenToWorld,
10459
+ startCanvuInteraction,
10228
10460
  startOrRestartStraightStrokeHoldTimer
10229
10461
  ]);
10230
10462
  react.useEffect(() => {
@@ -10275,15 +10507,32 @@ var VectorViewport = react.forwardRef(
10275
10507
  stopTouchEvent(ev);
10276
10508
  return;
10277
10509
  }
10278
- wrapperRef.current?.focus({ preventScroll: true });
10279
- setContextMenu(null);
10280
- penDetectedRef.current = true;
10281
10510
  const startPoint = pointerSampleToWorldPoint(
10282
10511
  screenToWorld,
10283
10512
  touch.clientX,
10284
10513
  touch.clientY,
10285
10514
  touchPressure(touch)
10286
10515
  );
10516
+ if (!startCanvuInteraction({
10517
+ kind: "stroke",
10518
+ toolId: tool,
10519
+ pointerType: "pen",
10520
+ button: 0,
10521
+ worldX: startPoint.x,
10522
+ worldY: startPoint.y,
10523
+ clientX: touch.clientX,
10524
+ clientY: touch.clientY,
10525
+ shiftKey: ev.shiftKey,
10526
+ altKey: ev.altKey,
10527
+ metaKey: ev.metaKey,
10528
+ ctrlKey: ev.ctrlKey
10529
+ })) {
10530
+ stopTouchEvent(ev);
10531
+ return;
10532
+ }
10533
+ wrapperRef.current?.focus({ preventScroll: true });
10534
+ setContextMenu(null);
10535
+ penDetectedRef.current = true;
10287
10536
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, touch.clientX, touch.clientY) : void 0;
10288
10537
  const itemId = createShapeId();
10289
10538
  const item = createFreehandStrokeItem(
@@ -10329,6 +10578,12 @@ var VectorViewport = react.forwardRef(
10329
10578
  touch.clientY,
10330
10579
  touchPressure(touch)
10331
10580
  );
10581
+ updateCanvuInteractionCurrent({
10582
+ worldX: endpoint.x,
10583
+ worldY: endpoint.y,
10584
+ clientX: touch.clientX,
10585
+ clientY: touch.clientY
10586
+ });
10332
10587
  if (updateStraightStrokeForMove(st, touch.clientX, touch.clientY, endpoint)) {
10333
10588
  debugApplePencilPointer("touchmove-stroke", {
10334
10589
  touchId: touch.identifier,
@@ -10370,16 +10625,42 @@ var VectorViewport = react.forwardRef(
10370
10625
  if (st.kind !== "stroke") return;
10371
10626
  const touch = findChangedTouch(ev.changedTouches);
10372
10627
  if (!touch) return;
10628
+ const currentPoint = pointerSampleToWorldPoint(
10629
+ screenToWorld,
10630
+ touch.clientX,
10631
+ touch.clientY,
10632
+ touchPressure(touch)
10633
+ );
10634
+ updateCanvuInteractionCurrent({
10635
+ worldX: currentPoint.x,
10636
+ worldY: currentPoint.y,
10637
+ clientX: touch.clientX,
10638
+ clientY: touch.clientY
10639
+ });
10640
+ if (ev.type === "touchcancel") {
10641
+ clearStraightStrokeHoldTimer(st);
10642
+ dragStateRef.current = { kind: "idle" };
10643
+ releaseInteractionPointer();
10644
+ if (st.itemId) {
10645
+ renderSceneWithLivePenStroke(null);
10646
+ }
10647
+ emitRemoteStrokePreviewClear();
10648
+ setPlacementPreview(null);
10649
+ finishCanvuInteraction("cancelled", {
10650
+ current: {
10651
+ worldX: currentPoint.x,
10652
+ worldY: currentPoint.y,
10653
+ clientX: touch.clientX,
10654
+ clientY: touch.clientY
10655
+ }
10656
+ });
10657
+ stopTouchEvent(ev);
10658
+ return;
10659
+ }
10373
10660
  const cam = cameraRef.current;
10374
10661
  if (cam) {
10375
10662
  if (st.straightLine?.active) {
10376
- const endpoint = pointerSampleToWorldPoint(
10377
- screenToWorld,
10378
- touch.clientX,
10379
- touch.clientY,
10380
- touchPressure(touch)
10381
- );
10382
- setStraightStrokeEndpoint(st, endpoint);
10663
+ setStraightStrokeEndpoint(st, currentPoint);
10383
10664
  } else {
10384
10665
  const completedPoints = appendTouchToStrokePoints(
10385
10666
  st.points,
@@ -10430,12 +10711,17 @@ var VectorViewport = react.forwardRef(
10430
10711
  }, [
10431
10712
  applePencilNav,
10432
10713
  emitRemoteStrokePreview,
10714
+ emitRemoteStrokePreviewClear,
10433
10715
  finalizeStrokeDragState,
10716
+ finishCanvuInteraction,
10434
10717
  interactive,
10718
+ releaseInteractionPointer,
10435
10719
  renderSceneWithLivePenStroke,
10436
10720
  screenToWorld,
10437
10721
  setStraightStrokeEndpoint,
10722
+ startCanvuInteraction,
10438
10723
  startOrRestartStraightStrokeHoldTimer,
10724
+ updateCanvuInteractionCurrent,
10439
10725
  updateStraightStrokeForMove
10440
10726
  ]);
10441
10727
  react.useEffect(() => {
@@ -10448,6 +10734,12 @@ var VectorViewport = react.forwardRef(
10448
10734
  if (st.kind === "tap") return;
10449
10735
  if (st.kind === "marquee") {
10450
10736
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
10737
+ updateCanvuInteractionCurrent({
10738
+ worldX: worldX2,
10739
+ worldY: worldY2,
10740
+ clientX: ev.clientX,
10741
+ clientY: ev.clientY
10742
+ });
10451
10743
  const raw = rectFromCorners(st.startWorld, { x: worldX2, y: worldY2 });
10452
10744
  setPlacementPreview({ kind: "marquee", rect: raw });
10453
10745
  const nextCand = collectItemIdsInRect(
@@ -10475,6 +10767,12 @@ var VectorViewport = react.forwardRef(
10475
10767
  ev.clientY,
10476
10768
  ev.pointerType === "pen" ? ev.pressure : void 0
10477
10769
  );
10770
+ updateCanvuInteractionCurrent({
10771
+ worldX: endpoint.x,
10772
+ worldY: endpoint.y,
10773
+ clientX: ev.clientX,
10774
+ clientY: ev.clientY
10775
+ });
10478
10776
  if (updateStraightStrokeForMove(st, ev.clientX, ev.clientY, endpoint)) {
10479
10777
  return;
10480
10778
  }
@@ -10523,6 +10821,12 @@ var VectorViewport = react.forwardRef(
10523
10821
  }
10524
10822
  if (st.kind === "erase") {
10525
10823
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
10824
+ updateCanvuInteractionCurrent({
10825
+ worldX: worldX2,
10826
+ worldY: worldY2,
10827
+ clientX: ev.clientX,
10828
+ clientY: ev.clientY
10829
+ });
10526
10830
  const lineHitWorld = 10 / cam.zoom;
10527
10831
  setEraserTrail(
10528
10832
  (prev) => pruneEraserTrail([...prev, { x: worldX2, y: worldY2, t: Date.now() }])
@@ -10548,6 +10852,12 @@ var VectorViewport = react.forwardRef(
10548
10852
  const change = onItemsChangeRef.current;
10549
10853
  if (!change) return;
10550
10854
  const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
10855
+ updateCanvuInteractionCurrent({
10856
+ worldX,
10857
+ worldY,
10858
+ clientX: ev.clientX,
10859
+ clientY: ev.clientY
10860
+ });
10551
10861
  if (st.kind === "select-mode-item-click") {
10552
10862
  const screenDx = ev.clientX - st.startScreen.x;
10553
10863
  const screenDy = ev.clientY - st.startScreen.y;
@@ -10680,29 +10990,64 @@ var VectorViewport = react.forwardRef(
10680
10990
  setPlacementPreview(null);
10681
10991
  marqueeCandidateIdsRef.current = [];
10682
10992
  setMarqueeCandidateIds([]);
10993
+ finishCanvuInteraction("cancelled");
10683
10994
  return;
10684
10995
  }
10996
+ const finishOutcome = ev.type === "pointercancel" ? "cancelled" : "completed";
10997
+ const currentWorld = screenToWorld(ev.clientX, ev.clientY);
10998
+ const currentInteractionPoint = {
10999
+ worldX: currentWorld.worldX,
11000
+ worldY: currentWorld.worldY,
11001
+ clientX: ev.clientX,
11002
+ clientY: ev.clientY
11003
+ };
11004
+ updateCanvuInteractionCurrent(currentInteractionPoint);
10685
11005
  if (st.kind === "move" || st.kind === "resize" || st.kind === "rotate") {
11006
+ const info = st.kind === "move" ? { motive: "move", itemIds: [...st.ids] } : st.kind === "resize" ? { motive: "resize", itemIds: [st.id] } : { motive: "rotate", itemIds: [st.id] };
10686
11007
  dragStateRef.current = { kind: "idle" };
10687
11008
  releaseInteractionPointer();
11009
+ finishCanvuInteraction(finishOutcome, {
11010
+ current: currentInteractionPoint,
11011
+ ...finishOutcome === "completed" ? { info } : {}
11012
+ });
10688
11013
  return;
10689
11014
  }
10690
11015
  if (st.kind === "select-mode-item-click") {
10691
11016
  dragStateRef.current = { kind: "idle" };
10692
11017
  releaseInteractionPointer();
10693
- if (ev.type === "pointercancel") return;
11018
+ if (finishOutcome === "cancelled") {
11019
+ finishCanvuInteraction("cancelled", {
11020
+ current: currentInteractionPoint
11021
+ });
11022
+ return;
11023
+ }
10694
11024
  const dx = ev.clientX - st.startScreen.x;
10695
11025
  const dy = ev.clientY - st.startScreen.y;
10696
- if (Math.hypot(dx, dy) > TAP_PX) return;
11026
+ if (Math.hypot(dx, dy) > TAP_PX) {
11027
+ finishCanvuInteraction("cancelled", {
11028
+ current: currentInteractionPoint
11029
+ });
11030
+ return;
11031
+ }
10697
11032
  const item = itemsRef.current.find((candidate) => candidate.id === st.id) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.id);
10698
- if (!item) return;
11033
+ if (!item) {
11034
+ finishCanvuInteraction("cancelled", {
11035
+ current: currentInteractionPoint
11036
+ });
11037
+ return;
11038
+ }
10699
11039
  const placement = findSelectModeItemClickPlacement(
10700
11040
  item,
10701
11041
  allCustomPlacementsRef.current
10702
11042
  );
10703
11043
  const onSelectModeItemClick = placement?.onSelectModeItemClick;
10704
- if (!onSelectModeItemClick) return;
10705
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11044
+ if (!onSelectModeItemClick) {
11045
+ finishCanvuInteraction("cancelled", {
11046
+ current: currentInteractionPoint
11047
+ });
11048
+ return;
11049
+ }
11050
+ const { worldX, worldY } = currentWorld;
10706
11051
  onSelectModeItemClick({
10707
11052
  item,
10708
11053
  worldX,
@@ -10723,6 +11068,10 @@ var VectorViewport = react.forwardRef(
10723
11068
  },
10724
11069
  setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
10725
11070
  });
11071
+ finishCanvuInteraction("completed", {
11072
+ current: currentInteractionPoint,
11073
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
11074
+ });
10726
11075
  return;
10727
11076
  }
10728
11077
  if (st.kind === "marquee") {
@@ -10731,16 +11080,25 @@ var VectorViewport = react.forwardRef(
10731
11080
  setPlacementPreview(null);
10732
11081
  marqueeCandidateIdsRef.current = [];
10733
11082
  setMarqueeCandidateIds([]);
10734
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11083
+ const { worldX, worldY } = currentWorld;
10735
11084
  const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
10736
11085
  const br = normalizeRect(raw);
10737
11086
  const screenDx = ev.clientX - st.startScreen.x;
10738
11087
  const screenDy = ev.clientY - st.startScreen.y;
11088
+ if (finishOutcome === "cancelled") {
11089
+ finishCanvuInteraction("cancelled", {
11090
+ current: currentInteractionPoint
11091
+ });
11092
+ return;
11093
+ }
10739
11094
  const tooSmall = Math.hypot(screenDx, screenDy) < TAP_PX || br.width < MIN_MARQUEE_WORLD && br.height < MIN_MARQUEE_WORLD;
10740
11095
  if (tooSmall) {
10741
11096
  if (!st.shiftKey) {
10742
11097
  setEffectiveSelectedIdsRef.current([]);
10743
11098
  }
11099
+ finishCanvuInteraction("cancelled", {
11100
+ current: currentInteractionPoint
11101
+ });
10744
11102
  return;
10745
11103
  }
10746
11104
  const picked = collectItemIdsInRect(resolvedItemsRef.current, br);
@@ -10757,9 +11115,26 @@ var VectorViewport = react.forwardRef(
10757
11115
  } else {
10758
11116
  setEffectiveSelectedIdsRef.current(picked);
10759
11117
  }
11118
+ finishCanvuInteraction("completed", {
11119
+ current: currentInteractionPoint
11120
+ });
10760
11121
  return;
10761
11122
  }
10762
11123
  if (st.kind === "stroke") {
11124
+ if (finishOutcome === "cancelled") {
11125
+ clearStraightStrokeHoldTimer(st);
11126
+ dragStateRef.current = { kind: "idle" };
11127
+ releaseInteractionPointer();
11128
+ if (st.itemId) {
11129
+ renderSceneWithLivePenStroke(null);
11130
+ }
11131
+ emitRemoteStrokePreviewClear();
11132
+ setPlacementPreview(null);
11133
+ finishCanvuInteraction("cancelled", {
11134
+ current: currentInteractionPoint
11135
+ });
11136
+ return;
11137
+ }
10763
11138
  const completedPoints = (() => {
10764
11139
  if (st.straightLine?.active) {
10765
11140
  const endpoint = pointerSampleToWorldPoint(
@@ -10790,15 +11165,20 @@ var VectorViewport = react.forwardRef(
10790
11165
  }
10791
11166
  if (st.kind === "erase") {
10792
11167
  const change = onItemsChangeRef.current;
11168
+ const erasedIds = [...eraserPreviewIdsRef.current];
11169
+ const info = erasedIds.length > 0 ? {
11170
+ motive: "erase",
11171
+ itemIds: erasedIds,
11172
+ toolId: "eraser"
11173
+ } : void 0;
10793
11174
  if (change && eraserPreviewIdsRef.current.size > 0) {
10794
11175
  const idSet = new Set(eraserPreviewIdsRef.current);
10795
- change(
10796
- itemsRef.current.filter((i) => !idSet.has(i.id)),
10797
- {
10798
- motive: "erase",
10799
- itemIds: [...idSet]
10800
- }
10801
- );
11176
+ if (finishOutcome === "completed") {
11177
+ change(
11178
+ itemsRef.current.filter((i) => !idSet.has(i.id)),
11179
+ info
11180
+ );
11181
+ }
10802
11182
  }
10803
11183
  eraserPreviewIdsRef.current.clear();
10804
11184
  setEraserPreviewIds([]);
@@ -10806,7 +11186,13 @@ var VectorViewport = react.forwardRef(
10806
11186
  setEraserActive(false);
10807
11187
  dragStateRef.current = { kind: "idle" };
10808
11188
  releaseInteractionPointer();
10809
- requestAutoResetTool("eraser");
11189
+ if (finishOutcome === "completed") {
11190
+ requestAutoResetTool("eraser");
11191
+ }
11192
+ finishCanvuInteraction(finishOutcome, {
11193
+ current: currentInteractionPoint,
11194
+ ...finishOutcome === "completed" && info ? { info } : {}
11195
+ });
10810
11196
  return;
10811
11197
  }
10812
11198
  if (st.kind === "tap") {
@@ -10814,11 +11200,22 @@ var VectorViewport = react.forwardRef(
10814
11200
  const dy = ev.clientY - st.startScreen.y;
10815
11201
  dragStateRef.current = { kind: "idle" };
10816
11202
  releaseInteractionPointer();
10817
- if (Math.hypot(dx, dy) > TAP_PX) return;
11203
+ if (finishOutcome === "cancelled" || Math.hypot(dx, dy) > TAP_PX) {
11204
+ finishCanvuInteraction("cancelled", {
11205
+ current: currentInteractionPoint
11206
+ });
11207
+ return;
11208
+ }
10818
11209
  const change = onItemsChangeRef.current;
10819
- if (!change) return;
11210
+ if (!change) {
11211
+ finishCanvuInteraction("cancelled", {
11212
+ current: currentInteractionPoint
11213
+ });
11214
+ return;
11215
+ }
10820
11216
  const id = createShapeId();
10821
11217
  const { x: worldX, y: worldY } = st.startWorld;
11218
+ let info;
10822
11219
  if (st.tool === "text") {
10823
11220
  const fs = strokeStyleRef.current.textFontSize;
10824
11221
  const baseline = textBaselineYFor(fs);
@@ -10841,11 +11238,12 @@ var VectorViewport = react.forwardRef(
10841
11238
  bounds: { ...newItem.bounds }
10842
11239
  };
10843
11240
  const hidden = applyTextDraftWhileEditing(newItem, "");
10844
- change([...itemsRef.current, hidden], {
11241
+ info = {
10845
11242
  motive: "text-create",
10846
11243
  itemIds: [id],
10847
11244
  toolId: st.tool
10848
- });
11245
+ };
11246
+ change([...itemsRef.current, hidden], info);
10849
11247
  setEffectiveSelectedIdsRef.current([id]);
10850
11248
  setEditingTextId(id);
10851
11249
  setDraftText("");
@@ -10854,6 +11252,10 @@ var VectorViewport = react.forwardRef(
10854
11252
  imageInputRef.current?.click();
10855
11253
  }
10856
11254
  requestAutoResetTool(st.tool);
11255
+ finishCanvuInteraction("completed", {
11256
+ current: currentInteractionPoint,
11257
+ ...info ? { info } : {}
11258
+ });
10857
11259
  return;
10858
11260
  }
10859
11261
  if (st.kind === "place") {
@@ -10864,11 +11266,19 @@ var VectorViewport = react.forwardRef(
10864
11266
  dragStateRef.current = { kind: "idle" };
10865
11267
  releaseInteractionPointer();
10866
11268
  setPlacementPreview(null);
10867
- if (!change) return;
11269
+ if (finishOutcome === "cancelled" || !change) {
11270
+ finishCanvuInteraction("cancelled", {
11271
+ current: currentInteractionPoint
11272
+ });
11273
+ return;
11274
+ }
10868
11275
  if (st.tool === "arrow") {
10869
11276
  const screenDx = ev.clientX - st.startScreen.x;
10870
11277
  const screenDy = ev.clientY - st.startScreen.y;
10871
11278
  if (Math.hypot(screenDx, screenDy) < MIN_ARROW_DRAG_PX) {
11279
+ finishCanvuInteraction("cancelled", {
11280
+ current: currentInteractionPoint
11281
+ });
10872
11282
  return;
10873
11283
  }
10874
11284
  const maxDist = ARROW_BIND_SNAP_PX / cam.zoom;
@@ -10900,18 +11310,23 @@ var VectorViewport = react.forwardRef(
10900
11310
  ...snapB ? { end: snapB.binding } : {}
10901
11311
  };
10902
11312
  }
11313
+ const info2 = {
11314
+ motive: "place",
11315
+ itemIds: [id2],
11316
+ toolId: st.tool
11317
+ };
10903
11318
  change(
10904
11319
  [
10905
11320
  ...itemsRef.current,
10906
11321
  createLineItem(id2, rawArrow, line, "arrow", pen2, arrowBind)
10907
11322
  ],
10908
- {
10909
- motive: "place",
10910
- itemIds: [id2],
10911
- toolId: st.tool
10912
- }
11323
+ info2
10913
11324
  );
10914
11325
  setEffectiveSelectedIdsRef.current([id2]);
11326
+ finishCanvuInteraction("completed", {
11327
+ current: currentInteractionPoint,
11328
+ info: info2
11329
+ });
10915
11330
  return;
10916
11331
  }
10917
11332
  let raw = rectFromCorners(a, b);
@@ -10936,47 +11351,61 @@ var VectorViewport = react.forwardRef(
10936
11351
  }
10937
11352
  const id = createShapeId();
10938
11353
  const pen = strokeStyleRef.current;
11354
+ let info;
10939
11355
  if (cpUp && st.tool === cpUp.toolId) {
10940
11356
  const item = tagCustomPlacementItem(
10941
11357
  cpUp.createItem({ id, bounds: br }),
10942
11358
  cpUp.toolId
10943
11359
  );
10944
- change(itemsRef.current.concat(item), {
11360
+ info = {
10945
11361
  motive: "place",
10946
11362
  itemIds: [id],
10947
11363
  toolId: st.tool
10948
- });
11364
+ };
11365
+ change(itemsRef.current.concat(item), info);
10949
11366
  if (cpUp.selectAfterCreate !== false) {
10950
11367
  setEffectiveSelectedIdsRef.current([id]);
10951
11368
  }
11369
+ finishCanvuInteraction("completed", {
11370
+ current: currentInteractionPoint,
11371
+ info
11372
+ });
10952
11373
  return;
10953
11374
  }
10954
11375
  if (st.tool === "rect") {
10955
- change([...itemsRef.current, createRectangleItem(id, raw, pen)], {
11376
+ info = {
10956
11377
  motive: "place",
10957
11378
  itemIds: [id],
10958
11379
  toolId: st.tool
10959
- });
11380
+ };
11381
+ change([...itemsRef.current, createRectangleItem(id, raw, pen)], info);
10960
11382
  setEffectiveSelectedIdsRef.current([id]);
10961
11383
  } else if (st.tool === "ellipse") {
10962
- change([...itemsRef.current, createEllipseItem(id, raw, pen)], {
11384
+ info = {
10963
11385
  motive: "place",
10964
11386
  itemIds: [id],
10965
11387
  toolId: st.tool
10966
- });
11388
+ };
11389
+ change([...itemsRef.current, createEllipseItem(id, raw, pen)], info);
10967
11390
  setEffectiveSelectedIdsRef.current([id]);
10968
11391
  } else if (st.tool === "architectural-cloud") {
11392
+ info = {
11393
+ motive: "place",
11394
+ itemIds: [id],
11395
+ toolId: st.tool
11396
+ };
10969
11397
  change(
10970
11398
  [...itemsRef.current, createArchitecturalCloudItem(id, raw, pen)],
10971
- {
10972
- motive: "place",
10973
- itemIds: [id],
10974
- toolId: st.tool
10975
- }
11399
+ info
10976
11400
  );
10977
11401
  setEffectiveSelectedIdsRef.current([id]);
10978
11402
  } else if (st.tool === "line" || st.tool === "arrow") {
10979
11403
  const line = lineEndpointsToLocal(raw, lineA, lineB);
11404
+ info = {
11405
+ motive: "place",
11406
+ itemIds: [id],
11407
+ toolId: st.tool
11408
+ };
10980
11409
  change(
10981
11410
  [
10982
11411
  ...itemsRef.current,
@@ -10988,15 +11417,15 @@ var VectorViewport = react.forwardRef(
10988
11417
  pen
10989
11418
  )
10990
11419
  ],
10991
- {
10992
- motive: "place",
10993
- itemIds: [id],
10994
- toolId: st.tool
10995
- }
11420
+ info
10996
11421
  );
10997
11422
  setEffectiveSelectedIdsRef.current([id]);
10998
11423
  }
10999
11424
  requestAutoResetTool(st.tool);
11425
+ finishCanvuInteraction("completed", {
11426
+ current: currentInteractionPoint,
11427
+ ...info ? { info } : {}
11428
+ });
11000
11429
  }
11001
11430
  };
11002
11431
  document.addEventListener("pointermove", onMove);
@@ -11014,11 +11443,13 @@ var VectorViewport = react.forwardRef(
11014
11443
  pruneEraserTrail,
11015
11444
  pruneLaserTrail,
11016
11445
  finalizeStrokeDragState,
11446
+ finishCanvuInteraction,
11017
11447
  renderSceneWithLivePenStroke,
11018
11448
  releaseInteractionPointer,
11019
11449
  requestAutoResetTool,
11020
11450
  screenToWorld,
11021
11451
  setStraightStrokeEndpoint,
11452
+ updateCanvuInteractionCurrent,
11022
11453
  updateStraightStrokeForMove
11023
11454
  ]);
11024
11455
  const selectedItemsForOverlay = react.useMemo(() => {