canvu-react 0.4.63 → 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
@@ -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
  }
@@ -8143,6 +8167,8 @@ var VectorViewport = react.forwardRef(
8143
8167
  selectedIds: selectedIdsProp,
8144
8168
  onSelectionChange,
8145
8169
  onItemsChange: consumerOnItemsChange,
8170
+ onBeforeInteraction: consumerOnBeforeInteraction,
8171
+ onAfterInteraction: consumerOnAfterInteraction,
8146
8172
  onActivateLink,
8147
8173
  onWorldPointerDown: consumerOnWorldPointerDown,
8148
8174
  toolbar,
@@ -8268,6 +8294,24 @@ var VectorViewport = react.forwardRef(
8268
8294
  (contribution) => contribution.callbacks?.onCameraChange
8269
8295
  )
8270
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
+ );
8271
8315
  const onItemsChange = react.useMemo(() => {
8272
8316
  const middlewares = orderedPluginContributions.map((contribution) => contribution.wrapOnItemsChange).filter(
8273
8317
  (middleware) => middleware != null
@@ -8411,6 +8455,81 @@ var VectorViewport = react.forwardRef(
8411
8455
  [items]
8412
8456
  );
8413
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
+ );
8414
8533
  onWorldPointerDownRef.current = onWorldPointerDown;
8415
8534
  const originalOnItemsChangeRef = react.useRef(onItemsChange);
8416
8535
  originalOnItemsChangeRef.current = onItemsChange;
@@ -8651,9 +8770,14 @@ var VectorViewport = react.forwardRef(
8651
8770
  );
8652
8771
  if (item) {
8653
8772
  const exists = itemsRef.current.some((it) => it.id === id);
8773
+ const info = {
8774
+ motive: "draw",
8775
+ itemIds: [id],
8776
+ toolId: args.tool
8777
+ };
8654
8778
  change(
8655
8779
  exists ? replaceItem(itemsRef.current, id, item) : [...itemsRef.current, item],
8656
- { motive: "draw", itemIds: [id], toolId: args.tool }
8780
+ info
8657
8781
  );
8658
8782
  patchCurrentStrokeStyle({
8659
8783
  stroke: item.stroke ?? DEFAULT_STROKE_STYLE.stroke,
@@ -8661,10 +8785,15 @@ var VectorViewport = react.forwardRef(
8661
8785
  strokeOpacity: item.strokeOpacity,
8662
8786
  strokeDash: item.strokeDash
8663
8787
  });
8788
+ if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8789
+ requestAutoResetTool(args.tool);
8790
+ }
8791
+ return info;
8664
8792
  }
8665
8793
  if (!shouldKeepToolForContinuousPenInput(args.tool, args.pointerType)) {
8666
8794
  requestAutoResetTool(args.tool);
8667
8795
  }
8796
+ return void 0;
8668
8797
  },
8669
8798
  [
8670
8799
  patchCurrentStrokeStyle,
@@ -8795,17 +8924,19 @@ var VectorViewport = react.forwardRef(
8795
8924
  }
8796
8925
  emitRemoteStrokePreviewClear();
8797
8926
  setPlacementPreview(null);
8798
- commitCompletedStroke({
8927
+ const info = commitCompletedStroke({
8799
8928
  tool,
8800
8929
  pointerType,
8801
8930
  points: [...pts],
8802
8931
  style,
8803
8932
  itemId
8804
8933
  });
8934
+ finishCanvuInteraction("completed", { info });
8805
8935
  },
8806
8936
  [
8807
8937
  commitCompletedStroke,
8808
8938
  emitRemoteStrokePreviewClear,
8939
+ finishCanvuInteraction,
8809
8940
  releaseInteractionPointer,
8810
8941
  renderSceneWithLivePenStroke
8811
8942
  ]
@@ -9875,12 +10006,26 @@ var VectorViewport = react.forwardRef(
9875
10006
  if (!raw) return void 0;
9876
10007
  return raw.toolKind === "arrow" && raw.arrowBind ? bakeArrowItemToAbsolute(raw, canonical) : raw;
9877
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
+ };
9878
10028
  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
10029
  const toErase = collectEraserTargetsAtWorldPoint(
9885
10030
  resolved,
9886
10031
  worldX,
@@ -9890,6 +10035,15 @@ var VectorViewport = react.forwardRef(
9890
10035
  ignoreLocked: true
9891
10036
  }
9892
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);
9893
10047
  if (toErase.length > 0) {
9894
10048
  for (const id of toErase) {
9895
10049
  eraserPreviewIdsRef.current.add(id);
@@ -9922,6 +10076,10 @@ var VectorViewport = react.forwardRef(
9922
10076
  const snapSpin = bakedSnapshot(selected.id);
9923
10077
  if (!snapSpin) return;
9924
10078
  const pivot = itemPivotWorld(selected);
10079
+ if (!startInteraction("rotate", [selected.id])) {
10080
+ stopHandledInteraction();
10081
+ return;
10082
+ }
9925
10083
  dragStateRef.current = {
9926
10084
  kind: "rotate",
9927
10085
  id: selected.id,
@@ -9946,6 +10104,10 @@ var VectorViewport = react.forwardRef(
9946
10104
  if (hb) {
9947
10105
  const snapRs = bakedSnapshot(selected.id);
9948
10106
  if (!snapRs) return;
10107
+ if (!startInteraction("resize", [selected.id])) {
10108
+ stopHandledInteraction();
10109
+ return;
10110
+ }
9949
10111
  dragStateRef.current = {
9950
10112
  kind: "resize",
9951
10113
  id: selected.id,
@@ -9978,6 +10140,10 @@ var VectorViewport = react.forwardRef(
9978
10140
  moveSnapshots[id] = snap;
9979
10141
  }
9980
10142
  }
10143
+ if (!startInteraction("select-mode-item-click", [hit.id])) {
10144
+ stopHandledInteraction();
10145
+ return;
10146
+ }
9981
10147
  dragStateRef.current = {
9982
10148
  kind: "select-mode-item-click",
9983
10149
  id: hit.id,
@@ -9994,7 +10160,14 @@ var VectorViewport = react.forwardRef(
9994
10160
  }
9995
10161
  if (e.shiftKey) {
9996
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
+ }
9997
10167
  setEffectiveSelectedIdsRef.current(next);
10168
+ finishCanvuInteraction("completed", {
10169
+ info: { motive: "custom", itemIds: [hit.id], toolId: tool }
10170
+ });
9998
10171
  e.preventDefault();
9999
10172
  e.stopPropagation();
10000
10173
  return;
@@ -10002,7 +10175,6 @@ var VectorViewport = react.forwardRef(
10002
10175
  let idsToMove;
10003
10176
  if (!cur.includes(hit.id)) {
10004
10177
  idsToMove = [hit.id];
10005
- setEffectiveSelectedIdsRef.current(idsToMove);
10006
10178
  } else {
10007
10179
  idsToMove = [...cur];
10008
10180
  }
@@ -10013,6 +10185,13 @@ var VectorViewport = react.forwardRef(
10013
10185
  snapshots[id] = snap;
10014
10186
  }
10015
10187
  }
10188
+ if (!startInteraction("move", idsToMove)) {
10189
+ stopHandledInteraction();
10190
+ return;
10191
+ }
10192
+ if (!cur.includes(hit.id)) {
10193
+ setEffectiveSelectedIdsRef.current(idsToMove);
10194
+ }
10016
10195
  dragStateRef.current = {
10017
10196
  kind: "move",
10018
10197
  ids: idsToMove,
@@ -10037,9 +10216,14 @@ var VectorViewport = react.forwardRef(
10037
10216
  }
10038
10217
  }
10039
10218
  if (Object.keys(snapshots).length > 0) {
10219
+ const moveIds = Object.keys(snapshots);
10220
+ if (!startInteraction("move", moveIds)) {
10221
+ stopHandledInteraction();
10222
+ return;
10223
+ }
10040
10224
  dragStateRef.current = {
10041
10225
  kind: "move",
10042
- ids: Object.keys(snapshots),
10226
+ ids: moveIds,
10043
10227
  snapshots,
10044
10228
  startWorld: { x: worldX, y: worldY }
10045
10229
  };
@@ -10050,6 +10234,10 @@ var VectorViewport = react.forwardRef(
10050
10234
  }
10051
10235
  }
10052
10236
  }
10237
+ if (!startInteraction("marquee")) {
10238
+ stopHandledInteraction();
10239
+ return;
10240
+ }
10053
10241
  dragStateRef.current = {
10054
10242
  kind: "marquee",
10055
10243
  startWorld: { x: worldX, y: worldY },
@@ -10078,6 +10266,10 @@ var VectorViewport = react.forwardRef(
10078
10266
  );
10079
10267
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10080
10268
  const directPenStroke = e.pointerType === "pen" && (tool === "draw" || tool === "marker");
10269
+ if (!startInteraction("stroke")) {
10270
+ stopHandledInteraction();
10271
+ return;
10272
+ }
10081
10273
  let itemId;
10082
10274
  if (directPenStroke) {
10083
10275
  itemId = createShapeId();
@@ -10121,6 +10313,10 @@ var VectorViewport = react.forwardRef(
10121
10313
  return;
10122
10314
  }
10123
10315
  if (tool === "text" || tool === "image") {
10316
+ if (!startInteraction("tap")) {
10317
+ stopHandledInteraction();
10318
+ return;
10319
+ }
10124
10320
  dragStateRef.current = {
10125
10321
  kind: "tap",
10126
10322
  tool,
@@ -10134,6 +10330,10 @@ var VectorViewport = react.forwardRef(
10134
10330
  }
10135
10331
  const cp = customPlacementRef.current;
10136
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
+ }
10137
10337
  dragStateRef.current = {
10138
10338
  kind: "place",
10139
10339
  tool,
@@ -10151,8 +10351,10 @@ var VectorViewport = react.forwardRef(
10151
10351
  captureInteractionPointer,
10152
10352
  emitRemoteStrokePreview,
10153
10353
  finalizeStrokeDragState,
10354
+ finishCanvuInteraction,
10154
10355
  renderSceneWithLivePenStroke,
10155
10356
  screenToWorld,
10357
+ startCanvuInteraction,
10156
10358
  startOrRestartStraightStrokeHoldTimer
10157
10359
  ]
10158
10360
  );
@@ -10181,14 +10383,32 @@ var VectorViewport = react.forwardRef(
10181
10383
  e.stopImmediatePropagation();
10182
10384
  return;
10183
10385
  }
10184
- wrapperRef.current?.focus({ preventScroll: true });
10185
- setContextMenu(null);
10186
10386
  const startPoint = pointerSampleToWorldPoint(
10187
10387
  screenToWorld,
10188
10388
  e.clientX,
10189
10389
  e.clientY,
10190
10390
  e.pressure
10191
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);
10192
10412
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, e.clientX, e.clientY) : void 0;
10193
10413
  const itemId = createShapeId();
10194
10414
  const item = createFreehandStrokeItem(
@@ -10236,6 +10456,7 @@ var VectorViewport = react.forwardRef(
10236
10456
  interactive,
10237
10457
  renderSceneWithLivePenStroke,
10238
10458
  screenToWorld,
10459
+ startCanvuInteraction,
10239
10460
  startOrRestartStraightStrokeHoldTimer
10240
10461
  ]);
10241
10462
  react.useEffect(() => {
@@ -10286,15 +10507,32 @@ var VectorViewport = react.forwardRef(
10286
10507
  stopTouchEvent(ev);
10287
10508
  return;
10288
10509
  }
10289
- wrapperRef.current?.focus({ preventScroll: true });
10290
- setContextMenu(null);
10291
- penDetectedRef.current = true;
10292
10510
  const startPoint = pointerSampleToWorldPoint(
10293
10511
  screenToWorld,
10294
10512
  touch.clientX,
10295
10513
  touch.clientY,
10296
10514
  touchPressure(touch)
10297
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;
10298
10536
  const straightLine = tool === "draw" ? createStraightStrokeState(startPoint, touch.clientX, touch.clientY) : void 0;
10299
10537
  const itemId = createShapeId();
10300
10538
  const item = createFreehandStrokeItem(
@@ -10340,6 +10578,12 @@ var VectorViewport = react.forwardRef(
10340
10578
  touch.clientY,
10341
10579
  touchPressure(touch)
10342
10580
  );
10581
+ updateCanvuInteractionCurrent({
10582
+ worldX: endpoint.x,
10583
+ worldY: endpoint.y,
10584
+ clientX: touch.clientX,
10585
+ clientY: touch.clientY
10586
+ });
10343
10587
  if (updateStraightStrokeForMove(st, touch.clientX, touch.clientY, endpoint)) {
10344
10588
  debugApplePencilPointer("touchmove-stroke", {
10345
10589
  touchId: touch.identifier,
@@ -10381,16 +10625,42 @@ var VectorViewport = react.forwardRef(
10381
10625
  if (st.kind !== "stroke") return;
10382
10626
  const touch = findChangedTouch(ev.changedTouches);
10383
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
+ }
10384
10660
  const cam = cameraRef.current;
10385
10661
  if (cam) {
10386
10662
  if (st.straightLine?.active) {
10387
- const endpoint = pointerSampleToWorldPoint(
10388
- screenToWorld,
10389
- touch.clientX,
10390
- touch.clientY,
10391
- touchPressure(touch)
10392
- );
10393
- setStraightStrokeEndpoint(st, endpoint);
10663
+ setStraightStrokeEndpoint(st, currentPoint);
10394
10664
  } else {
10395
10665
  const completedPoints = appendTouchToStrokePoints(
10396
10666
  st.points,
@@ -10441,12 +10711,17 @@ var VectorViewport = react.forwardRef(
10441
10711
  }, [
10442
10712
  applePencilNav,
10443
10713
  emitRemoteStrokePreview,
10714
+ emitRemoteStrokePreviewClear,
10444
10715
  finalizeStrokeDragState,
10716
+ finishCanvuInteraction,
10445
10717
  interactive,
10718
+ releaseInteractionPointer,
10446
10719
  renderSceneWithLivePenStroke,
10447
10720
  screenToWorld,
10448
10721
  setStraightStrokeEndpoint,
10722
+ startCanvuInteraction,
10449
10723
  startOrRestartStraightStrokeHoldTimer,
10724
+ updateCanvuInteractionCurrent,
10450
10725
  updateStraightStrokeForMove
10451
10726
  ]);
10452
10727
  react.useEffect(() => {
@@ -10459,6 +10734,12 @@ var VectorViewport = react.forwardRef(
10459
10734
  if (st.kind === "tap") return;
10460
10735
  if (st.kind === "marquee") {
10461
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
+ });
10462
10743
  const raw = rectFromCorners(st.startWorld, { x: worldX2, y: worldY2 });
10463
10744
  setPlacementPreview({ kind: "marquee", rect: raw });
10464
10745
  const nextCand = collectItemIdsInRect(
@@ -10486,6 +10767,12 @@ var VectorViewport = react.forwardRef(
10486
10767
  ev.clientY,
10487
10768
  ev.pointerType === "pen" ? ev.pressure : void 0
10488
10769
  );
10770
+ updateCanvuInteractionCurrent({
10771
+ worldX: endpoint.x,
10772
+ worldY: endpoint.y,
10773
+ clientX: ev.clientX,
10774
+ clientY: ev.clientY
10775
+ });
10489
10776
  if (updateStraightStrokeForMove(st, ev.clientX, ev.clientY, endpoint)) {
10490
10777
  return;
10491
10778
  }
@@ -10534,6 +10821,12 @@ var VectorViewport = react.forwardRef(
10534
10821
  }
10535
10822
  if (st.kind === "erase") {
10536
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
+ });
10537
10830
  const lineHitWorld = 10 / cam.zoom;
10538
10831
  setEraserTrail(
10539
10832
  (prev) => pruneEraserTrail([...prev, { x: worldX2, y: worldY2, t: Date.now() }])
@@ -10559,6 +10852,12 @@ var VectorViewport = react.forwardRef(
10559
10852
  const change = onItemsChangeRef.current;
10560
10853
  if (!change) return;
10561
10854
  const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
10855
+ updateCanvuInteractionCurrent({
10856
+ worldX,
10857
+ worldY,
10858
+ clientX: ev.clientX,
10859
+ clientY: ev.clientY
10860
+ });
10562
10861
  if (st.kind === "select-mode-item-click") {
10563
10862
  const screenDx = ev.clientX - st.startScreen.x;
10564
10863
  const screenDy = ev.clientY - st.startScreen.y;
@@ -10691,29 +10990,64 @@ var VectorViewport = react.forwardRef(
10691
10990
  setPlacementPreview(null);
10692
10991
  marqueeCandidateIdsRef.current = [];
10693
10992
  setMarqueeCandidateIds([]);
10993
+ finishCanvuInteraction("cancelled");
10694
10994
  return;
10695
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);
10696
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] };
10697
11007
  dragStateRef.current = { kind: "idle" };
10698
11008
  releaseInteractionPointer();
11009
+ finishCanvuInteraction(finishOutcome, {
11010
+ current: currentInteractionPoint,
11011
+ ...finishOutcome === "completed" ? { info } : {}
11012
+ });
10699
11013
  return;
10700
11014
  }
10701
11015
  if (st.kind === "select-mode-item-click") {
10702
11016
  dragStateRef.current = { kind: "idle" };
10703
11017
  releaseInteractionPointer();
10704
- if (ev.type === "pointercancel") return;
11018
+ if (finishOutcome === "cancelled") {
11019
+ finishCanvuInteraction("cancelled", {
11020
+ current: currentInteractionPoint
11021
+ });
11022
+ return;
11023
+ }
10705
11024
  const dx = ev.clientX - st.startScreen.x;
10706
11025
  const dy = ev.clientY - st.startScreen.y;
10707
- 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
+ }
10708
11032
  const item = itemsRef.current.find((candidate) => candidate.id === st.id) ?? resolvedItemsRef.current.find((candidate) => candidate.id === st.id);
10709
- if (!item) return;
11033
+ if (!item) {
11034
+ finishCanvuInteraction("cancelled", {
11035
+ current: currentInteractionPoint
11036
+ });
11037
+ return;
11038
+ }
10710
11039
  const placement = findSelectModeItemClickPlacement(
10711
11040
  item,
10712
11041
  allCustomPlacementsRef.current
10713
11042
  );
10714
11043
  const onSelectModeItemClick = placement?.onSelectModeItemClick;
10715
- if (!onSelectModeItemClick) return;
10716
- 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;
10717
11051
  onSelectModeItemClick({
10718
11052
  item,
10719
11053
  worldX,
@@ -10734,6 +11068,10 @@ var VectorViewport = react.forwardRef(
10734
11068
  },
10735
11069
  setSelectedIds: (ids) => setEffectiveSelectedIdsRef.current(ids)
10736
11070
  });
11071
+ finishCanvuInteraction("completed", {
11072
+ current: currentInteractionPoint,
11073
+ info: { motive: "custom", itemIds: [item.id], toolId: "select" }
11074
+ });
10737
11075
  return;
10738
11076
  }
10739
11077
  if (st.kind === "marquee") {
@@ -10742,16 +11080,25 @@ var VectorViewport = react.forwardRef(
10742
11080
  setPlacementPreview(null);
10743
11081
  marqueeCandidateIdsRef.current = [];
10744
11082
  setMarqueeCandidateIds([]);
10745
- const { worldX, worldY } = screenToWorld(ev.clientX, ev.clientY);
11083
+ const { worldX, worldY } = currentWorld;
10746
11084
  const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
10747
11085
  const br = normalizeRect(raw);
10748
11086
  const screenDx = ev.clientX - st.startScreen.x;
10749
11087
  const screenDy = ev.clientY - st.startScreen.y;
11088
+ if (finishOutcome === "cancelled") {
11089
+ finishCanvuInteraction("cancelled", {
11090
+ current: currentInteractionPoint
11091
+ });
11092
+ return;
11093
+ }
10750
11094
  const tooSmall = Math.hypot(screenDx, screenDy) < TAP_PX || br.width < MIN_MARQUEE_WORLD && br.height < MIN_MARQUEE_WORLD;
10751
11095
  if (tooSmall) {
10752
11096
  if (!st.shiftKey) {
10753
11097
  setEffectiveSelectedIdsRef.current([]);
10754
11098
  }
11099
+ finishCanvuInteraction("cancelled", {
11100
+ current: currentInteractionPoint
11101
+ });
10755
11102
  return;
10756
11103
  }
10757
11104
  const picked = collectItemIdsInRect(resolvedItemsRef.current, br);
@@ -10768,9 +11115,26 @@ var VectorViewport = react.forwardRef(
10768
11115
  } else {
10769
11116
  setEffectiveSelectedIdsRef.current(picked);
10770
11117
  }
11118
+ finishCanvuInteraction("completed", {
11119
+ current: currentInteractionPoint
11120
+ });
10771
11121
  return;
10772
11122
  }
10773
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
+ }
10774
11138
  const completedPoints = (() => {
10775
11139
  if (st.straightLine?.active) {
10776
11140
  const endpoint = pointerSampleToWorldPoint(
@@ -10801,15 +11165,20 @@ var VectorViewport = react.forwardRef(
10801
11165
  }
10802
11166
  if (st.kind === "erase") {
10803
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;
10804
11174
  if (change && eraserPreviewIdsRef.current.size > 0) {
10805
11175
  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
- );
11176
+ if (finishOutcome === "completed") {
11177
+ change(
11178
+ itemsRef.current.filter((i) => !idSet.has(i.id)),
11179
+ info
11180
+ );
11181
+ }
10813
11182
  }
10814
11183
  eraserPreviewIdsRef.current.clear();
10815
11184
  setEraserPreviewIds([]);
@@ -10817,7 +11186,13 @@ var VectorViewport = react.forwardRef(
10817
11186
  setEraserActive(false);
10818
11187
  dragStateRef.current = { kind: "idle" };
10819
11188
  releaseInteractionPointer();
10820
- requestAutoResetTool("eraser");
11189
+ if (finishOutcome === "completed") {
11190
+ requestAutoResetTool("eraser");
11191
+ }
11192
+ finishCanvuInteraction(finishOutcome, {
11193
+ current: currentInteractionPoint,
11194
+ ...finishOutcome === "completed" && info ? { info } : {}
11195
+ });
10821
11196
  return;
10822
11197
  }
10823
11198
  if (st.kind === "tap") {
@@ -10825,11 +11200,22 @@ var VectorViewport = react.forwardRef(
10825
11200
  const dy = ev.clientY - st.startScreen.y;
10826
11201
  dragStateRef.current = { kind: "idle" };
10827
11202
  releaseInteractionPointer();
10828
- 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
+ }
10829
11209
  const change = onItemsChangeRef.current;
10830
- if (!change) return;
11210
+ if (!change) {
11211
+ finishCanvuInteraction("cancelled", {
11212
+ current: currentInteractionPoint
11213
+ });
11214
+ return;
11215
+ }
10831
11216
  const id = createShapeId();
10832
11217
  const { x: worldX, y: worldY } = st.startWorld;
11218
+ let info;
10833
11219
  if (st.tool === "text") {
10834
11220
  const fs = strokeStyleRef.current.textFontSize;
10835
11221
  const baseline = textBaselineYFor(fs);
@@ -10852,11 +11238,12 @@ var VectorViewport = react.forwardRef(
10852
11238
  bounds: { ...newItem.bounds }
10853
11239
  };
10854
11240
  const hidden = applyTextDraftWhileEditing(newItem, "");
10855
- change([...itemsRef.current, hidden], {
11241
+ info = {
10856
11242
  motive: "text-create",
10857
11243
  itemIds: [id],
10858
11244
  toolId: st.tool
10859
- });
11245
+ };
11246
+ change([...itemsRef.current, hidden], info);
10860
11247
  setEffectiveSelectedIdsRef.current([id]);
10861
11248
  setEditingTextId(id);
10862
11249
  setDraftText("");
@@ -10865,6 +11252,10 @@ var VectorViewport = react.forwardRef(
10865
11252
  imageInputRef.current?.click();
10866
11253
  }
10867
11254
  requestAutoResetTool(st.tool);
11255
+ finishCanvuInteraction("completed", {
11256
+ current: currentInteractionPoint,
11257
+ ...info ? { info } : {}
11258
+ });
10868
11259
  return;
10869
11260
  }
10870
11261
  if (st.kind === "place") {
@@ -10875,11 +11266,19 @@ var VectorViewport = react.forwardRef(
10875
11266
  dragStateRef.current = { kind: "idle" };
10876
11267
  releaseInteractionPointer();
10877
11268
  setPlacementPreview(null);
10878
- if (!change) return;
11269
+ if (finishOutcome === "cancelled" || !change) {
11270
+ finishCanvuInteraction("cancelled", {
11271
+ current: currentInteractionPoint
11272
+ });
11273
+ return;
11274
+ }
10879
11275
  if (st.tool === "arrow") {
10880
11276
  const screenDx = ev.clientX - st.startScreen.x;
10881
11277
  const screenDy = ev.clientY - st.startScreen.y;
10882
11278
  if (Math.hypot(screenDx, screenDy) < MIN_ARROW_DRAG_PX) {
11279
+ finishCanvuInteraction("cancelled", {
11280
+ current: currentInteractionPoint
11281
+ });
10883
11282
  return;
10884
11283
  }
10885
11284
  const maxDist = ARROW_BIND_SNAP_PX / cam.zoom;
@@ -10911,18 +11310,23 @@ var VectorViewport = react.forwardRef(
10911
11310
  ...snapB ? { end: snapB.binding } : {}
10912
11311
  };
10913
11312
  }
11313
+ const info2 = {
11314
+ motive: "place",
11315
+ itemIds: [id2],
11316
+ toolId: st.tool
11317
+ };
10914
11318
  change(
10915
11319
  [
10916
11320
  ...itemsRef.current,
10917
11321
  createLineItem(id2, rawArrow, line, "arrow", pen2, arrowBind)
10918
11322
  ],
10919
- {
10920
- motive: "place",
10921
- itemIds: [id2],
10922
- toolId: st.tool
10923
- }
11323
+ info2
10924
11324
  );
10925
11325
  setEffectiveSelectedIdsRef.current([id2]);
11326
+ finishCanvuInteraction("completed", {
11327
+ current: currentInteractionPoint,
11328
+ info: info2
11329
+ });
10926
11330
  return;
10927
11331
  }
10928
11332
  let raw = rectFromCorners(a, b);
@@ -10947,47 +11351,61 @@ var VectorViewport = react.forwardRef(
10947
11351
  }
10948
11352
  const id = createShapeId();
10949
11353
  const pen = strokeStyleRef.current;
11354
+ let info;
10950
11355
  if (cpUp && st.tool === cpUp.toolId) {
10951
11356
  const item = tagCustomPlacementItem(
10952
11357
  cpUp.createItem({ id, bounds: br }),
10953
11358
  cpUp.toolId
10954
11359
  );
10955
- change(itemsRef.current.concat(item), {
11360
+ info = {
10956
11361
  motive: "place",
10957
11362
  itemIds: [id],
10958
11363
  toolId: st.tool
10959
- });
11364
+ };
11365
+ change(itemsRef.current.concat(item), info);
10960
11366
  if (cpUp.selectAfterCreate !== false) {
10961
11367
  setEffectiveSelectedIdsRef.current([id]);
10962
11368
  }
11369
+ finishCanvuInteraction("completed", {
11370
+ current: currentInteractionPoint,
11371
+ info
11372
+ });
10963
11373
  return;
10964
11374
  }
10965
11375
  if (st.tool === "rect") {
10966
- change([...itemsRef.current, createRectangleItem(id, raw, pen)], {
11376
+ info = {
10967
11377
  motive: "place",
10968
11378
  itemIds: [id],
10969
11379
  toolId: st.tool
10970
- });
11380
+ };
11381
+ change([...itemsRef.current, createRectangleItem(id, raw, pen)], info);
10971
11382
  setEffectiveSelectedIdsRef.current([id]);
10972
11383
  } else if (st.tool === "ellipse") {
10973
- change([...itemsRef.current, createEllipseItem(id, raw, pen)], {
11384
+ info = {
10974
11385
  motive: "place",
10975
11386
  itemIds: [id],
10976
11387
  toolId: st.tool
10977
- });
11388
+ };
11389
+ change([...itemsRef.current, createEllipseItem(id, raw, pen)], info);
10978
11390
  setEffectiveSelectedIdsRef.current([id]);
10979
11391
  } else if (st.tool === "architectural-cloud") {
11392
+ info = {
11393
+ motive: "place",
11394
+ itemIds: [id],
11395
+ toolId: st.tool
11396
+ };
10980
11397
  change(
10981
11398
  [...itemsRef.current, createArchitecturalCloudItem(id, raw, pen)],
10982
- {
10983
- motive: "place",
10984
- itemIds: [id],
10985
- toolId: st.tool
10986
- }
11399
+ info
10987
11400
  );
10988
11401
  setEffectiveSelectedIdsRef.current([id]);
10989
11402
  } else if (st.tool === "line" || st.tool === "arrow") {
10990
11403
  const line = lineEndpointsToLocal(raw, lineA, lineB);
11404
+ info = {
11405
+ motive: "place",
11406
+ itemIds: [id],
11407
+ toolId: st.tool
11408
+ };
10991
11409
  change(
10992
11410
  [
10993
11411
  ...itemsRef.current,
@@ -10999,15 +11417,15 @@ var VectorViewport = react.forwardRef(
10999
11417
  pen
11000
11418
  )
11001
11419
  ],
11002
- {
11003
- motive: "place",
11004
- itemIds: [id],
11005
- toolId: st.tool
11006
- }
11420
+ info
11007
11421
  );
11008
11422
  setEffectiveSelectedIdsRef.current([id]);
11009
11423
  }
11010
11424
  requestAutoResetTool(st.tool);
11425
+ finishCanvuInteraction("completed", {
11426
+ current: currentInteractionPoint,
11427
+ ...info ? { info } : {}
11428
+ });
11011
11429
  }
11012
11430
  };
11013
11431
  document.addEventListener("pointermove", onMove);
@@ -11025,11 +11443,13 @@ var VectorViewport = react.forwardRef(
11025
11443
  pruneEraserTrail,
11026
11444
  pruneLaserTrail,
11027
11445
  finalizeStrokeDragState,
11446
+ finishCanvuInteraction,
11028
11447
  renderSceneWithLivePenStroke,
11029
11448
  releaseInteractionPointer,
11030
11449
  requestAutoResetTool,
11031
11450
  screenToWorld,
11032
11451
  setStraightStrokeEndpoint,
11452
+ updateCanvuInteractionCurrent,
11033
11453
  updateStraightStrokeForMove
11034
11454
  ]);
11035
11455
  const selectedItemsForOverlay = react.useMemo(() => {