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.js CHANGED
@@ -2112,7 +2112,7 @@ var collapseButtonStyle = {
2112
2112
  cursor: "pointer",
2113
2113
  padding: 0
2114
2114
  };
2115
- var collapsedButtonStyle = {
2115
+ var defaultCollapsedButtonStyle = {
2116
2116
  display: "inline-flex",
2117
2117
  alignItems: "center",
2118
2118
  justifyContent: "center",
@@ -2141,7 +2141,10 @@ function ImagesMenu({
2141
2141
  onItemsChange,
2142
2142
  onFocusItem,
2143
2143
  labels,
2144
- defaultOpen = false
2144
+ defaultOpen = false,
2145
+ collapsedButtonClassName,
2146
+ collapsedButtonStyle,
2147
+ renderCollapsedButton
2145
2148
  }) {
2146
2149
  const managed = useMemo(() => items.filter(isManagedImage), [items]);
2147
2150
  const sensors = useSensors(
@@ -2162,18 +2165,26 @@ function ImagesMenu({
2162
2165
  duplicate: labels?.duplicate ?? labels?.copy ?? defaultLabels.duplicate
2163
2166
  };
2164
2167
  if (collapsed) {
2165
- return /* @__PURE__ */ jsx(
2166
- "button",
2167
- {
2168
- type: "button",
2169
- "data-slot": "images-menu-collapsed",
2170
- style: collapsedButtonStyle,
2171
- "aria-label": resolvedLabels.expand,
2172
- title: resolvedLabels.expand,
2173
- onClick: () => setCollapsed(false),
2174
- children: /* @__PURE__ */ jsx(Images, { size: 20 })
2175
- }
2176
- );
2168
+ const buttonProps = {
2169
+ type: "button",
2170
+ "data-slot": "images-menu-collapsed",
2171
+ className: collapsedButtonClassName,
2172
+ style: {
2173
+ ...defaultCollapsedButtonStyle,
2174
+ ...collapsedButtonStyle ?? {}
2175
+ },
2176
+ "aria-label": resolvedLabels.expand,
2177
+ title: resolvedLabels.expand,
2178
+ onClick: () => setCollapsed(false)
2179
+ };
2180
+ if (renderCollapsedButton) {
2181
+ return renderCollapsedButton({
2182
+ count: managed.length,
2183
+ label: resolvedLabels.expand,
2184
+ buttonProps
2185
+ });
2186
+ }
2187
+ return /* @__PURE__ */ jsx("button", { ...buttonProps, children: /* @__PURE__ */ jsx(Images, { size: 20 }) });
2177
2188
  }
2178
2189
  const onDragEnd = (event) => {
2179
2190
  const { active, over } = event;
@@ -3135,7 +3146,9 @@ function ToolPluginComponent({
3135
3146
  toolTransform,
3136
3147
  createItem,
3137
3148
  selectAfterCreate,
3138
- onSelectModeItemClick
3149
+ onSelectModeItemClick,
3150
+ onBeforeInteraction,
3151
+ onAfterInteraction
3139
3152
  }) {
3140
3153
  const contribution = useMemo(
3141
3154
  () => ({
@@ -3148,9 +3161,27 @@ function ToolPluginComponent({
3148
3161
  selectAfterCreate,
3149
3162
  onSelectModeItemClick
3150
3163
  }
3151
- ] : 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
3152
3175
  }),
3153
- [createItem, onSelectModeItemClick, selectAfterCreate, tool, toolTransform]
3176
+ [
3177
+ createItem,
3178
+ onAfterInteraction,
3179
+ onBeforeInteraction,
3180
+ onSelectModeItemClick,
3181
+ selectAfterCreate,
3182
+ tool,
3183
+ toolTransform
3184
+ ]
3154
3185
  );
3155
3186
  useCanvuPluginContribution(pluginId, contribution);
3156
3187
  return null;
@@ -3161,6 +3192,8 @@ function createToolPlugin(options) {
3161
3192
  toolTransform,
3162
3193
  selectAfterCreate,
3163
3194
  onSelectModeItemClick,
3195
+ onBeforeInteraction,
3196
+ onAfterInteraction,
3164
3197
  ...tool
3165
3198
  } = options;
3166
3199
  const pluginId = `canvu.plugin.tool:${tool.id}`;
@@ -3175,7 +3208,9 @@ function createToolPlugin(options) {
3175
3208
  toolTransform,
3176
3209
  createItem,
3177
3210
  selectAfterCreate,
3178
- onSelectModeItemClick
3211
+ onSelectModeItemClick,
3212
+ onBeforeInteraction,
3213
+ onAfterInteraction
3179
3214
  }
3180
3215
  );
3181
3216
  }
@@ -8125,6 +8160,8 @@ var VectorViewport = forwardRef(
8125
8160
  selectedIds: selectedIdsProp,
8126
8161
  onSelectionChange,
8127
8162
  onItemsChange: consumerOnItemsChange,
8163
+ onBeforeInteraction: consumerOnBeforeInteraction,
8164
+ onAfterInteraction: consumerOnAfterInteraction,
8128
8165
  onActivateLink,
8129
8166
  onWorldPointerDown: consumerOnWorldPointerDown,
8130
8167
  toolbar,
@@ -8250,6 +8287,24 @@ var VectorViewport = forwardRef(
8250
8287
  (contribution) => contribution.callbacks?.onCameraChange
8251
8288
  )
8252
8289
  );
8290
+ const beforeInteractionHooks = useMemo(
8291
+ () => [
8292
+ consumerOnBeforeInteraction,
8293
+ ...orderedPluginContributions.map(
8294
+ (contribution) => contribution.interactionHooks?.onBeforeInteraction
8295
+ )
8296
+ ].filter((hook) => hook != null),
8297
+ [consumerOnBeforeInteraction, orderedPluginContributions]
8298
+ );
8299
+ const afterInteractionHooks = useMemo(
8300
+ () => [
8301
+ consumerOnAfterInteraction,
8302
+ ...orderedPluginContributions.map(
8303
+ (contribution) => contribution.interactionHooks?.onAfterInteraction
8304
+ )
8305
+ ].filter((hook) => hook != null),
8306
+ [consumerOnAfterInteraction, orderedPluginContributions]
8307
+ );
8253
8308
  const onItemsChange = useMemo(() => {
8254
8309
  const middlewares = orderedPluginContributions.map((contribution) => contribution.wrapOnItemsChange).filter(
8255
8310
  (middleware) => middleware != null
@@ -8393,6 +8448,81 @@ var VectorViewport = forwardRef(
8393
8448
  [items]
8394
8449
  );
8395
8450
  itemsRef.current = normalizedItems;
8451
+ const beforeInteractionHooksRef = useRef(beforeInteractionHooks);
8452
+ beforeInteractionHooksRef.current = beforeInteractionHooks;
8453
+ const afterInteractionHooksRef = useRef(afterInteractionHooks);
8454
+ afterInteractionHooksRef.current = afterInteractionHooks;
8455
+ const canvuInteractionSeqRef = useRef(0);
8456
+ const activeCanvuInteractionRef = useRef(null);
8457
+ const makeInteractionPoint = useCallback(
8458
+ (input) => ({
8459
+ worldX: input.worldX,
8460
+ worldY: input.worldY,
8461
+ clientX: input.clientX,
8462
+ clientY: input.clientY
8463
+ }),
8464
+ []
8465
+ );
8466
+ const startCanvuInteraction = useCallback(
8467
+ (input) => {
8468
+ const start = makeInteractionPoint(input);
8469
+ const detail = {
8470
+ interactionId: `canvu-interaction-${++canvuInteractionSeqRef.current}`,
8471
+ kind: input.kind,
8472
+ toolId: input.toolId,
8473
+ pointerType: input.pointerType,
8474
+ button: input.button,
8475
+ shiftKey: input.shiftKey,
8476
+ altKey: input.altKey,
8477
+ metaKey: input.metaKey,
8478
+ ctrlKey: input.ctrlKey,
8479
+ start,
8480
+ current: start,
8481
+ selectedIds: [...effectiveSelectedIdsRef.current],
8482
+ itemIds: [...input.itemIds ?? []],
8483
+ items: itemsRef.current
8484
+ };
8485
+ for (const hook of beforeInteractionHooksRef.current) {
8486
+ if (hook(detail) === "handled") {
8487
+ return null;
8488
+ }
8489
+ }
8490
+ activeCanvuInteractionRef.current = detail;
8491
+ return detail;
8492
+ },
8493
+ [makeInteractionPoint]
8494
+ );
8495
+ const updateCanvuInteractionCurrent = useCallback(
8496
+ (input) => {
8497
+ const detail = activeCanvuInteractionRef.current;
8498
+ if (!detail) return;
8499
+ activeCanvuInteractionRef.current = {
8500
+ ...detail,
8501
+ current: makeInteractionPoint(input)
8502
+ };
8503
+ },
8504
+ [makeInteractionPoint]
8505
+ );
8506
+ const finishCanvuInteraction = useCallback(
8507
+ (outcome, options) => {
8508
+ const detail = activeCanvuInteractionRef.current;
8509
+ if (!detail) return;
8510
+ activeCanvuInteractionRef.current = null;
8511
+ const current = options?.current ? makeInteractionPoint(options.current) : detail.current;
8512
+ const info = options?.info;
8513
+ const afterDetail = {
8514
+ ...detail,
8515
+ current,
8516
+ itemIds: detail.itemIds.length > 0 ? detail.itemIds : [...info?.itemIds ?? []],
8517
+ outcome,
8518
+ ...info ? { info } : {}
8519
+ };
8520
+ for (const hook of afterInteractionHooksRef.current) {
8521
+ hook(afterDetail);
8522
+ }
8523
+ },
8524
+ [makeInteractionPoint]
8525
+ );
8396
8526
  onWorldPointerDownRef.current = onWorldPointerDown;
8397
8527
  const originalOnItemsChangeRef = useRef(onItemsChange);
8398
8528
  originalOnItemsChangeRef.current = onItemsChange;
@@ -8633,9 +8763,14 @@ var VectorViewport = forwardRef(
8633
8763
  );
8634
8764
  if (item) {
8635
8765
  const exists = itemsRef.current.some((it) => it.id === id);
8766
+ const info = {
8767
+ motive: "draw",
8768
+ itemIds: [id],
8769
+ toolId: args.tool
8770
+ };
8636
8771
  change(
8637
8772
  exists ? replaceItem(itemsRef.current, id, item) : [...itemsRef.current, item],
8638
- { motive: "draw", itemIds: [id], toolId: args.tool }
8773
+ info
8639
8774
  );
8640
8775
  patchCurrentStrokeStyle({
8641
8776
  stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
@@ -8643,10 +8778,15 @@ var VectorViewport = forwardRef(
8643
8778
  strokeOpacity: item.strokeOpacity,
8644
8779
  strokeDash: item.strokeDash
8645
8780
  });
8781
+ if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8782
+ requestAutoResetTool(args.tool);
8783
+ }
8784
+ return info;
8646
8785
  }
8647
8786
  if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8648
8787
  requestAutoResetTool(args.tool);
8649
8788
  }
8789
+ return void 0;
8650
8790
  },
8651
8791
  [
8652
8792
  patchCurrentStrokeStyle,
@@ -8777,17 +8917,19 @@ var VectorViewport = forwardRef(
8777
8917
  }
8778
8918
  emitRemoteStrokePreviewClear();
8779
8919
  setPlacementPreview(null);
8780
- commitCompletedStroke({
8920
+ const info = commitCompletedStroke({
8781
8921
  tool,
8782
8922
  pointerType,
8783
8923
  points: [...pts],
8784
8924
  style,
8785
8925
  itemId
8786
8926
  });
8927
+ finishCanvuInteraction("completed", { info });
8787
8928
  },
8788
8929
  [
8789
8930
  commitCompletedStroke,
8790
8931
  emitRemoteStrokePreviewClear,
8932
+ finishCanvuInteraction,
8791
8933
  releaseInteractionPointer,
8792
8934
  renderSceneWithLivePenStroke
8793
8935
  ]
@@ -9857,12 +9999,26 @@ var VectorViewport = forwardRef(
9857
9999
  if (!raw) return void 0;
9858
10000
  return raw.toolKind === "arrow" && raw.arrowBind ? bakeArrowItemToAbsolute(raw, canonical) : raw;
9859
10001
  };
10002
+ const startInteraction = (kind, itemIds) => startCanvuInteraction({
10003
+ kind,
10004
+ toolId: tool,
10005
+ pointerType: e.pointerType,
10006
+ button: e.button,
10007
+ worldX,
10008
+ worldY,
10009
+ clientX: e.clientX,
10010
+ clientY: e.clientY,
10011
+ shiftKey: e.shiftKey,
10012
+ altKey: e.altKey,
10013
+ metaKey: e.metaKey,
10014
+ ctrlKey: e.ctrlKey,
10015
+ itemIds
10016
+ });
10017
+ const stopHandledInteraction = () => {
10018
+ e.preventDefault();
10019
+ e.stopPropagation();
10020
+ };
9860
10021
  if (tool === "eraser") {
9861
- dragStateRef.current = { kind: "erase" };
9862
- eraserPreviewIdsRef.current = /* @__PURE__ */ new Set();
9863
- setEraserPreviewIds([]);
9864
- setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
9865
- setEraserActive(true);
9866
10022
  const toErase = collectEraserTargetsAtWorldPoint(
9867
10023
  resolved,
9868
10024
  worldX,
@@ -9872,6 +10028,15 @@ var VectorViewport = forwardRef(
9872
10028
  ignoreLocked: true
9873
10029
  }
9874
10030
  );
10031
+ if (!startInteraction("erase", toErase)) {
10032
+ stopHandledInteraction();
10033
+ return;
10034
+ }
10035
+ dragStateRef.current = { kind: "erase" };
10036
+ eraserPreviewIdsRef.current = /* @__PURE__ */ new Set();
10037
+ setEraserPreviewIds([]);
10038
+ setEraserTrail([{ x: worldX, y: worldY, t: Date.now() }]);
10039
+ setEraserActive(true);
9875
10040
  if (toErase.length > 0) {
9876
10041
  for (const id of toErase) {
9877
10042
  eraserPreviewIdsRef.current.add(id);
@@ -9904,6 +10069,10 @@ var VectorViewport = forwardRef(
9904
10069
  const snapSpin = bakedSnapshot(selected.id);
9905
10070
  if (!snapSpin) return;
9906
10071
  const pivot = itemPivotWorld(selected);
10072
+ if (!startInteraction("rotate", [selected.id])) {
10073
+ stopHandledInteraction();
10074
+ return;
10075
+ }
9907
10076
  dragStateRef.current = {
9908
10077
  kind: "rotate",
9909
10078
  id: selected.id,
@@ -9928,6 +10097,10 @@ var VectorViewport = forwardRef(
9928
10097
  if (hb) {
9929
10098
  const snapRs = bakedSnapshot(selected.id);
9930
10099
  if (!snapRs) return;
10100
+ if (!startInteraction("resize", [selected.id])) {
10101
+ stopHandledInteraction();
10102
+ return;
10103
+ }
9931
10104
  dragStateRef.current = {
9932
10105
  kind: "resize",
9933
10106
  id: selected.id,
@@ -9960,6 +10133,10 @@ var VectorViewport = forwardRef(
9960
10133
  moveSnapshots[id] = snap;
9961
10134
  }
9962
10135
  }
10136
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10137
+ stopHandledInteraction();
10138
+ return;
10139
+ }
9963
10140
  dragStateRef.current = {
9964
10141
  kind: "select-mode-item-click",
9965
10142
  id: hit.id,
@@ -9976,7 +10153,14 @@ var VectorViewport = forwardRef(
9976
10153
  }
9977
10154
  if (e.shiftKey) {
9978
10155
  const next = cur.includes(hit.id) ? cur.filter((id) => id !== hit.id) : [...cur, hit.id];
10156
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10157
+ stopHandledInteraction();
10158
+ return;
10159
+ }
9979
10160
  setEffectiveSelectedIdsRef.current(next);
10161
+ finishCanvuInteraction("completed", {
10162
+ info: { motive: "custom", itemIds: [hit.id], toolId: tool }
10163
+ });
9980
10164
  e.preventDefault();
9981
10165
  e.stopPropagation();
9982
10166
  return;
@@ -9984,7 +10168,6 @@ var VectorViewport = forwardRef(
9984
10168
  let idsToMove;
9985
10169
  if (!cur.includes(hit.id)) {
9986
10170
  idsToMove = [hit.id];
9987
- setEffectiveSelectedIdsRef.current(idsToMove);
9988
10171
  } else {
9989
10172
  idsToMove = [...cur];
9990
10173
  }
@@ -9995,6 +10178,13 @@ var VectorViewport = forwardRef(
9995
10178
  snapshots[id] = snap;
9996
10179
  }
9997
10180
  }
10181
+ if (!startInteraction("move", idsToMove)) {
10182
+ stopHandledInteraction();
10183
+ return;
10184
+ }
10185
+ if (!cur.includes(hit.id)) {
10186
+ setEffectiveSelectedIdsRef.current(idsToMove);
10187
+ }
9998
10188
  dragStateRef.current = {
9999
10189
  kind: "move",
10000
10190
  ids: idsToMove,
@@ -10019,9 +10209,14 @@ var VectorViewport = forwardRef(
10019
10209
  }
10020
10210
  }
10021
10211
  if (Object.keys(snapshots).length > 0) {
10212
+ const moveIds = Object.keys(snapshots);
10213
+ if (!startInteraction("move", moveIds)) {
10214
+ stopHandledInteraction();
10215
+ return;
10216
+ }
10022
10217
  dragStateRef.current = {
10023
10218
  kind: "move",
10024
- ids: Object.keys(snapshots),
10219
+ ids: moveIds,
10025
10220
  snapshots,
10026
10221
  startWorld: { x: worldX, y: worldY }
10027
10222
  };
@@ -10032,6 +10227,10 @@ var VectorViewport = forwardRef(
10032
10227
  }
10033
10228
  }
10034
10229
  }
10230
+ if (!startInteraction("marquee")) {
10231
+ stopHandledInteraction();
10232
+ return;
10233
+ }
10035
10234
  dragStateRef.current = {
10036
10235
  kind: "marquee",
10037
10236
  startWorld: { x: worldX, y: worldY },
@@ -10060,6 +10259,10 @@ var VectorViewport = forwardRef(
10060
10259
  );
10061
10260
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10062
10261
  const directPenStroke = e.pointerType === "pen" && (tool === "draw" || tool === "marker");
10262
+ if (!startInteraction("stroke")) {
10263
+ stopHandledInteraction();
10264
+ return;
10265
+ }
10063
10266
  let itemId;
10064
10267
  if (directPenStroke) {
10065
10268
  itemId = createShapeId();
@@ -10103,6 +10306,10 @@ var VectorViewport = forwardRef(
10103
10306
  return;
10104
10307
  }
10105
10308
  if (tool === "text" || tool === "image") {
10309
+ if (!startInteraction("tap")) {
10310
+ stopHandledInteraction();
10311
+ return;
10312
+ }
10106
10313
  dragStateRef.current = {
10107
10314
  kind: "tap",
10108
10315
  tool,
@@ -10116,6 +10323,10 @@ var VectorViewport = forwardRef(
10116
10323
  }
10117
10324
  const cp = customPlacementRef.current;
10118
10325
  if (tool === "rect" || tool === "ellipse" || tool === "architectural-cloud" || tool === "line" || tool === "arrow" || cp && tool === cp.toolId) {
10326
+ if (!startInteraction("place")) {
10327
+ stopHandledInteraction();
10328
+ return;
10329
+ }
10119
10330
  dragStateRef.current = {
10120
10331
  kind: "place",
10121
10332
  tool,
@@ -10133,8 +10344,10 @@ var VectorViewport = forwardRef(
10133
10344
  captureInteractionPointer,
10134
10345
  emitRemoteStrokePreview,
10135
10346
  finalizeStrokeDragState,
10347
+ finishCanvuInteraction,
10136
10348
  renderSceneWithLivePenStroke,
10137
10349
  screenToWorld,
10350
+ startCanvuInteraction,
10138
10351
  startOrRestartStraightStrokeHoldTimer
10139
10352
  ]
10140
10353
  );
@@ -10163,14 +10376,32 @@ var VectorViewport = forwardRef(
10163
10376
  e.stopImmediatePropagation();
10164
10377
  return;
10165
10378
  }
10166
- wrapperRef.current?.focus({ preventScroll: true });
10167
- setContextMenu(null);
10168
10379
  const startPoint = pointerSampleToWorldPoint(
10169
10380
  screenToWorld,
10170
10381
  e.clientX,
10171
10382
  e.clientY,
10172
10383
  e.pressure
10173
10384
  );
10385
+ if (!startCanvuInteraction({
10386
+ kind: "stroke",
10387
+ toolId: tool,
10388
+ pointerType: e.pointerType,
10389
+ button: e.button,
10390
+ worldX: startPoint.x,
10391
+ worldY: startPoint.y,
10392
+ clientX: e.clientX,
10393
+ clientY: e.clientY,
10394
+ shiftKey: e.shiftKey,
10395
+ altKey: e.altKey,
10396
+ metaKey: e.metaKey,
10397
+ ctrlKey: e.ctrlKey
10398
+ })) {
10399
+ e.preventDefault();
10400
+ e.stopImmediatePropagation();
10401
+ return;
10402
+ }
10403
+ wrapperRef.current?.focus({ preventScroll: true });
10404
+ setContextMenu(null);
10174
10405
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10175
10406
  const itemId = createShapeId();
10176
10407
  const item = createFreehandStrokeItem(
@@ -10218,6 +10449,7 @@ var VectorViewport = forwardRef(
10218
10449
  interactive,
10219
10450
  renderSceneWithLivePenStroke,
10220
10451
  screenToWorld,
10452
+ startCanvuInteraction,
10221
10453
  startOrRestartStraightStrokeHoldTimer
10222
10454
  ]);
10223
10455
  useEffect(() => {
@@ -10268,15 +10500,32 @@ var VectorViewport = forwardRef(
10268
10500
  stopTouchEvent(ev);
10269
10501
  return;
10270
10502
  }
10271
- wrapperRef.current?.focus({ preventScroll: true });
10272
- setContextMenu(null);
10273
- penDetectedRef.current = true;
10274
10503
  const startPoint = pointerSampleToWorldPoint(
10275
10504
  screenToWorld,
10276
10505
  touch.clientX,
10277
10506
  touch.clientY,
10278
10507
  touchPressure(touch)
10279
10508
  );
10509
+ if (!startCanvuInteraction({
10510
+ kind: "stroke",
10511
+ toolId: tool,
10512
+ pointerType: "pen",
10513
+ button: 0,
10514
+ worldX: startPoint.x,
10515
+ worldY: startPoint.y,
10516
+ clientX: touch.clientX,
10517
+ clientY: touch.clientY,
10518
+ shiftKey: ev.shiftKey,
10519
+ altKey: ev.altKey,
10520
+ metaKey: ev.metaKey,
10521
+ ctrlKey: ev.ctrlKey
10522
+ })) {
10523
+ stopTouchEvent(ev);
10524
+ return;
10525
+ }
10526
+ wrapperRef.current?.focus({ preventScroll: true });
10527
+ setContextMenu(null);
10528
+ penDetectedRef.current = true;
10280
10529
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, touch.clientX, touch.clientY) : void 0;
10281
10530
  const itemId = createShapeId();
10282
10531
  const item = createFreehandStrokeItem(
@@ -10322,6 +10571,12 @@ var VectorViewport = forwardRef(
10322
10571
  touch.clientY,
10323
10572
  touchPressure(touch)
10324
10573
  );
10574
+ updateCanvuInteractionCurrent({
10575
+ worldX: endpoint.x,
10576
+ worldY: endpoint.y,
10577
+ clientX: touch.clientX,
10578
+ clientY: touch.clientY
10579
+ });
10325
10580
  if (updateStraightStrokeForMove(st, touch.clientX, touch.clientY, endpoint)) {
10326
10581
  debugApplePencilPointer("touchmove-stroke", {
10327
10582
  touchId: touch.identifier,
@@ -10363,16 +10618,42 @@ var VectorViewport = forwardRef(
10363
10618
  if (st.kind !== "stroke") return;
10364
10619
  const touch = findChangedTouch(ev.changedTouches);
10365
10620
  if (!touch) return;
10621
+ const currentPoint = pointerSampleToWorldPoint(
10622
+ screenToWorld,
10623
+ touch.clientX,
10624
+ touch.clientY,
10625
+ touchPressure(touch)
10626
+ );
10627
+ updateCanvuInteractionCurrent({
10628
+ worldX: currentPoint.x,
10629
+ worldY: currentPoint.y,
10630
+ clientX: touch.clientX,
10631
+ clientY: touch.clientY
10632
+ });
10633
+ if (ev.type === "touchcancel") {
10634
+ clearStraightStrokeHoldTimer(st);
10635
+ dragStateRef.current = { kind: "idle" };
10636
+ releaseInteractionPointer();
10637
+ if (st.itemId) {
10638
+ renderSceneWithLivePenStroke(null);
10639
+ }
10640
+ emitRemoteStrokePreviewClear();
10641
+ setPlacementPreview(null);
10642
+ finishCanvuInteraction("cancelled", {
10643
+ current: {
10644
+ worldX: currentPoint.x,
10645
+ worldY: currentPoint.y,
10646
+ clientX: touch.clientX,
10647
+ clientY: touch.clientY
10648
+ }
10649
+ });
10650
+ stopTouchEvent(ev);
10651
+ return;
10652
+ }
10366
10653
  const cam = cameraRef.current;
10367
10654
  if (cam) {
10368
10655
  if (st.straightLine?.active) {
10369
- const endpoint = pointerSampleToWorldPoint(
10370
- screenToWorld,
10371
- touch.clientX,
10372
- touch.clientY,
10373
- touchPressure(touch)
10374
- );
10375
- setStraightStrokeEndpoint(st, endpoint);
10656
+ setStraightStrokeEndpoint(st, currentPoint);
10376
10657
  } else {
10377
10658
  const completedPoints = appendTouchToStrokePoints(
10378
10659
  st.points,
@@ -10423,12 +10704,17 @@ var VectorViewport = forwardRef(
10423
10704
  }, [
10424
10705
  applePencilNav,
10425
10706
  emitRemoteStrokePreview,
10707
+ emitRemoteStrokePreviewClear,
10426
10708
  finalizeStrokeDragState,
10709
+ finishCanvuInteraction,
10427
10710
  interactive,
10711
+ releaseInteractionPointer,
10428
10712
  renderSceneWithLivePenStroke,
10429
10713
  screenToWorld,
10430
10714
  setStraightStrokeEndpoint,
10715
+ startCanvuInteraction,
10431
10716
  startOrRestartStraightStrokeHoldTimer,
10717
+ updateCanvuInteractionCurrent,
10432
10718
  updateStraightStrokeForMove
10433
10719
  ]);
10434
10720
  useEffect(() => {
@@ -10441,6 +10727,12 @@ var VectorViewport = forwardRef(
10441
10727
  if (st.kind === "tap") return;
10442
10728
  if (st.kind === "marquee") {
10443
10729
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
10730
+ updateCanvuInteractionCurrent({
10731
+ worldX: worldX2,
10732
+ worldY: worldY2,
10733
+ clientX: ev.clientX,
10734
+ clientY: ev.clientY
10735
+ });
10444
10736
  const raw = rectFromCorners(st.startWorld, { x: worldX2, y: worldY2 });
10445
10737
  setPlacementPreview({ kind: "marquee", rect: raw });
10446
10738
  const nextCand = collectItemIdsInRect(
@@ -10468,6 +10760,12 @@ var VectorViewport = forwardRef(
10468
10760
  ev.clientY,
10469
10761
  ev.pointerType === "pen" ? ev.pressure : void 0
10470
10762
  );
10763
+ updateCanvuInteractionCurrent({
10764
+ worldX: endpoint.x,
10765
+ worldY: endpoint.y,
10766
+ clientX: ev.clientX,
10767
+ clientY: ev.clientY
10768
+ });
10471
10769
  if (updateStraightStrokeForMove(st, ev.clientX, ev.clientY, endpoint)) {
10472
10770
  return;
10473
10771
  }
@@ -10516,6 +10814,12 @@ var VectorViewport = forwardRef(
10516
10814
  }
10517
10815
  if (st.kind === "erase") {
10518
10816
  const { worldX: worldX2, worldY: worldY2 } = screenToWorld(ev.clientX, ev.clientY);
10817
+ updateCanvuInteractionCurrent({
10818
+ worldX: worldX2,
10819
+ worldY: worldY2,
10820
+ clientX: ev.clientX,
10821
+ clientY: ev.clientY
10822
+ });
10519
10823
  const lineHitWorld = 10 / cam.zoom;
10520
10824
  setEraserTrail(
10521
10825
  (prev) => pruneEraserTrail([...prev, { x: worldX2, y: worldY2, t: Date.now() }])
@@ -10541,6 +10845,12 @@ var VectorViewport = forwardRef(
10541
10845
  const change = onItemsChangeRef.current;
10542
10846
  if (!change) return;
10543
10847
  const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
10848
+ updateCanvuInteractionCurrent({
10849
+ worldX,
10850
+ worldY,
10851
+ clientX: ev.clientX,
10852
+ clientY: ev.clientY
10853
+ });
10544
10854
  if (st.kind === "select-mode-item-click") {
10545
10855
  const screenDx = ev.clientX - st.startScreen.x;
10546
10856
  const screenDy = ev.clientY - st.startScreen.y;
@@ -10673,29 +10983,64 @@ var VectorViewport = forwardRef(
10673
10983
  setPlacementPreview(null);
10674
10984
  marqueeCandidateIdsRef.current = [];
10675
10985
  setMarqueeCandidateIds([]);
10986
+ finishCanvuInteraction("cancelled");
10676
10987
  return;
10677
10988
  }
10989
+ const finishOutcome = ev.type === "pointercancel" ? "cancelled" : "completed";
10990
+ const currentWorld = screenToWorld(ev.clientX, ev.clientY);
10991
+ const currentInteractionPoint = {
10992
+ worldX: currentWorld.worldX,
10993
+ worldY: currentWorld.worldY,
10994
+ clientX: ev.clientX,
10995
+ clientY: ev.clientY
10996
+ };
10997
+ updateCanvuInteractionCurrent(currentInteractionPoint);
10678
10998
  if (st.kind === "move" || st.kind === "resize" || st.kind === "rotate") {
10999
+ const info = st.kind === "move" ? { motive: "move", itemIds: [...st.ids] } : st.kind === "resize" ? { motive: "resize", itemIds: [st.id] } : { motive: "rotate", itemIds: [st.id] };
10679
11000
  dragStateRef.current = { kind: "idle" };
10680
11001
  releaseInteractionPointer();
11002
+ finishCanvuInteraction(finishOutcome, {
11003
+ current: currentInteractionPoint,
11004
+ ...finishOutcome === "completed" ? { info } : {}
11005
+ });
10681
11006
  return;
10682
11007
  }
10683
11008
  if (st.kind === "select-mode-item-click") {
10684
11009
  dragStateRef.current = { kind: "idle" };
10685
11010
  releaseInteractionPointer();
10686
- if (ev.type === "pointercancel") return;
11011
+ if (finishOutcome === "cancelled") {
11012
+ finishCanvuInteraction("cancelled", {
11013
+ current: currentInteractionPoint
11014
+ });
11015
+ return;
11016
+ }
10687
11017
  const dx = ev.clientX - st.startScreen.x;
10688
11018
  const dy = ev.clientY - st.startScreen.y;
10689
- if (Math.hypot(dx, dy) > TAP_PX) return;
11019
+ if (Math.hypot(dx, dy) > TAP_PX) {
11020
+ finishCanvuInteraction("cancelled", {
11021
+ current: currentInteractionPoint
11022
+ });
11023
+ return;
11024
+ }
10690
11025
  const item = itemsRef.current.find((candidate) => candidate.id === st.id) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.id);
10691
- if (!item) return;
11026
+ if (!item) {
11027
+ finishCanvuInteraction("cancelled", {
11028
+ current: currentInteractionPoint
11029
+ });
11030
+ return;
11031
+ }
10692
11032
  const placement = findSelectModeItemClickPlacement(
10693
11033
  item,
10694
11034
  allCustomPlacementsRef.current
10695
11035
  );
10696
11036
  const onSelectModeItemClick = placement?.onSelectModeItemClick;
10697
- if (!onSelectModeItemClick) return;
10698
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11037
+ if (!onSelectModeItemClick) {
11038
+ finishCanvuInteraction("cancelled", {
11039
+ current: currentInteractionPoint
11040
+ });
11041
+ return;
11042
+ }
11043
+ const { worldX, worldY } = currentWorld;
10699
11044
  onSelectModeItemClick({
10700
11045
  item,
10701
11046
  worldX,
@@ -10716,6 +11061,10 @@ var VectorViewport = forwardRef(
10716
11061
  },
10717
11062
  setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
10718
11063
  });
11064
+ finishCanvuInteraction("completed", {
11065
+ current: currentInteractionPoint,
11066
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
11067
+ });
10719
11068
  return;
10720
11069
  }
10721
11070
  if (st.kind === "marquee") {
@@ -10724,16 +11073,25 @@ var VectorViewport = forwardRef(
10724
11073
  setPlacementPreview(null);
10725
11074
  marqueeCandidateIdsRef.current = [];
10726
11075
  setMarqueeCandidateIds([]);
10727
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11076
+ const { worldX, worldY } = currentWorld;
10728
11077
  const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
10729
11078
  const br = normalizeRect(raw);
10730
11079
  const screenDx = ev.clientX - st.startScreen.x;
10731
11080
  const screenDy = ev.clientY - st.startScreen.y;
11081
+ if (finishOutcome === "cancelled") {
11082
+ finishCanvuInteraction("cancelled", {
11083
+ current: currentInteractionPoint
11084
+ });
11085
+ return;
11086
+ }
10732
11087
  const tooSmall = Math.hypot(screenDx, screenDy) < TAP_PX || br.width < MIN_MARQUEE_WORLD && br.height < MIN_MARQUEE_WORLD;
10733
11088
  if (tooSmall) {
10734
11089
  if (!st.shiftKey) {
10735
11090
  setEffectiveSelectedIdsRef.current([]);
10736
11091
  }
11092
+ finishCanvuInteraction("cancelled", {
11093
+ current: currentInteractionPoint
11094
+ });
10737
11095
  return;
10738
11096
  }
10739
11097
  const picked = collectItemIdsInRect(resolvedItemsRef.current, br);
@@ -10750,9 +11108,26 @@ var VectorViewport = forwardRef(
10750
11108
  } else {
10751
11109
  setEffectiveSelectedIdsRef.current(picked);
10752
11110
  }
11111
+ finishCanvuInteraction("completed", {
11112
+ current: currentInteractionPoint
11113
+ });
10753
11114
  return;
10754
11115
  }
10755
11116
  if (st.kind === "stroke") {
11117
+ if (finishOutcome === "cancelled") {
11118
+ clearStraightStrokeHoldTimer(st);
11119
+ dragStateRef.current = { kind: "idle" };
11120
+ releaseInteractionPointer();
11121
+ if (st.itemId) {
11122
+ renderSceneWithLivePenStroke(null);
11123
+ }
11124
+ emitRemoteStrokePreviewClear();
11125
+ setPlacementPreview(null);
11126
+ finishCanvuInteraction("cancelled", {
11127
+ current: currentInteractionPoint
11128
+ });
11129
+ return;
11130
+ }
10756
11131
  const completedPoints = (() => {
10757
11132
  if (st.straightLine?.active) {
10758
11133
  const endpoint = pointerSampleToWorldPoint(
@@ -10783,15 +11158,20 @@ var VectorViewport = forwardRef(
10783
11158
  }
10784
11159
  if (st.kind === "erase") {
10785
11160
  const change = onItemsChangeRef.current;
11161
+ const erasedIds = [...eraserPreviewIdsRef.current];
11162
+ const info = erasedIds.length > 0 ? {
11163
+ motive: "erase",
11164
+ itemIds: erasedIds,
11165
+ toolId: "eraser"
11166
+ } : void 0;
10786
11167
  if (change && eraserPreviewIdsRef.current.size > 0) {
10787
11168
  const idSet = new Set(eraserPreviewIdsRef.current);
10788
- change(
10789
- itemsRef.current.filter((i) => !idSet.has(i.id)),
10790
- {
10791
- motive: "erase",
10792
- itemIds: [...idSet]
10793
- }
10794
- );
11169
+ if (finishOutcome === "completed") {
11170
+ change(
11171
+ itemsRef.current.filter((i) => !idSet.has(i.id)),
11172
+ info
11173
+ );
11174
+ }
10795
11175
  }
10796
11176
  eraserPreviewIdsRef.current.clear();
10797
11177
  setEraserPreviewIds([]);
@@ -10799,7 +11179,13 @@ var VectorViewport = forwardRef(
10799
11179
  setEraserActive(false);
10800
11180
  dragStateRef.current = { kind: "idle" };
10801
11181
  releaseInteractionPointer();
10802
- requestAutoResetTool("eraser");
11182
+ if (finishOutcome === "completed") {
11183
+ requestAutoResetTool("eraser");
11184
+ }
11185
+ finishCanvuInteraction(finishOutcome, {
11186
+ current: currentInteractionPoint,
11187
+ ...finishOutcome === "completed" && info ? { info } : {}
11188
+ });
10803
11189
  return;
10804
11190
  }
10805
11191
  if (st.kind === "tap") {
@@ -10807,11 +11193,22 @@ var VectorViewport = forwardRef(
10807
11193
  const dy = ev.clientY - st.startScreen.y;
10808
11194
  dragStateRef.current = { kind: "idle" };
10809
11195
  releaseInteractionPointer();
10810
- if (Math.hypot(dx, dy) > TAP_PX) return;
11196
+ if (finishOutcome === "cancelled" || Math.hypot(dx, dy) > TAP_PX) {
11197
+ finishCanvuInteraction("cancelled", {
11198
+ current: currentInteractionPoint
11199
+ });
11200
+ return;
11201
+ }
10811
11202
  const change = onItemsChangeRef.current;
10812
- if (!change) return;
11203
+ if (!change) {
11204
+ finishCanvuInteraction("cancelled", {
11205
+ current: currentInteractionPoint
11206
+ });
11207
+ return;
11208
+ }
10813
11209
  const id = createShapeId();
10814
11210
  const { x: worldX, y: worldY } = st.startWorld;
11211
+ let info;
10815
11212
  if (st.tool === "text") {
10816
11213
  const fs = strokeStyleRef.current.textFontSize;
10817
11214
  const baseline = textBaselineYFor(fs);
@@ -10834,11 +11231,12 @@ var VectorViewport = forwardRef(
10834
11231
  bounds: { ...newItem.bounds }
10835
11232
  };
10836
11233
  const hidden = applyTextDraftWhileEditing(newItem, "");
10837
- change([...itemsRef.current, hidden], {
11234
+ info = {
10838
11235
  motive: "text-create",
10839
11236
  itemIds: [id],
10840
11237
  toolId: st.tool
10841
- });
11238
+ };
11239
+ change([...itemsRef.current, hidden], info);
10842
11240
  setEffectiveSelectedIdsRef.current([id]);
10843
11241
  setEditingTextId(id);
10844
11242
  setDraftText("");
@@ -10847,6 +11245,10 @@ var VectorViewport = forwardRef(
10847
11245
  imageInputRef.current?.click();
10848
11246
  }
10849
11247
  requestAutoResetTool(st.tool);
11248
+ finishCanvuInteraction("completed", {
11249
+ current: currentInteractionPoint,
11250
+ ...info ? { info } : {}
11251
+ });
10850
11252
  return;
10851
11253
  }
10852
11254
  if (st.kind === "place") {
@@ -10857,11 +11259,19 @@ var VectorViewport = forwardRef(
10857
11259
  dragStateRef.current = { kind: "idle" };
10858
11260
  releaseInteractionPointer();
10859
11261
  setPlacementPreview(null);
10860
- if (!change) return;
11262
+ if (finishOutcome === "cancelled" || !change) {
11263
+ finishCanvuInteraction("cancelled", {
11264
+ current: currentInteractionPoint
11265
+ });
11266
+ return;
11267
+ }
10861
11268
  if (st.tool === "arrow") {
10862
11269
  const screenDx = ev.clientX - st.startScreen.x;
10863
11270
  const screenDy = ev.clientY - st.startScreen.y;
10864
11271
  if (Math.hypot(screenDx, screenDy) < MIN_ARROW_DRAG_PX) {
11272
+ finishCanvuInteraction("cancelled", {
11273
+ current: currentInteractionPoint
11274
+ });
10865
11275
  return;
10866
11276
  }
10867
11277
  const maxDist = ARROW_BIND_SNAP_PX / cam.zoom;
@@ -10893,18 +11303,23 @@ var VectorViewport = forwardRef(
10893
11303
  ...snapB ? { end: snapB.binding } : {}
10894
11304
  };
10895
11305
  }
11306
+ const info2 = {
11307
+ motive: "place",
11308
+ itemIds: [id2],
11309
+ toolId: st.tool
11310
+ };
10896
11311
  change(
10897
11312
  [
10898
11313
  ...itemsRef.current,
10899
11314
  createLineItem(id2, rawArrow, line, "arrow", pen2, arrowBind)
10900
11315
  ],
10901
- {
10902
- motive: "place",
10903
- itemIds: [id2],
10904
- toolId: st.tool
10905
- }
11316
+ info2
10906
11317
  );
10907
11318
  setEffectiveSelectedIdsRef.current([id2]);
11319
+ finishCanvuInteraction("completed", {
11320
+ current: currentInteractionPoint,
11321
+ info: info2
11322
+ });
10908
11323
  return;
10909
11324
  }
10910
11325
  let raw = rectFromCorners(a, b);
@@ -10929,47 +11344,61 @@ var VectorViewport = forwardRef(
10929
11344
  }
10930
11345
  const id = createShapeId();
10931
11346
  const pen = strokeStyleRef.current;
11347
+ let info;
10932
11348
  if (cpUp && st.tool === cpUp.toolId) {
10933
11349
  const item = tagCustomPlacementItem(
10934
11350
  cpUp.createItem({ id, bounds: br }),
10935
11351
  cpUp.toolId
10936
11352
  );
10937
- change(itemsRef.current.concat(item), {
11353
+ info = {
10938
11354
  motive: "place",
10939
11355
  itemIds: [id],
10940
11356
  toolId: st.tool
10941
- });
11357
+ };
11358
+ change(itemsRef.current.concat(item), info);
10942
11359
  if (cpUp.selectAfterCreate !== false) {
10943
11360
  setEffectiveSelectedIdsRef.current([id]);
10944
11361
  }
11362
+ finishCanvuInteraction("completed", {
11363
+ current: currentInteractionPoint,
11364
+ info
11365
+ });
10945
11366
  return;
10946
11367
  }
10947
11368
  if (st.tool === "rect") {
10948
- change([...itemsRef.current, createRectangleItem(id, raw, pen)], {
11369
+ info = {
10949
11370
  motive: "place",
10950
11371
  itemIds: [id],
10951
11372
  toolId: st.tool
10952
- });
11373
+ };
11374
+ change([...itemsRef.current, createRectangleItem(id, raw, pen)], info);
10953
11375
  setEffectiveSelectedIdsRef.current([id]);
10954
11376
  } else if (st.tool === "ellipse") {
10955
- change([...itemsRef.current, createEllipseItem(id, raw, pen)], {
11377
+ info = {
10956
11378
  motive: "place",
10957
11379
  itemIds: [id],
10958
11380
  toolId: st.tool
10959
- });
11381
+ };
11382
+ change([...itemsRef.current, createEllipseItem(id, raw, pen)], info);
10960
11383
  setEffectiveSelectedIdsRef.current([id]);
10961
11384
  } else if (st.tool === "architectural-cloud") {
11385
+ info = {
11386
+ motive: "place",
11387
+ itemIds: [id],
11388
+ toolId: st.tool
11389
+ };
10962
11390
  change(
10963
11391
  [...itemsRef.current, createArchitecturalCloudItem(id, raw, pen)],
10964
- {
10965
- motive: "place",
10966
- itemIds: [id],
10967
- toolId: st.tool
10968
- }
11392
+ info
10969
11393
  );
10970
11394
  setEffectiveSelectedIdsRef.current([id]);
10971
11395
  } else if (st.tool === "line" || st.tool === "arrow") {
10972
11396
  const line = lineEndpointsToLocal(raw, lineA, lineB);
11397
+ info = {
11398
+ motive: "place",
11399
+ itemIds: [id],
11400
+ toolId: st.tool
11401
+ };
10973
11402
  change(
10974
11403
  [
10975
11404
  ...itemsRef.current,
@@ -10981,15 +11410,15 @@ var VectorViewport = forwardRef(
10981
11410
  pen
10982
11411
  )
10983
11412
  ],
10984
- {
10985
- motive: "place",
10986
- itemIds: [id],
10987
- toolId: st.tool
10988
- }
11413
+ info
10989
11414
  );
10990
11415
  setEffectiveSelectedIdsRef.current([id]);
10991
11416
  }
10992
11417
  requestAutoResetTool(st.tool);
11418
+ finishCanvuInteraction("completed", {
11419
+ current: currentInteractionPoint,
11420
+ ...info ? { info } : {}
11421
+ });
10993
11422
  }
10994
11423
  };
10995
11424
  document.addEventListener("pointermove", onMove);
@@ -11007,11 +11436,13 @@ var VectorViewport = forwardRef(
11007
11436
  pruneEraserTrail,
11008
11437
  pruneLaserTrail,
11009
11438
  finalizeStrokeDragState,
11439
+ finishCanvuInteraction,
11010
11440
  renderSceneWithLivePenStroke,
11011
11441
  releaseInteractionPointer,
11012
11442
  requestAutoResetTool,
11013
11443
  screenToWorld,
11014
11444
  setStraightStrokeEndpoint,
11445
+ updateCanvuInteractionCurrent,
11015
11446
  updateStraightStrokeForMove
11016
11447
  ]);
11017
11448
  const selectedItemsForOverlay = useMemo(() => {