canvu-react 0.4.17 → 0.4.19

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/native.js CHANGED
@@ -940,6 +940,10 @@ function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
940
940
  const ly = sin * dx + cos * dy;
941
941
  return { x: c.x + lx, y: c.y + ly };
942
942
  }
943
+ function itemPivotWorld(item) {
944
+ const r = normalizeRect(item.bounds);
945
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
946
+ }
943
947
  function boundsAabbForRotatedItem(item) {
944
948
  const rot = getItemRotationRad(item);
945
949
  if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
@@ -970,6 +974,7 @@ function boundsAabbForRotatedItem(item) {
970
974
  }
971
975
 
972
976
  // src/interaction/resize-handles.ts
977
+ var HANDLE_IDS = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
973
978
  function getHandleWorldPosition(bounds, id) {
974
979
  const r = normalizeRect(bounds);
975
980
  const cx = r.x + r.width / 2;
@@ -993,6 +998,30 @@ function getHandleWorldPosition(bounds, id) {
993
998
  return { x: r.x, y: cy };
994
999
  }
995
1000
  }
1001
+ function hitTestResizeHandle(bounds, worldX, worldY, radiusWorld, rotationRad = 0) {
1002
+ const r = normalizeRect(bounds);
1003
+ const pl = worldToItemLocal(
1004
+ worldX,
1005
+ worldY,
1006
+ r.x,
1007
+ r.y,
1008
+ r.width,
1009
+ r.height,
1010
+ rotationRad
1011
+ );
1012
+ const localBounds2 = { x: 0, y: 0, width: r.width, height: r.height };
1013
+ let best = null;
1014
+ let bestD = radiusWorld;
1015
+ for (const id of HANDLE_IDS) {
1016
+ const p = getHandleWorldPosition(localBounds2, id);
1017
+ const d = Math.hypot(pl.x - p.x, pl.y - p.y);
1018
+ if (d <= bestD) {
1019
+ bestD = d;
1020
+ best = id;
1021
+ }
1022
+ }
1023
+ return best;
1024
+ }
996
1025
  function getHandleWorldPositionRotated(bounds, handle, rotationRad) {
997
1026
  const r = normalizeRect(bounds);
998
1027
  const p = getHandleWorldPosition(
@@ -1013,6 +1042,10 @@ function getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld)
1013
1042
  rotationRad
1014
1043
  );
1015
1044
  }
1045
+ function hitTestRotateHandle(bounds, rotationRad, worldX, worldY, radiusWorld, handleOffsetWorld) {
1046
+ const p = getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld);
1047
+ return Math.hypot(worldX - p.x, worldY - p.y) <= radiusWorld;
1048
+ }
1016
1049
  function rectFromCorners(a, b) {
1017
1050
  const minX = Math.min(a.x, b.x);
1018
1051
  const maxX = Math.max(a.x, b.x);
@@ -1020,6 +1053,155 @@ function rectFromCorners(a, b) {
1020
1053
  const maxY = Math.max(a.y, b.y);
1021
1054
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1022
1055
  }
1056
+ function clampMinSize(r, min) {
1057
+ const n = normalizeRect(r);
1058
+ return {
1059
+ x: n.x,
1060
+ y: n.y,
1061
+ width: Math.max(min, n.width),
1062
+ height: Math.max(min, n.height)
1063
+ };
1064
+ }
1065
+ function computeResizeBounds(bounds, handle, currentWorld) {
1066
+ const r = normalizeRect(bounds);
1067
+ const x0 = r.x;
1068
+ const y0 = r.y;
1069
+ const x1 = r.x + r.width;
1070
+ const y1 = r.y + r.height;
1071
+ const minSize = 8;
1072
+ switch (handle) {
1073
+ case "nw":
1074
+ return clampMinSize(rectFromCorners(currentWorld, { x: x1, y: y1 }), minSize);
1075
+ case "ne":
1076
+ return clampMinSize(rectFromCorners(currentWorld, { x: x0, y: y1 }), minSize);
1077
+ case "se":
1078
+ return clampMinSize(rectFromCorners(currentWorld, { x: x0, y: y0 }), minSize);
1079
+ case "sw":
1080
+ return clampMinSize(rectFromCorners(currentWorld, { x: x1, y: y0 }), minSize);
1081
+ case "n":
1082
+ return clampMinSize(
1083
+ rectFromCorners({ x: x0, y: currentWorld.y }, { x: x1, y: y1 }),
1084
+ minSize
1085
+ );
1086
+ case "s":
1087
+ return clampMinSize(
1088
+ rectFromCorners({ x: x0, y: y0 }, { x: x1, y: currentWorld.y }),
1089
+ minSize
1090
+ );
1091
+ case "e":
1092
+ return clampMinSize(
1093
+ rectFromCorners({ x: x0, y: y0 }, { x: currentWorld.x, y: y1 }),
1094
+ minSize
1095
+ );
1096
+ case "w":
1097
+ return clampMinSize(
1098
+ rectFromCorners({ x: currentWorld.x, y: y0 }, { x: x1, y: y1 }),
1099
+ minSize
1100
+ );
1101
+ default:
1102
+ return r;
1103
+ }
1104
+ }
1105
+ function computeResizeBoundsFixedAspect(bounds, handle, currentWorld, aspect) {
1106
+ const r = normalizeRect(bounds);
1107
+ const x0 = r.x;
1108
+ const y0 = r.y;
1109
+ const x1 = r.x + r.width;
1110
+ const y1 = r.y + r.height;
1111
+ const w = r.width;
1112
+ const h = r.height;
1113
+ const minSize = 8;
1114
+ const a = Math.max(1e-9, aspect);
1115
+ function cornerAspectRect(anchor, cursor) {
1116
+ const dx = cursor.x - anchor.x;
1117
+ const dy = cursor.y - anchor.y;
1118
+ const aw = Math.abs(dx);
1119
+ const ah = Math.abs(dy);
1120
+ let W;
1121
+ let H;
1122
+ if (aw < 1e-12 && ah < 1e-12) {
1123
+ W = minSize;
1124
+ H = minSize / a;
1125
+ } else if (ah < 1e-12) {
1126
+ W = Math.max(minSize, aw);
1127
+ H = W / a;
1128
+ } else if (aw < 1e-12) {
1129
+ H = Math.max(minSize, ah);
1130
+ W = H * a;
1131
+ } else if (aw / ah > a) {
1132
+ W = aw;
1133
+ H = aw / a;
1134
+ } else {
1135
+ H = ah;
1136
+ W = ah * a;
1137
+ }
1138
+ if (W < minSize || H < minSize) {
1139
+ const scale = Math.max(minSize / W, minSize / H);
1140
+ W *= scale;
1141
+ H *= scale;
1142
+ }
1143
+ const sx = Math.sign(dx) || 1;
1144
+ const sy = Math.sign(dy) || 1;
1145
+ return rectFromCorners(anchor, {
1146
+ x: anchor.x + sx * W,
1147
+ y: anchor.y + sy * H
1148
+ });
1149
+ }
1150
+ switch (handle) {
1151
+ case "se":
1152
+ return cornerAspectRect({ x: x0, y: y0 }, currentWorld);
1153
+ case "nw":
1154
+ return cornerAspectRect({ x: x1, y: y1 }, currentWorld);
1155
+ case "ne":
1156
+ return cornerAspectRect({ x: x0, y: y1 }, currentWorld);
1157
+ case "sw":
1158
+ return cornerAspectRect({ x: x1, y: y0 }, currentWorld);
1159
+ case "e": {
1160
+ const rawW = currentWorld.x - x0;
1161
+ const aw = Math.max(minSize, Math.abs(rawW));
1162
+ const H = aw / a;
1163
+ const newY = y0 + h / 2 - H / 2;
1164
+ const sign = Math.sign(rawW) || 1;
1165
+ return normalizeRect({ x: x0, y: newY, width: sign * aw, height: H });
1166
+ }
1167
+ case "w": {
1168
+ const rawW = x1 - currentWorld.x;
1169
+ const aw = Math.max(minSize, Math.abs(rawW));
1170
+ const H = aw / a;
1171
+ const newY = y0 + h / 2 - H / 2;
1172
+ const sign = Math.sign(rawW) || 1;
1173
+ return normalizeRect({
1174
+ x: x1 - sign * aw,
1175
+ y: newY,
1176
+ width: sign * aw,
1177
+ height: H
1178
+ });
1179
+ }
1180
+ case "n": {
1181
+ const rawH = y1 - currentWorld.y;
1182
+ const ah = Math.max(minSize, Math.abs(rawH));
1183
+ const W = ah * a;
1184
+ const newX = x0 + w / 2 - W / 2;
1185
+ const sign = Math.sign(rawH) || 1;
1186
+ return normalizeRect({
1187
+ x: newX,
1188
+ y: y1 - sign * ah,
1189
+ width: W,
1190
+ height: sign * ah
1191
+ });
1192
+ }
1193
+ case "s": {
1194
+ const rawH = currentWorld.y - y0;
1195
+ const ah = Math.max(minSize, Math.abs(rawH));
1196
+ const W = ah * a;
1197
+ const newX = x0 + w / 2 - W / 2;
1198
+ const sign = Math.sign(rawH) || 1;
1199
+ return normalizeRect({ x: newX, y: y0, width: W, height: sign * ah });
1200
+ }
1201
+ default:
1202
+ return r;
1203
+ }
1204
+ }
1023
1205
 
1024
1206
  // src/scene/freehand-path.ts
1025
1207
  function smoothFreehandPointsToPathD(points) {
@@ -3581,6 +3763,211 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
3581
3763
  return ids;
3582
3764
  }
3583
3765
 
3766
+ // src/interaction/mutations.ts
3767
+ function computeNewBoundsForResize(item, sb, handle, currentWorld) {
3768
+ const rot = getItemRotationRad(item);
3769
+ if (Math.abs(rot) < 1e-12) {
3770
+ if (item.toolKind === "image") {
3771
+ let aspect;
3772
+ if (item.imageIntrinsicSize) {
3773
+ const iw = Math.max(1e-9, item.imageIntrinsicSize.width);
3774
+ const ih = Math.max(1e-9, item.imageIntrinsicSize.height);
3775
+ aspect = iw / ih;
3776
+ } else if (item.imageVectorLocalSize) {
3777
+ const lw = Math.max(1e-9, item.imageVectorLocalSize.width);
3778
+ const lh = Math.max(1e-9, item.imageVectorLocalSize.height);
3779
+ aspect = lw / lh;
3780
+ }
3781
+ return aspect !== void 0 ? computeResizeBoundsFixedAspect(sb, handle, currentWorld, aspect) : computeResizeBounds(sb, handle, currentWorld);
3782
+ }
3783
+ return computeResizeBounds(sb, handle, currentWorld);
3784
+ }
3785
+ const local = worldToItemLocal(
3786
+ currentWorld.x,
3787
+ currentWorld.y,
3788
+ sb.x,
3789
+ sb.y,
3790
+ sb.width,
3791
+ sb.height,
3792
+ rot
3793
+ );
3794
+ const localBounds2 = { x: 0, y: 0, width: sb.width, height: sb.height };
3795
+ let nbLocal;
3796
+ if (item.toolKind === "image") {
3797
+ let aspect;
3798
+ if (item.imageIntrinsicSize) {
3799
+ const iw = Math.max(1e-9, item.imageIntrinsicSize.width);
3800
+ const ih = Math.max(1e-9, item.imageIntrinsicSize.height);
3801
+ aspect = iw / ih;
3802
+ } else if (item.imageVectorLocalSize) {
3803
+ const lw = Math.max(1e-9, item.imageVectorLocalSize.width);
3804
+ const lh = Math.max(1e-9, item.imageVectorLocalSize.height);
3805
+ aspect = lw / lh;
3806
+ }
3807
+ nbLocal = aspect !== void 0 ? computeResizeBoundsFixedAspect(localBounds2, handle, local, aspect) : computeResizeBounds(localBounds2, handle, local);
3808
+ } else {
3809
+ nbLocal = computeResizeBounds(localBounds2, handle, local);
3810
+ }
3811
+ return {
3812
+ x: sb.x + nbLocal.x,
3813
+ y: sb.y + nbLocal.y,
3814
+ width: nbLocal.width,
3815
+ height: nbLocal.height
3816
+ };
3817
+ }
3818
+ function applyRotationFromPointer(item, startRotation, startPointerAngleRad, pointerAngleRad) {
3819
+ let delta = pointerAngleRad - startPointerAngleRad;
3820
+ while (delta > Math.PI) {
3821
+ delta -= 2 * Math.PI;
3822
+ }
3823
+ while (delta < -Math.PI) {
3824
+ delta += 2 * Math.PI;
3825
+ }
3826
+ return { ...item, rotation: startRotation + delta };
3827
+ }
3828
+ function resizeItemByHandle(item, start, handle, currentWorld) {
3829
+ const sb = normalizeRect(start.bounds);
3830
+ const newBoundsRaw = computeNewBoundsForResize(item, sb, handle, currentWorld);
3831
+ const nb = normalizeRect(newBoundsRaw);
3832
+ const k = item.toolKind;
3833
+ if (k === "rect") {
3834
+ const style = resolveStrokeStyle(item);
3835
+ return {
3836
+ ...item,
3837
+ x: nb.x,
3838
+ y: nb.y,
3839
+ bounds: nb,
3840
+ childrenSvg: buildRectSvg(nb.width, nb.height, style)
3841
+ };
3842
+ }
3843
+ if (k === "ellipse") {
3844
+ const style = resolveStrokeStyle(item);
3845
+ return {
3846
+ ...item,
3847
+ x: nb.x,
3848
+ y: nb.y,
3849
+ bounds: nb,
3850
+ childrenSvg: buildEllipseSvg(nb.width, nb.height, style)
3851
+ };
3852
+ }
3853
+ if (k === "architectural-cloud") {
3854
+ const style = resolveStrokeStyle(item);
3855
+ return {
3856
+ ...item,
3857
+ x: nb.x,
3858
+ y: nb.y,
3859
+ bounds: nb,
3860
+ childrenSvg: buildArchitecturalCloudSvg(nb.width, nb.height, style)
3861
+ };
3862
+ }
3863
+ if (k === "text" && item.text !== void 0) {
3864
+ const sfw = Math.max(sb.width, 1e-6);
3865
+ const sfh = Math.max(sb.height, 1e-6);
3866
+ const baseFs = item.textFontSize ?? DEFAULT_TEXT_FONT_SIZE;
3867
+ const areaRatio = nb.width * nb.height / (sfw * sfh);
3868
+ const scale = Math.sqrt(Math.max(areaRatio, 1e-12));
3869
+ const nextFs = Math.min(256, Math.max(6, baseFs * scale));
3870
+ return rebuildItemSvg({
3871
+ ...item,
3872
+ x: nb.x,
3873
+ y: nb.y,
3874
+ bounds: nb,
3875
+ textFixedBounds: true,
3876
+ textFontSize: nextFs
3877
+ });
3878
+ }
3879
+ if (k === "image") {
3880
+ if (item.imageRasterHref && item.imageIntrinsicSize) {
3881
+ return {
3882
+ ...item,
3883
+ x: nb.x,
3884
+ y: nb.y,
3885
+ bounds: nb,
3886
+ childrenSvg: buildRasterImageChildrenSvg(
3887
+ item.imageRasterHref,
3888
+ item.imageIntrinsicSize,
3889
+ nb
3890
+ )
3891
+ };
3892
+ }
3893
+ if (item.imageVectorInnerSvg && item.imageVectorLocalSize) {
3894
+ const lw = Math.max(1e-6, item.imageVectorLocalSize.width);
3895
+ const lh = Math.max(1e-6, item.imageVectorLocalSize.height);
3896
+ const arB = nb.width / Math.max(1e-9, nb.height);
3897
+ const arI = lw / lh;
3898
+ let childrenSvg;
3899
+ if (Math.abs(arB - arI) < 1e-3) {
3900
+ const s = nb.width / lw;
3901
+ childrenSvg = `<g transform="scale(${s})">${item.imageVectorInnerSvg}</g>`;
3902
+ } else {
3903
+ const s = Math.min(nb.width / lw, nb.height / lh);
3904
+ const tx = (nb.width - lw * s) / 2;
3905
+ const ty = (nb.height - lh * s) / 2;
3906
+ childrenSvg = `<g transform="translate(${tx}, ${ty}) scale(${s})">${item.imageVectorInnerSvg}</g>`;
3907
+ }
3908
+ return {
3909
+ ...item,
3910
+ x: nb.x,
3911
+ y: nb.y,
3912
+ bounds: nb,
3913
+ childrenSvg
3914
+ };
3915
+ }
3916
+ }
3917
+ if ((k === "line" || k === "arrow") && start.line) {
3918
+ const sfw = Math.max(sb.width, 1e-6);
3919
+ const sfh = Math.max(sb.height, 1e-6);
3920
+ const f = start.line;
3921
+ const u1 = f.x1 / sfw;
3922
+ const v1 = f.y1 / sfh;
3923
+ const u2 = f.x2 / sfw;
3924
+ const v2 = f.y2 / sfh;
3925
+ const newLine = {
3926
+ x1: u1 * nb.width,
3927
+ y1: v1 * nb.height,
3928
+ x2: u2 * nb.width,
3929
+ y2: v2 * nb.height
3930
+ };
3931
+ const style = resolveStrokeStyle(item);
3932
+ const childrenSvg = k === "arrow" ? buildArrowSvg(item.id, newLine, style) : buildLineSvg(newLine, style);
3933
+ return {
3934
+ ...item,
3935
+ x: nb.x,
3936
+ y: nb.y,
3937
+ bounds: nb,
3938
+ line: newLine,
3939
+ childrenSvg
3940
+ };
3941
+ }
3942
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item.pathPointsLocal && item.pathPointsLocal.length > 0) {
3943
+ const sfw = Math.max(sb.width, 1e-6);
3944
+ const sfh = Math.max(sb.height, 1e-6);
3945
+ const sx = nb.width / sfw;
3946
+ const sy = nb.height / sfh;
3947
+ const scaledPoints = item.pathPointsLocal.map((p) => ({
3948
+ x: p.x * sx,
3949
+ y: p.y * sy,
3950
+ ...p.pressure != null ? { pressure: p.pressure } : {}
3951
+ }));
3952
+ return rebuildItemSvg({
3953
+ ...item,
3954
+ x: nb.x,
3955
+ y: nb.y,
3956
+ bounds: nb,
3957
+ pathPointsLocal: scaledPoints
3958
+ });
3959
+ }
3960
+ if (k === "custom" && item.customIntrinsicSize && item.customInnerSvg) {
3961
+ return rebuildItemSvg({
3962
+ ...item,
3963
+ x: nb.x,
3964
+ y: nb.y,
3965
+ bounds: nb
3966
+ });
3967
+ }
3968
+ return { ...item, x: nb.x, y: nb.y, bounds: nb };
3969
+ }
3970
+
3584
3971
  // src/native/native-tool-cursors.ts
3585
3972
  var ICON_SIZE = 24;
3586
3973
  var CENTER_HOTSPOT = { x: 12, y: 12 };
@@ -3606,10 +3993,73 @@ function nativeCursorForVectorToolId(toolId) {
3606
3993
  return null;
3607
3994
  }
3608
3995
  }
3996
+ function nativeCrosshairToolCursor() {
3997
+ return { kind: "crosshair", size: ICON_SIZE, hotspot: CENTER_HOTSPOT };
3998
+ }
3609
3999
  function nativeFallbackToolCursorPoint(size) {
3610
4000
  if (size.width <= 0 || size.height <= 0) return null;
3611
4001
  return { x: size.width / 2, y: size.height / 2 };
3612
4002
  }
4003
+
4004
+ // src/native/native-vector-interactions.ts
4005
+ var NATIVE_SELECTION_HANDLE_HIT_RADIUS_PX = 24;
4006
+ function supportsNativeResizeHandles(item) {
4007
+ const k = item?.toolKind;
4008
+ if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
4009
+ return true;
4010
+ }
4011
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
4012
+ return true;
4013
+ }
4014
+ return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
4015
+ }
4016
+ function hitTestNativeSelectionHandle({
4017
+ selectedItem,
4018
+ selectedCount,
4019
+ worldPoint,
4020
+ zoom
4021
+ }) {
4022
+ if (selectedCount !== 1) return null;
4023
+ if (!selectedItem || selectedItem.locked) return null;
4024
+ if (!supportsNativeResizeHandles(selectedItem)) return null;
4025
+ const bounds = normalizeRect(selectedItem.bounds);
4026
+ const rotation = selectedItem.rotation ?? 0;
4027
+ const handleRadiusWorld = NATIVE_SELECTION_HANDLE_HIT_RADIUS_PX / Math.max(zoom, 1e-9);
4028
+ const rotateOffsetWorld = 24 / Math.max(zoom, 1e-9);
4029
+ if (hitTestRotateHandle(
4030
+ bounds,
4031
+ rotation,
4032
+ worldPoint.x,
4033
+ worldPoint.y,
4034
+ handleRadiusWorld,
4035
+ rotateOffsetWorld
4036
+ )) {
4037
+ return { kind: "rotate" };
4038
+ }
4039
+ const handle = hitTestResizeHandle(
4040
+ bounds,
4041
+ worldPoint.x,
4042
+ worldPoint.y,
4043
+ handleRadiusWorld,
4044
+ rotation
4045
+ );
4046
+ return handle ? { kind: "resize", handle } : null;
4047
+ }
4048
+ function resolveNativeCustomPlacement(toolId, customPlacement, customPlacements) {
4049
+ if (customPlacement?.toolId === toolId) return customPlacement;
4050
+ return customPlacements?.find((placement) => placement.toolId === toolId) ?? null;
4051
+ }
4052
+ function nativeRotationDragStart(input) {
4053
+ const pivotWorld = itemPivotWorld(input.item);
4054
+ return {
4055
+ pivotWorld,
4056
+ startPointerAngleRad: Math.atan2(
4057
+ input.worldPoint.y - pivotWorld.y,
4058
+ input.worldPoint.x - pivotWorld.x
4059
+ ),
4060
+ startRotation: input.item.rotation ?? 0
4061
+ };
4062
+ }
3613
4063
  var MIN_PLACE_SIZE = 8;
3614
4064
  var MIN_ARROW_DRAG_PX = 8;
3615
4065
  var TAP_PX = 20;
@@ -3624,16 +4074,6 @@ function isPlacementTool(toolId) {
3624
4074
  function isDefaultMarkerToolStyle(style) {
3625
4075
  return style.stroke === MARKER_TOOL_STYLE.stroke && style.strokeWidth === MARKER_TOOL_STYLE.strokeWidth && style.strokeOpacity === MARKER_TOOL_STYLE.strokeOpacity;
3626
4076
  }
3627
- function supportsNativeResizeHandles(item) {
3628
- const k = item?.toolKind;
3629
- if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
3630
- return true;
3631
- }
3632
- if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
3633
- return true;
3634
- }
3635
- return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
3636
- }
3637
4077
  function placementPreviewForTool(toolId, start, end) {
3638
4078
  if (toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud") {
3639
4079
  return { kind: toolId, rect: rectFromCorners(start, end) };
@@ -3701,7 +4141,11 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3701
4141
  onSelectionChange,
3702
4142
  onItemsChange,
3703
4143
  onToolChangeRequest,
4144
+ onWorldPointerDown,
3704
4145
  onCameraChange,
4146
+ customPlacement,
4147
+ customPlacements = [],
4148
+ customCrosshairToolIds = [],
3705
4149
  toolbar,
3706
4150
  showStyleInspector = false,
3707
4151
  styleInspectorPlacement = "bottom"
@@ -3714,10 +4158,18 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3714
4158
  toolLockedRef.current = toolLocked;
3715
4159
  const onToolChangeRequestRef = useRef(onToolChangeRequest);
3716
4160
  onToolChangeRequestRef.current = onToolChangeRequest;
4161
+ const onWorldPointerDownRef = useRef(onWorldPointerDown);
4162
+ onWorldPointerDownRef.current = onWorldPointerDown;
3717
4163
  const onCameraChangeRef = useRef(onCameraChange);
3718
4164
  onCameraChangeRef.current = onCameraChange;
3719
4165
  const onItemsChangeRef = useRef(onItemsChange);
3720
4166
  onItemsChangeRef.current = onItemsChange;
4167
+ const customPlacementRef = useRef(customPlacement);
4168
+ customPlacementRef.current = customPlacement;
4169
+ const customPlacementsRef = useRef(customPlacements);
4170
+ customPlacementsRef.current = customPlacements;
4171
+ const customCrosshairToolIdsRef = useRef(customCrosshairToolIds);
4172
+ customCrosshairToolIdsRef.current = customCrosshairToolIds;
3721
4173
  const onSelectionChangeRef = useRef(onSelectionChange);
3722
4174
  onSelectionChangeRef.current = onSelectionChange;
3723
4175
  const itemsRef = useRef(items);
@@ -3825,6 +4277,11 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3825
4277
  setCameraTick((n) => n + 1);
3826
4278
  onCameraChangeRef.current?.();
3827
4279
  }, []);
4280
+ const cursorForToolId = useCallback((nextToolId) => {
4281
+ const builtInCursor = nativeCursorForVectorToolId(nextToolId);
4282
+ if (builtInCursor) return builtInCursor;
4283
+ return customCrosshairToolIdsRef.current.includes(nextToolId) ? nativeCrosshairToolCursor() : null;
4284
+ }, []);
3828
4285
  const onLayout = useCallback(
3829
4286
  (e) => {
3830
4287
  const { width, height } = e.nativeEvent.layout;
@@ -3834,7 +4291,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3834
4291
  setToolCursorPoint(null);
3835
4292
  return;
3836
4293
  }
3837
- if (!nativeCursorForVectorToolId(toolIdRef.current)) {
4294
+ if (!cursorForToolId(toolIdRef.current)) {
3838
4295
  setToolCursorPoint(null);
3839
4296
  return;
3840
4297
  }
@@ -3842,15 +4299,15 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3842
4299
  (current) => current ?? nativeFallbackToolCursorPoint(nextSize)
3843
4300
  );
3844
4301
  },
3845
- [interactive]
4302
+ [cursorForToolId, interactive]
3846
4303
  );
3847
4304
  const updateToolCursorPoint = useCallback(
3848
4305
  (point) => {
3849
4306
  if (!interactive) return;
3850
- if (!nativeCursorForVectorToolId(toolIdRef.current)) return;
4307
+ if (!cursorForToolId(toolIdRef.current)) return;
3851
4308
  setToolCursorPoint(point);
3852
4309
  },
3853
- [interactive]
4310
+ [cursorForToolId, interactive]
3854
4311
  );
3855
4312
  const hideToolCursor = useCallback(() => {
3856
4313
  setToolCursorPoint(null);
@@ -3861,7 +4318,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3861
4318
  setToolCursorPoint(null);
3862
4319
  return;
3863
4320
  }
3864
- if (!nativeCursorForVectorToolId(nextToolId)) {
4321
+ if (!cursorForToolId(nextToolId)) {
3865
4322
  setToolCursorPoint(null);
3866
4323
  return;
3867
4324
  }
@@ -3869,17 +4326,11 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3869
4326
  (current) => current ?? nativeFallbackToolCursorPoint(size)
3870
4327
  );
3871
4328
  },
3872
- [interactive, size]
4329
+ [cursorForToolId, interactive, size]
3873
4330
  );
3874
4331
  useEffect(() => {
3875
4332
  showFallbackToolCursor(toolId);
3876
4333
  }, [showFallbackToolCursor, toolId]);
3877
- const handlePointerMove = useCallback(
3878
- (event) => {
3879
- updateToolCursorPoint(screenPointFromPointerEvent(event));
3880
- },
3881
- [updateToolCursorPoint]
3882
- );
3883
4334
  const selectedItems = useMemo(
3884
4335
  () => items.filter((it) => selectedIds.includes(it.id)),
3885
4336
  [items, selectedIds]
@@ -3892,6 +4343,153 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3892
4343
  const showResizeHandles = interactive && selectedItems.length === 1 && !selectedItems[0]?.locked && supportsNativeResizeHandles(selectedItems[0]);
3893
4344
  const lastPinchDist = useRef(null);
3894
4345
  const lastPanPoint = useRef(null);
4346
+ const applyDragMoveAtScreenPoint = useCallback(
4347
+ (point, pagePoint) => {
4348
+ const cam = cameraRef.current;
4349
+ if (!cam) return;
4350
+ updateToolCursorPoint(point);
4351
+ const { worldX, worldY } = screenToWorld(point.x, point.y);
4352
+ const st = dragStateRef.current;
4353
+ if (st.kind === "pan") {
4354
+ const current = pagePoint ?? point;
4355
+ if (lastPanPoint.current) {
4356
+ const dx = current.x - lastPanPoint.current.x;
4357
+ const dy = current.y - lastPanPoint.current.y;
4358
+ cam.x += dx;
4359
+ cam.y += dy;
4360
+ requestRender();
4361
+ }
4362
+ lastPanPoint.current = current;
4363
+ return;
4364
+ }
4365
+ lastPanPoint.current = null;
4366
+ if (st.kind === "draw") {
4367
+ const pts = st.points;
4368
+ const last = pts[pts.length - 1];
4369
+ const dx = worldX - (last?.x ?? worldX);
4370
+ const dy = worldY - (last?.y ?? worldY);
4371
+ const shouldAppendPoint = Math.hypot(dx, dy) > 0.5 / cam.zoom;
4372
+ if (shouldAppendPoint) {
4373
+ pts.push({ x: worldX, y: worldY });
4374
+ }
4375
+ if (st.tool === "laser") {
4376
+ if (shouldAppendPoint) {
4377
+ setLaserTrail((prev) => [
4378
+ ...prev,
4379
+ { x: worldX, y: worldY, t: Date.now() }
4380
+ ]);
4381
+ }
4382
+ return;
4383
+ }
4384
+ setPlacementPreview({
4385
+ kind: "stroke",
4386
+ tool: st.tool,
4387
+ points: [...pts],
4388
+ style: { ...strokeStyleRef.current }
4389
+ });
4390
+ return;
4391
+ }
4392
+ if (st.kind === "move") {
4393
+ const dx = worldX - st.startWorld.x;
4394
+ const dy = worldY - st.startWorld.y;
4395
+ const change = onItemsChangeRef.current;
4396
+ if (!change) return;
4397
+ const nextList = itemsRef.current.map((it) => {
4398
+ const snap = st.snapshots[it.id];
4399
+ if (!snap) return it;
4400
+ return {
4401
+ ...snap,
4402
+ x: snap.x + dx,
4403
+ y: snap.y + dy,
4404
+ bounds: {
4405
+ ...snap.bounds,
4406
+ x: snap.bounds.x + dx,
4407
+ y: snap.bounds.y + dy
4408
+ }
4409
+ };
4410
+ });
4411
+ change(nextList);
4412
+ return;
4413
+ }
4414
+ if (st.kind === "rotate") {
4415
+ const change = onItemsChangeRef.current;
4416
+ if (!change) return;
4417
+ const angle = Math.atan2(worldY - st.pivotWorld.y, worldX - st.pivotWorld.x);
4418
+ const next = applyRotationFromPointer(
4419
+ st.snapshot,
4420
+ st.startRotation,
4421
+ st.startPointerAngleRad,
4422
+ angle
4423
+ );
4424
+ change(itemsRef.current.map((item) => item.id === st.id ? next : item));
4425
+ return;
4426
+ }
4427
+ if (st.kind === "resize") {
4428
+ const change = onItemsChangeRef.current;
4429
+ if (!change) return;
4430
+ const next = resizeItemByHandle(st.snapshot, st.start, st.handle, {
4431
+ x: worldX,
4432
+ y: worldY
4433
+ });
4434
+ change(itemsRef.current.map((item) => item.id === st.id ? next : item));
4435
+ return;
4436
+ }
4437
+ if (st.kind === "marquee") {
4438
+ const a = st.startWorld;
4439
+ const b = { x: worldX, y: worldY };
4440
+ const rect = {
4441
+ x: Math.min(a.x, b.x),
4442
+ y: Math.min(a.y, b.y),
4443
+ width: Math.abs(b.x - a.x),
4444
+ height: Math.abs(b.y - a.y)
4445
+ };
4446
+ setPlacementPreview({ kind: "marquee", rect });
4447
+ return;
4448
+ }
4449
+ if (st.kind === "erase") {
4450
+ setEraserTrail((prev) => [...prev, { x: worldX, y: worldY, t: Date.now() }]);
4451
+ const toErase = collectEraserTargetsAtWorldPoint(
4452
+ itemsRef.current,
4453
+ worldX,
4454
+ worldY,
4455
+ { lineHitWorld: 10 / cam.zoom, ignoreLocked: true }
4456
+ );
4457
+ for (const id of toErase) {
4458
+ eraserPreviewIdSetRef.current.add(id);
4459
+ }
4460
+ setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
4461
+ return;
4462
+ }
4463
+ if (st.kind === "place") {
4464
+ setPlacementPreview(
4465
+ placementPreviewForTool(st.tool, st.startWorld, {
4466
+ x: worldX,
4467
+ y: worldY
4468
+ })
4469
+ );
4470
+ return;
4471
+ }
4472
+ if (st.kind === "custom-place") {
4473
+ setPlacementPreview({
4474
+ kind: "rect",
4475
+ rect: rectFromCorners(st.startWorld, { x: worldX, y: worldY })
4476
+ });
4477
+ return;
4478
+ }
4479
+ },
4480
+ [requestRender, screenToWorld, updateToolCursorPoint]
4481
+ );
4482
+ const handlePointerMove = useCallback(
4483
+ (event) => {
4484
+ const point = screenPointFromPointerEvent(event);
4485
+ if (dragStateRef.current.kind !== "idle") {
4486
+ applyDragMoveAtScreenPoint(point, point);
4487
+ return;
4488
+ }
4489
+ updateToolCursorPoint(point);
4490
+ },
4491
+ [applyDragMoveAtScreenPoint, updateToolCursorPoint]
4492
+ );
3895
4493
  const panResponder = useMemo(
3896
4494
  () => PanResponder.create({
3897
4495
  onStartShouldSetPanResponder: () => true,
@@ -3921,6 +4519,40 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3921
4519
  return;
3922
4520
  }
3923
4521
  if (tool === "select") {
4522
+ const currentSelectedIds = selectedIdsRef.current;
4523
+ const selectedItem = currentSelectedIds.length === 1 ? itemsRef.current.find((item) => item.id === currentSelectedIds[0]) : void 0;
4524
+ const selectionHandle = hitTestNativeSelectionHandle({
4525
+ selectedItem,
4526
+ selectedCount: currentSelectedIds.length,
4527
+ worldPoint: { x: worldX, y: worldY },
4528
+ zoom: cam.zoom
4529
+ });
4530
+ if (selectionHandle && selectedItem) {
4531
+ if (selectionHandle.kind === "rotate") {
4532
+ const rotationStart = nativeRotationDragStart({
4533
+ item: selectedItem,
4534
+ worldPoint: { x: worldX, y: worldY }
4535
+ });
4536
+ dragStateRef.current = {
4537
+ kind: "rotate",
4538
+ id: selectedItem.id,
4539
+ snapshot: selectedItem,
4540
+ ...rotationStart
4541
+ };
4542
+ return;
4543
+ }
4544
+ dragStateRef.current = {
4545
+ kind: "resize",
4546
+ id: selectedItem.id,
4547
+ handle: selectionHandle.handle,
4548
+ snapshot: selectedItem,
4549
+ start: {
4550
+ bounds: selectedItem.bounds,
4551
+ line: selectedItem.line
4552
+ }
4553
+ };
4554
+ return;
4555
+ }
3924
4556
  const hit = hitTestWorldPoint(itemsRef.current, worldX, worldY, {
3925
4557
  lineHitWorld: 10 / cam.zoom,
3926
4558
  ignoreLocked: true
@@ -4013,6 +4645,25 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4013
4645
  );
4014
4646
  return;
4015
4647
  }
4648
+ const customPlacement2 = resolveNativeCustomPlacement(
4649
+ tool,
4650
+ customPlacementRef.current,
4651
+ customPlacementsRef.current
4652
+ );
4653
+ if (customPlacement2) {
4654
+ dragStateRef.current = {
4655
+ kind: "custom-place",
4656
+ tool,
4657
+ placement: customPlacement2,
4658
+ startWorld: { x: worldX, y: worldY },
4659
+ startScreen: { x: sx, y: sy }
4660
+ };
4661
+ setPlacementPreview({
4662
+ kind: "rect",
4663
+ rect: { x: worldX, y: worldY, width: 0, height: 0 }
4664
+ });
4665
+ return;
4666
+ }
4016
4667
  if (tool === "note" || tool === "text") {
4017
4668
  dragStateRef.current = {
4018
4669
  kind: "tap",
@@ -4022,6 +4673,18 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4022
4673
  };
4023
4674
  return;
4024
4675
  }
4676
+ const handleWorldPointerDown = onWorldPointerDownRef.current;
4677
+ if (handleWorldPointerDown) {
4678
+ handleWorldPointerDown({
4679
+ toolId: tool,
4680
+ worldX,
4681
+ worldY,
4682
+ screenX: sx,
4683
+ screenY: sy
4684
+ });
4685
+ requestSelectToolAfterUse();
4686
+ return;
4687
+ }
4025
4688
  dragStateRef.current = { kind: "pan" };
4026
4689
  },
4027
4690
  onPanResponderMove: (evt) => {
@@ -4053,108 +4716,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4053
4716
  return;
4054
4717
  }
4055
4718
  lastPinchDist.current = null;
4056
- updateToolCursorPoint({ x: sx, y: sy });
4057
- const { worldX, worldY } = screenToWorld(sx, sy);
4058
- const st = dragStateRef.current;
4059
- if (st.kind === "pan") {
4060
- const current = { x: pageX, y: pageY };
4061
- if (lastPanPoint.current) {
4062
- const dx = current.x - lastPanPoint.current.x;
4063
- const dy = current.y - lastPanPoint.current.y;
4064
- cam.x += dx;
4065
- cam.y += dy;
4066
- requestRender();
4067
- }
4068
- lastPanPoint.current = current;
4069
- return;
4070
- }
4071
- lastPanPoint.current = null;
4072
- if (st.kind === "draw") {
4073
- const pts = st.points;
4074
- const last = pts[pts.length - 1];
4075
- const dx = worldX - (last?.x ?? worldX);
4076
- const dy = worldY - (last?.y ?? worldY);
4077
- const shouldAppendPoint = Math.hypot(dx, dy) > 0.5 / cam.zoom;
4078
- if (shouldAppendPoint) {
4079
- pts.push({ x: worldX, y: worldY });
4080
- }
4081
- if (st.tool === "laser") {
4082
- if (shouldAppendPoint) {
4083
- setLaserTrail((prev) => [
4084
- ...prev,
4085
- { x: worldX, y: worldY, t: Date.now() }
4086
- ]);
4087
- }
4088
- return;
4089
- }
4090
- setPlacementPreview({
4091
- kind: "stroke",
4092
- tool: st.tool,
4093
- points: [...pts],
4094
- style: { ...strokeStyleRef.current }
4095
- });
4096
- return;
4097
- }
4098
- if (st.kind === "move") {
4099
- const dx = worldX - st.startWorld.x;
4100
- const dy = worldY - st.startWorld.y;
4101
- const change = onItemsChangeRef.current;
4102
- if (!change) return;
4103
- const nextList = itemsRef.current.map((it) => {
4104
- const snap = st.snapshots[it.id];
4105
- if (!snap) return it;
4106
- return {
4107
- ...snap,
4108
- x: snap.x + dx,
4109
- y: snap.y + dy,
4110
- bounds: {
4111
- ...snap.bounds,
4112
- x: snap.bounds.x + dx,
4113
- y: snap.bounds.y + dy
4114
- }
4115
- };
4116
- });
4117
- change(nextList);
4118
- return;
4119
- }
4120
- if (st.kind === "marquee") {
4121
- const a = st.startWorld;
4122
- const b = { x: worldX, y: worldY };
4123
- const rect = {
4124
- x: Math.min(a.x, b.x),
4125
- y: Math.min(a.y, b.y),
4126
- width: Math.abs(b.x - a.x),
4127
- height: Math.abs(b.y - a.y)
4128
- };
4129
- setPlacementPreview({ kind: "marquee", rect });
4130
- return;
4131
- }
4132
- if (st.kind === "erase") {
4133
- setEraserTrail((prev) => [
4134
- ...prev,
4135
- { x: worldX, y: worldY, t: Date.now() }
4136
- ]);
4137
- const toErase = collectEraserTargetsAtWorldPoint(
4138
- itemsRef.current,
4139
- worldX,
4140
- worldY,
4141
- { lineHitWorld: 10 / cam.zoom, ignoreLocked: true }
4142
- );
4143
- for (const id of toErase) {
4144
- eraserPreviewIdSetRef.current.add(id);
4145
- }
4146
- setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
4147
- return;
4148
- }
4149
- if (st.kind === "place") {
4150
- setPlacementPreview(
4151
- placementPreviewForTool(st.tool, st.startWorld, {
4152
- x: worldX,
4153
- y: worldY
4154
- })
4155
- );
4156
- return;
4157
- }
4719
+ applyDragMoveAtScreenPoint({ x: sx, y: sy }, { x: pageX, y: pageY });
4158
4720
  },
4159
4721
  onPanResponderRelease: (evt) => {
4160
4722
  lastPinchDist.current = null;
@@ -4198,6 +4760,10 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4198
4760
  dragStateRef.current = { kind: "idle" };
4199
4761
  return;
4200
4762
  }
4763
+ if (st.kind === "resize" || st.kind === "rotate") {
4764
+ dragStateRef.current = { kind: "idle" };
4765
+ return;
4766
+ }
4201
4767
  if (st.kind === "marquee") {
4202
4768
  dragStateRef.current = { kind: "idle" };
4203
4769
  setPlacementPreview(null);
@@ -4295,6 +4861,34 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4295
4861
  requestSelectToolAfterUse();
4296
4862
  return;
4297
4863
  }
4864
+ if (st.kind === "custom-place") {
4865
+ dragStateRef.current = { kind: "idle" };
4866
+ setPlacementPreview(null);
4867
+ const change = onItemsChangeRef.current;
4868
+ if (!change) return;
4869
+ const { worldX, worldY } = screenToWorld(
4870
+ evt.nativeEvent.locationX,
4871
+ evt.nativeEvent.locationY
4872
+ );
4873
+ const center = {
4874
+ x: (st.startWorld.x + worldX) / 2,
4875
+ y: (st.startWorld.y + worldY) / 2
4876
+ };
4877
+ const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
4878
+ const normalized = normalizeRect(raw);
4879
+ const bounds = normalized.width < MIN_PLACE_SIZE || normalized.height < MIN_PLACE_SIZE ? {
4880
+ x: center.x - 60,
4881
+ y: center.y - 40,
4882
+ width: 120,
4883
+ height: 80
4884
+ } : normalized;
4885
+ const id = createShapeId();
4886
+ const item = st.placement.createItem({ id, bounds });
4887
+ change([...itemsRef.current, item]);
4888
+ onSelectionChangeRef.current?.([id]);
4889
+ requestSelectToolAfterUse();
4890
+ return;
4891
+ }
4298
4892
  if (st.kind === "tap") {
4299
4893
  dragStateRef.current = { kind: "idle" };
4300
4894
  const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
@@ -4356,6 +4950,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4356
4950
  }
4357
4951
  }),
4358
4952
  [
4953
+ applyDragMoveAtScreenPoint,
4359
4954
  screenToWorld,
4360
4955
  requestRender,
4361
4956
  requestSelectToolAfterUse,
@@ -4386,7 +4981,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4386
4981
  [requestRender, size]
4387
4982
  );
4388
4983
  const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
4389
- const activeToolCursor = nativeCursorForVectorToolId(toolId);
4984
+ const activeToolCursor = cursorForToolId(toolId);
4390
4985
  const toolCursor = activeToolCursor && toolCursorPoint ? { cursor: activeToolCursor, point: toolCursorPoint } : null;
4391
4986
  return /* @__PURE__ */ jsx(
4392
4987
  View,