canvu-react 0.4.17 → 0.4.18

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.cjs CHANGED
@@ -946,6 +946,10 @@ function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
946
946
  const ly = sin * dx + cos * dy;
947
947
  return { x: c.x + lx, y: c.y + ly };
948
948
  }
949
+ function itemPivotWorld(item) {
950
+ const r = normalizeRect(item.bounds);
951
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
952
+ }
949
953
  function boundsAabbForRotatedItem(item) {
950
954
  const rot = getItemRotationRad(item);
951
955
  if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
@@ -976,6 +980,7 @@ function boundsAabbForRotatedItem(item) {
976
980
  }
977
981
 
978
982
  // src/interaction/resize-handles.ts
983
+ var HANDLE_IDS = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
979
984
  function getHandleWorldPosition(bounds, id) {
980
985
  const r = normalizeRect(bounds);
981
986
  const cx = r.x + r.width / 2;
@@ -999,6 +1004,30 @@ function getHandleWorldPosition(bounds, id) {
999
1004
  return { x: r.x, y: cy };
1000
1005
  }
1001
1006
  }
1007
+ function hitTestResizeHandle(bounds, worldX, worldY, radiusWorld, rotationRad = 0) {
1008
+ const r = normalizeRect(bounds);
1009
+ const pl = worldToItemLocal(
1010
+ worldX,
1011
+ worldY,
1012
+ r.x,
1013
+ r.y,
1014
+ r.width,
1015
+ r.height,
1016
+ rotationRad
1017
+ );
1018
+ const localBounds2 = { x: 0, y: 0, width: r.width, height: r.height };
1019
+ let best = null;
1020
+ let bestD = radiusWorld;
1021
+ for (const id of HANDLE_IDS) {
1022
+ const p = getHandleWorldPosition(localBounds2, id);
1023
+ const d = Math.hypot(pl.x - p.x, pl.y - p.y);
1024
+ if (d <= bestD) {
1025
+ bestD = d;
1026
+ best = id;
1027
+ }
1028
+ }
1029
+ return best;
1030
+ }
1002
1031
  function getHandleWorldPositionRotated(bounds, handle, rotationRad) {
1003
1032
  const r = normalizeRect(bounds);
1004
1033
  const p = getHandleWorldPosition(
@@ -1019,6 +1048,10 @@ function getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld)
1019
1048
  rotationRad
1020
1049
  );
1021
1050
  }
1051
+ function hitTestRotateHandle(bounds, rotationRad, worldX, worldY, radiusWorld, handleOffsetWorld) {
1052
+ const p = getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld);
1053
+ return Math.hypot(worldX - p.x, worldY - p.y) <= radiusWorld;
1054
+ }
1022
1055
  function rectFromCorners(a, b) {
1023
1056
  const minX = Math.min(a.x, b.x);
1024
1057
  const maxX = Math.max(a.x, b.x);
@@ -1026,6 +1059,155 @@ function rectFromCorners(a, b) {
1026
1059
  const maxY = Math.max(a.y, b.y);
1027
1060
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1028
1061
  }
1062
+ function clampMinSize(r, min) {
1063
+ const n = normalizeRect(r);
1064
+ return {
1065
+ x: n.x,
1066
+ y: n.y,
1067
+ width: Math.max(min, n.width),
1068
+ height: Math.max(min, n.height)
1069
+ };
1070
+ }
1071
+ function computeResizeBounds(bounds, handle, currentWorld) {
1072
+ const r = normalizeRect(bounds);
1073
+ const x0 = r.x;
1074
+ const y0 = r.y;
1075
+ const x1 = r.x + r.width;
1076
+ const y1 = r.y + r.height;
1077
+ const minSize = 8;
1078
+ switch (handle) {
1079
+ case "nw":
1080
+ return clampMinSize(rectFromCorners(currentWorld, { x: x1, y: y1 }), minSize);
1081
+ case "ne":
1082
+ return clampMinSize(rectFromCorners(currentWorld, { x: x0, y: y1 }), minSize);
1083
+ case "se":
1084
+ return clampMinSize(rectFromCorners(currentWorld, { x: x0, y: y0 }), minSize);
1085
+ case "sw":
1086
+ return clampMinSize(rectFromCorners(currentWorld, { x: x1, y: y0 }), minSize);
1087
+ case "n":
1088
+ return clampMinSize(
1089
+ rectFromCorners({ x: x0, y: currentWorld.y }, { x: x1, y: y1 }),
1090
+ minSize
1091
+ );
1092
+ case "s":
1093
+ return clampMinSize(
1094
+ rectFromCorners({ x: x0, y: y0 }, { x: x1, y: currentWorld.y }),
1095
+ minSize
1096
+ );
1097
+ case "e":
1098
+ return clampMinSize(
1099
+ rectFromCorners({ x: x0, y: y0 }, { x: currentWorld.x, y: y1 }),
1100
+ minSize
1101
+ );
1102
+ case "w":
1103
+ return clampMinSize(
1104
+ rectFromCorners({ x: currentWorld.x, y: y0 }, { x: x1, y: y1 }),
1105
+ minSize
1106
+ );
1107
+ default:
1108
+ return r;
1109
+ }
1110
+ }
1111
+ function computeResizeBoundsFixedAspect(bounds, handle, currentWorld, aspect) {
1112
+ const r = normalizeRect(bounds);
1113
+ const x0 = r.x;
1114
+ const y0 = r.y;
1115
+ const x1 = r.x + r.width;
1116
+ const y1 = r.y + r.height;
1117
+ const w = r.width;
1118
+ const h = r.height;
1119
+ const minSize = 8;
1120
+ const a = Math.max(1e-9, aspect);
1121
+ function cornerAspectRect(anchor, cursor) {
1122
+ const dx = cursor.x - anchor.x;
1123
+ const dy = cursor.y - anchor.y;
1124
+ const aw = Math.abs(dx);
1125
+ const ah = Math.abs(dy);
1126
+ let W;
1127
+ let H;
1128
+ if (aw < 1e-12 && ah < 1e-12) {
1129
+ W = minSize;
1130
+ H = minSize / a;
1131
+ } else if (ah < 1e-12) {
1132
+ W = Math.max(minSize, aw);
1133
+ H = W / a;
1134
+ } else if (aw < 1e-12) {
1135
+ H = Math.max(minSize, ah);
1136
+ W = H * a;
1137
+ } else if (aw / ah > a) {
1138
+ W = aw;
1139
+ H = aw / a;
1140
+ } else {
1141
+ H = ah;
1142
+ W = ah * a;
1143
+ }
1144
+ if (W < minSize || H < minSize) {
1145
+ const scale = Math.max(minSize / W, minSize / H);
1146
+ W *= scale;
1147
+ H *= scale;
1148
+ }
1149
+ const sx = Math.sign(dx) || 1;
1150
+ const sy = Math.sign(dy) || 1;
1151
+ return rectFromCorners(anchor, {
1152
+ x: anchor.x + sx * W,
1153
+ y: anchor.y + sy * H
1154
+ });
1155
+ }
1156
+ switch (handle) {
1157
+ case "se":
1158
+ return cornerAspectRect({ x: x0, y: y0 }, currentWorld);
1159
+ case "nw":
1160
+ return cornerAspectRect({ x: x1, y: y1 }, currentWorld);
1161
+ case "ne":
1162
+ return cornerAspectRect({ x: x0, y: y1 }, currentWorld);
1163
+ case "sw":
1164
+ return cornerAspectRect({ x: x1, y: y0 }, currentWorld);
1165
+ case "e": {
1166
+ const rawW = currentWorld.x - x0;
1167
+ const aw = Math.max(minSize, Math.abs(rawW));
1168
+ const H = aw / a;
1169
+ const newY = y0 + h / 2 - H / 2;
1170
+ const sign = Math.sign(rawW) || 1;
1171
+ return normalizeRect({ x: x0, y: newY, width: sign * aw, height: H });
1172
+ }
1173
+ case "w": {
1174
+ const rawW = x1 - currentWorld.x;
1175
+ const aw = Math.max(minSize, Math.abs(rawW));
1176
+ const H = aw / a;
1177
+ const newY = y0 + h / 2 - H / 2;
1178
+ const sign = Math.sign(rawW) || 1;
1179
+ return normalizeRect({
1180
+ x: x1 - sign * aw,
1181
+ y: newY,
1182
+ width: sign * aw,
1183
+ height: H
1184
+ });
1185
+ }
1186
+ case "n": {
1187
+ const rawH = y1 - currentWorld.y;
1188
+ const ah = Math.max(minSize, Math.abs(rawH));
1189
+ const W = ah * a;
1190
+ const newX = x0 + w / 2 - W / 2;
1191
+ const sign = Math.sign(rawH) || 1;
1192
+ return normalizeRect({
1193
+ x: newX,
1194
+ y: y1 - sign * ah,
1195
+ width: W,
1196
+ height: sign * ah
1197
+ });
1198
+ }
1199
+ case "s": {
1200
+ const rawH = currentWorld.y - y0;
1201
+ const ah = Math.max(minSize, Math.abs(rawH));
1202
+ const W = ah * a;
1203
+ const newX = x0 + w / 2 - W / 2;
1204
+ const sign = Math.sign(rawH) || 1;
1205
+ return normalizeRect({ x: newX, y: y0, width: W, height: sign * ah });
1206
+ }
1207
+ default:
1208
+ return r;
1209
+ }
1210
+ }
1029
1211
 
1030
1212
  // src/scene/freehand-path.ts
1031
1213
  function smoothFreehandPointsToPathD(points) {
@@ -3587,6 +3769,211 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
3587
3769
  return ids;
3588
3770
  }
3589
3771
 
3772
+ // src/interaction/mutations.ts
3773
+ function computeNewBoundsForResize(item, sb, handle, currentWorld) {
3774
+ const rot = getItemRotationRad(item);
3775
+ if (Math.abs(rot) < 1e-12) {
3776
+ if (item.toolKind === "image") {
3777
+ let aspect;
3778
+ if (item.imageIntrinsicSize) {
3779
+ const iw = Math.max(1e-9, item.imageIntrinsicSize.width);
3780
+ const ih = Math.max(1e-9, item.imageIntrinsicSize.height);
3781
+ aspect = iw / ih;
3782
+ } else if (item.imageVectorLocalSize) {
3783
+ const lw = Math.max(1e-9, item.imageVectorLocalSize.width);
3784
+ const lh = Math.max(1e-9, item.imageVectorLocalSize.height);
3785
+ aspect = lw / lh;
3786
+ }
3787
+ return aspect !== void 0 ? computeResizeBoundsFixedAspect(sb, handle, currentWorld, aspect) : computeResizeBounds(sb, handle, currentWorld);
3788
+ }
3789
+ return computeResizeBounds(sb, handle, currentWorld);
3790
+ }
3791
+ const local = worldToItemLocal(
3792
+ currentWorld.x,
3793
+ currentWorld.y,
3794
+ sb.x,
3795
+ sb.y,
3796
+ sb.width,
3797
+ sb.height,
3798
+ rot
3799
+ );
3800
+ const localBounds2 = { x: 0, y: 0, width: sb.width, height: sb.height };
3801
+ let nbLocal;
3802
+ if (item.toolKind === "image") {
3803
+ let aspect;
3804
+ if (item.imageIntrinsicSize) {
3805
+ const iw = Math.max(1e-9, item.imageIntrinsicSize.width);
3806
+ const ih = Math.max(1e-9, item.imageIntrinsicSize.height);
3807
+ aspect = iw / ih;
3808
+ } else if (item.imageVectorLocalSize) {
3809
+ const lw = Math.max(1e-9, item.imageVectorLocalSize.width);
3810
+ const lh = Math.max(1e-9, item.imageVectorLocalSize.height);
3811
+ aspect = lw / lh;
3812
+ }
3813
+ nbLocal = aspect !== void 0 ? computeResizeBoundsFixedAspect(localBounds2, handle, local, aspect) : computeResizeBounds(localBounds2, handle, local);
3814
+ } else {
3815
+ nbLocal = computeResizeBounds(localBounds2, handle, local);
3816
+ }
3817
+ return {
3818
+ x: sb.x + nbLocal.x,
3819
+ y: sb.y + nbLocal.y,
3820
+ width: nbLocal.width,
3821
+ height: nbLocal.height
3822
+ };
3823
+ }
3824
+ function applyRotationFromPointer(item, startRotation, startPointerAngleRad, pointerAngleRad) {
3825
+ let delta = pointerAngleRad - startPointerAngleRad;
3826
+ while (delta > Math.PI) {
3827
+ delta -= 2 * Math.PI;
3828
+ }
3829
+ while (delta < -Math.PI) {
3830
+ delta += 2 * Math.PI;
3831
+ }
3832
+ return { ...item, rotation: startRotation + delta };
3833
+ }
3834
+ function resizeItemByHandle(item, start, handle, currentWorld) {
3835
+ const sb = normalizeRect(start.bounds);
3836
+ const newBoundsRaw = computeNewBoundsForResize(item, sb, handle, currentWorld);
3837
+ const nb = normalizeRect(newBoundsRaw);
3838
+ const k = item.toolKind;
3839
+ if (k === "rect") {
3840
+ const style = resolveStrokeStyle(item);
3841
+ return {
3842
+ ...item,
3843
+ x: nb.x,
3844
+ y: nb.y,
3845
+ bounds: nb,
3846
+ childrenSvg: buildRectSvg(nb.width, nb.height, style)
3847
+ };
3848
+ }
3849
+ if (k === "ellipse") {
3850
+ const style = resolveStrokeStyle(item);
3851
+ return {
3852
+ ...item,
3853
+ x: nb.x,
3854
+ y: nb.y,
3855
+ bounds: nb,
3856
+ childrenSvg: buildEllipseSvg(nb.width, nb.height, style)
3857
+ };
3858
+ }
3859
+ if (k === "architectural-cloud") {
3860
+ const style = resolveStrokeStyle(item);
3861
+ return {
3862
+ ...item,
3863
+ x: nb.x,
3864
+ y: nb.y,
3865
+ bounds: nb,
3866
+ childrenSvg: buildArchitecturalCloudSvg(nb.width, nb.height, style)
3867
+ };
3868
+ }
3869
+ if (k === "text" && item.text !== void 0) {
3870
+ const sfw = Math.max(sb.width, 1e-6);
3871
+ const sfh = Math.max(sb.height, 1e-6);
3872
+ const baseFs = item.textFontSize ?? DEFAULT_TEXT_FONT_SIZE;
3873
+ const areaRatio = nb.width * nb.height / (sfw * sfh);
3874
+ const scale = Math.sqrt(Math.max(areaRatio, 1e-12));
3875
+ const nextFs = Math.min(256, Math.max(6, baseFs * scale));
3876
+ return rebuildItemSvg({
3877
+ ...item,
3878
+ x: nb.x,
3879
+ y: nb.y,
3880
+ bounds: nb,
3881
+ textFixedBounds: true,
3882
+ textFontSize: nextFs
3883
+ });
3884
+ }
3885
+ if (k === "image") {
3886
+ if (item.imageRasterHref && item.imageIntrinsicSize) {
3887
+ return {
3888
+ ...item,
3889
+ x: nb.x,
3890
+ y: nb.y,
3891
+ bounds: nb,
3892
+ childrenSvg: buildRasterImageChildrenSvg(
3893
+ item.imageRasterHref,
3894
+ item.imageIntrinsicSize,
3895
+ nb
3896
+ )
3897
+ };
3898
+ }
3899
+ if (item.imageVectorInnerSvg && item.imageVectorLocalSize) {
3900
+ const lw = Math.max(1e-6, item.imageVectorLocalSize.width);
3901
+ const lh = Math.max(1e-6, item.imageVectorLocalSize.height);
3902
+ const arB = nb.width / Math.max(1e-9, nb.height);
3903
+ const arI = lw / lh;
3904
+ let childrenSvg;
3905
+ if (Math.abs(arB - arI) < 1e-3) {
3906
+ const s = nb.width / lw;
3907
+ childrenSvg = `<g transform="scale(${s})">${item.imageVectorInnerSvg}</g>`;
3908
+ } else {
3909
+ const s = Math.min(nb.width / lw, nb.height / lh);
3910
+ const tx = (nb.width - lw * s) / 2;
3911
+ const ty = (nb.height - lh * s) / 2;
3912
+ childrenSvg = `<g transform="translate(${tx}, ${ty}) scale(${s})">${item.imageVectorInnerSvg}</g>`;
3913
+ }
3914
+ return {
3915
+ ...item,
3916
+ x: nb.x,
3917
+ y: nb.y,
3918
+ bounds: nb,
3919
+ childrenSvg
3920
+ };
3921
+ }
3922
+ }
3923
+ if ((k === "line" || k === "arrow") && start.line) {
3924
+ const sfw = Math.max(sb.width, 1e-6);
3925
+ const sfh = Math.max(sb.height, 1e-6);
3926
+ const f = start.line;
3927
+ const u1 = f.x1 / sfw;
3928
+ const v1 = f.y1 / sfh;
3929
+ const u2 = f.x2 / sfw;
3930
+ const v2 = f.y2 / sfh;
3931
+ const newLine = {
3932
+ x1: u1 * nb.width,
3933
+ y1: v1 * nb.height,
3934
+ x2: u2 * nb.width,
3935
+ y2: v2 * nb.height
3936
+ };
3937
+ const style = resolveStrokeStyle(item);
3938
+ const childrenSvg = k === "arrow" ? buildArrowSvg(item.id, newLine, style) : buildLineSvg(newLine, style);
3939
+ return {
3940
+ ...item,
3941
+ x: nb.x,
3942
+ y: nb.y,
3943
+ bounds: nb,
3944
+ line: newLine,
3945
+ childrenSvg
3946
+ };
3947
+ }
3948
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item.pathPointsLocal && item.pathPointsLocal.length > 0) {
3949
+ const sfw = Math.max(sb.width, 1e-6);
3950
+ const sfh = Math.max(sb.height, 1e-6);
3951
+ const sx = nb.width / sfw;
3952
+ const sy = nb.height / sfh;
3953
+ const scaledPoints = item.pathPointsLocal.map((p) => ({
3954
+ x: p.x * sx,
3955
+ y: p.y * sy,
3956
+ ...p.pressure != null ? { pressure: p.pressure } : {}
3957
+ }));
3958
+ return rebuildItemSvg({
3959
+ ...item,
3960
+ x: nb.x,
3961
+ y: nb.y,
3962
+ bounds: nb,
3963
+ pathPointsLocal: scaledPoints
3964
+ });
3965
+ }
3966
+ if (k === "custom" && item.customIntrinsicSize && item.customInnerSvg) {
3967
+ return rebuildItemSvg({
3968
+ ...item,
3969
+ x: nb.x,
3970
+ y: nb.y,
3971
+ bounds: nb
3972
+ });
3973
+ }
3974
+ return { ...item, x: nb.x, y: nb.y, bounds: nb };
3975
+ }
3976
+
3590
3977
  // src/native/native-tool-cursors.ts
3591
3978
  var ICON_SIZE = 24;
3592
3979
  var CENTER_HOTSPOT = { x: 12, y: 12 };
@@ -3612,10 +3999,72 @@ function nativeCursorForVectorToolId(toolId) {
3612
3999
  return null;
3613
4000
  }
3614
4001
  }
4002
+ function nativeCrosshairToolCursor() {
4003
+ return { kind: "crosshair", size: ICON_SIZE, hotspot: CENTER_HOTSPOT };
4004
+ }
3615
4005
  function nativeFallbackToolCursorPoint(size) {
3616
4006
  if (size.width <= 0 || size.height <= 0) return null;
3617
4007
  return { x: size.width / 2, y: size.height / 2 };
3618
4008
  }
4009
+
4010
+ // src/native/native-vector-interactions.ts
4011
+ function supportsNativeResizeHandles(item) {
4012
+ const k = item?.toolKind;
4013
+ if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
4014
+ return true;
4015
+ }
4016
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
4017
+ return true;
4018
+ }
4019
+ return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
4020
+ }
4021
+ function hitTestNativeSelectionHandle({
4022
+ selectedItem,
4023
+ selectedCount,
4024
+ worldPoint,
4025
+ zoom
4026
+ }) {
4027
+ if (selectedCount !== 1) return null;
4028
+ if (!selectedItem || selectedItem.locked) return null;
4029
+ if (!supportsNativeResizeHandles(selectedItem)) return null;
4030
+ const bounds = normalizeRect(selectedItem.bounds);
4031
+ const rotation = selectedItem.rotation ?? 0;
4032
+ const handleRadiusWorld = 6 / Math.max(zoom, 1e-9);
4033
+ const rotateOffsetWorld = 24 / Math.max(zoom, 1e-9);
4034
+ if (hitTestRotateHandle(
4035
+ bounds,
4036
+ rotation,
4037
+ worldPoint.x,
4038
+ worldPoint.y,
4039
+ handleRadiusWorld,
4040
+ rotateOffsetWorld
4041
+ )) {
4042
+ return { kind: "rotate" };
4043
+ }
4044
+ const handle = hitTestResizeHandle(
4045
+ bounds,
4046
+ worldPoint.x,
4047
+ worldPoint.y,
4048
+ handleRadiusWorld,
4049
+ rotation
4050
+ );
4051
+ return handle ? { kind: "resize", handle } : null;
4052
+ }
4053
+ function resolveNativeCustomPlacement(toolId, customPlacement, customPlacements) {
4054
+ if (customPlacement?.toolId === toolId) return customPlacement;
4055
+ return customPlacements?.find((placement) => placement.toolId === toolId) ?? null;
4056
+ }
4057
+ function nativeRotationDragStart(input) {
4058
+ const pivotWorld = itemPivotWorld(input.item);
4059
+ return {
4060
+ pivotWorld,
4061
+ startPointerAngleRad: Math.atan2(
4062
+ input.worldPoint.y - pivotWorld.y,
4063
+ input.worldPoint.x - pivotWorld.x
4064
+ ),
4065
+ startRotation: input.item.rotation ?? 0
4066
+ };
4067
+ }
3619
4068
  var MIN_PLACE_SIZE = 8;
3620
4069
  var MIN_ARROW_DRAG_PX = 8;
3621
4070
  var TAP_PX = 20;
@@ -3630,16 +4079,6 @@ function isPlacementTool(toolId) {
3630
4079
  function isDefaultMarkerToolStyle(style) {
3631
4080
  return style.stroke === MARKER_TOOL_STYLE.stroke && style.strokeWidth === MARKER_TOOL_STYLE.strokeWidth && style.strokeOpacity === MARKER_TOOL_STYLE.strokeOpacity;
3632
4081
  }
3633
- function supportsNativeResizeHandles(item) {
3634
- const k = item?.toolKind;
3635
- if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
3636
- return true;
3637
- }
3638
- if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
3639
- return true;
3640
- }
3641
- return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
3642
- }
3643
4082
  function placementPreviewForTool(toolId, start, end) {
3644
4083
  if (toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud") {
3645
4084
  return { kind: toolId, rect: rectFromCorners(start, end) };
@@ -3707,7 +4146,11 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3707
4146
  onSelectionChange,
3708
4147
  onItemsChange,
3709
4148
  onToolChangeRequest,
4149
+ onWorldPointerDown,
3710
4150
  onCameraChange,
4151
+ customPlacement,
4152
+ customPlacements = [],
4153
+ customCrosshairToolIds = [],
3711
4154
  toolbar,
3712
4155
  showStyleInspector = false,
3713
4156
  styleInspectorPlacement = "bottom"
@@ -3720,10 +4163,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3720
4163
  toolLockedRef.current = toolLocked;
3721
4164
  const onToolChangeRequestRef = react.useRef(onToolChangeRequest);
3722
4165
  onToolChangeRequestRef.current = onToolChangeRequest;
4166
+ const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
4167
+ onWorldPointerDownRef.current = onWorldPointerDown;
3723
4168
  const onCameraChangeRef = react.useRef(onCameraChange);
3724
4169
  onCameraChangeRef.current = onCameraChange;
3725
4170
  const onItemsChangeRef = react.useRef(onItemsChange);
3726
4171
  onItemsChangeRef.current = onItemsChange;
4172
+ const customPlacementRef = react.useRef(customPlacement);
4173
+ customPlacementRef.current = customPlacement;
4174
+ const customPlacementsRef = react.useRef(customPlacements);
4175
+ customPlacementsRef.current = customPlacements;
4176
+ const customCrosshairToolIdsRef = react.useRef(customCrosshairToolIds);
4177
+ customCrosshairToolIdsRef.current = customCrosshairToolIds;
3727
4178
  const onSelectionChangeRef = react.useRef(onSelectionChange);
3728
4179
  onSelectionChangeRef.current = onSelectionChange;
3729
4180
  const itemsRef = react.useRef(items);
@@ -3831,6 +4282,11 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3831
4282
  setCameraTick((n) => n + 1);
3832
4283
  onCameraChangeRef.current?.();
3833
4284
  }, []);
4285
+ const cursorForToolId = react.useCallback((nextToolId) => {
4286
+ const builtInCursor = nativeCursorForVectorToolId(nextToolId);
4287
+ if (builtInCursor) return builtInCursor;
4288
+ return customCrosshairToolIdsRef.current.includes(nextToolId) ? nativeCrosshairToolCursor() : null;
4289
+ }, []);
3834
4290
  const onLayout = react.useCallback(
3835
4291
  (e) => {
3836
4292
  const { width, height } = e.nativeEvent.layout;
@@ -3840,7 +4296,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3840
4296
  setToolCursorPoint(null);
3841
4297
  return;
3842
4298
  }
3843
- if (!nativeCursorForVectorToolId(toolIdRef.current)) {
4299
+ if (!cursorForToolId(toolIdRef.current)) {
3844
4300
  setToolCursorPoint(null);
3845
4301
  return;
3846
4302
  }
@@ -3848,15 +4304,15 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3848
4304
  (current) => current ?? nativeFallbackToolCursorPoint(nextSize)
3849
4305
  );
3850
4306
  },
3851
- [interactive]
4307
+ [cursorForToolId, interactive]
3852
4308
  );
3853
4309
  const updateToolCursorPoint = react.useCallback(
3854
4310
  (point) => {
3855
4311
  if (!interactive) return;
3856
- if (!nativeCursorForVectorToolId(toolIdRef.current)) return;
4312
+ if (!cursorForToolId(toolIdRef.current)) return;
3857
4313
  setToolCursorPoint(point);
3858
4314
  },
3859
- [interactive]
4315
+ [cursorForToolId, interactive]
3860
4316
  );
3861
4317
  const hideToolCursor = react.useCallback(() => {
3862
4318
  setToolCursorPoint(null);
@@ -3867,7 +4323,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3867
4323
  setToolCursorPoint(null);
3868
4324
  return;
3869
4325
  }
3870
- if (!nativeCursorForVectorToolId(nextToolId)) {
4326
+ if (!cursorForToolId(nextToolId)) {
3871
4327
  setToolCursorPoint(null);
3872
4328
  return;
3873
4329
  }
@@ -3875,7 +4331,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3875
4331
  (current) => current ?? nativeFallbackToolCursorPoint(size)
3876
4332
  );
3877
4333
  },
3878
- [interactive, size]
4334
+ [cursorForToolId, interactive, size]
3879
4335
  );
3880
4336
  react.useEffect(() => {
3881
4337
  showFallbackToolCursor(toolId);
@@ -3927,6 +4383,40 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3927
4383
  return;
3928
4384
  }
3929
4385
  if (tool === "select") {
4386
+ const currentSelectedIds = selectedIdsRef.current;
4387
+ const selectedItem = currentSelectedIds.length === 1 ? itemsRef.current.find((item) => item.id === currentSelectedIds[0]) : void 0;
4388
+ const selectionHandle = hitTestNativeSelectionHandle({
4389
+ selectedItem,
4390
+ selectedCount: currentSelectedIds.length,
4391
+ worldPoint: { x: worldX, y: worldY },
4392
+ zoom: cam.zoom
4393
+ });
4394
+ if (selectionHandle && selectedItem) {
4395
+ if (selectionHandle.kind === "rotate") {
4396
+ const rotationStart = nativeRotationDragStart({
4397
+ item: selectedItem,
4398
+ worldPoint: { x: worldX, y: worldY }
4399
+ });
4400
+ dragStateRef.current = {
4401
+ kind: "rotate",
4402
+ id: selectedItem.id,
4403
+ snapshot: selectedItem,
4404
+ ...rotationStart
4405
+ };
4406
+ return;
4407
+ }
4408
+ dragStateRef.current = {
4409
+ kind: "resize",
4410
+ id: selectedItem.id,
4411
+ handle: selectionHandle.handle,
4412
+ snapshot: selectedItem,
4413
+ start: {
4414
+ bounds: selectedItem.bounds,
4415
+ line: selectedItem.line
4416
+ }
4417
+ };
4418
+ return;
4419
+ }
3930
4420
  const hit = hitTestWorldPoint(itemsRef.current, worldX, worldY, {
3931
4421
  lineHitWorld: 10 / cam.zoom,
3932
4422
  ignoreLocked: true
@@ -4019,6 +4509,25 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4019
4509
  );
4020
4510
  return;
4021
4511
  }
4512
+ const customPlacement2 = resolveNativeCustomPlacement(
4513
+ tool,
4514
+ customPlacementRef.current,
4515
+ customPlacementsRef.current
4516
+ );
4517
+ if (customPlacement2) {
4518
+ dragStateRef.current = {
4519
+ kind: "custom-place",
4520
+ tool,
4521
+ placement: customPlacement2,
4522
+ startWorld: { x: worldX, y: worldY },
4523
+ startScreen: { x: sx, y: sy }
4524
+ };
4525
+ setPlacementPreview({
4526
+ kind: "rect",
4527
+ rect: { x: worldX, y: worldY, width: 0, height: 0 }
4528
+ });
4529
+ return;
4530
+ }
4022
4531
  if (tool === "note" || tool === "text") {
4023
4532
  dragStateRef.current = {
4024
4533
  kind: "tap",
@@ -4028,6 +4537,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4028
4537
  };
4029
4538
  return;
4030
4539
  }
4540
+ const handleWorldPointerDown = onWorldPointerDownRef.current;
4541
+ if (handleWorldPointerDown) {
4542
+ handleWorldPointerDown({
4543
+ toolId: tool,
4544
+ worldX,
4545
+ worldY,
4546
+ screenX: sx,
4547
+ screenY: sy
4548
+ });
4549
+ requestSelectToolAfterUse();
4550
+ return;
4551
+ }
4031
4552
  dragStateRef.current = { kind: "pan" };
4032
4553
  },
4033
4554
  onPanResponderMove: (evt) => {
@@ -4123,6 +4644,36 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4123
4644
  change(nextList);
4124
4645
  return;
4125
4646
  }
4647
+ if (st.kind === "rotate") {
4648
+ const change = onItemsChangeRef.current;
4649
+ if (!change) return;
4650
+ const angle = Math.atan2(
4651
+ worldY - st.pivotWorld.y,
4652
+ worldX - st.pivotWorld.x
4653
+ );
4654
+ const next = applyRotationFromPointer(
4655
+ st.snapshot,
4656
+ st.startRotation,
4657
+ st.startPointerAngleRad,
4658
+ angle
4659
+ );
4660
+ change(
4661
+ itemsRef.current.map((item) => item.id === st.id ? next : item)
4662
+ );
4663
+ return;
4664
+ }
4665
+ if (st.kind === "resize") {
4666
+ const change = onItemsChangeRef.current;
4667
+ if (!change) return;
4668
+ const next = resizeItemByHandle(st.snapshot, st.start, st.handle, {
4669
+ x: worldX,
4670
+ y: worldY
4671
+ });
4672
+ change(
4673
+ itemsRef.current.map((item) => item.id === st.id ? next : item)
4674
+ );
4675
+ return;
4676
+ }
4126
4677
  if (st.kind === "marquee") {
4127
4678
  const a = st.startWorld;
4128
4679
  const b = { x: worldX, y: worldY };
@@ -4161,6 +4712,13 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4161
4712
  );
4162
4713
  return;
4163
4714
  }
4715
+ if (st.kind === "custom-place") {
4716
+ setPlacementPreview({
4717
+ kind: "rect",
4718
+ rect: rectFromCorners(st.startWorld, { x: worldX, y: worldY })
4719
+ });
4720
+ return;
4721
+ }
4164
4722
  },
4165
4723
  onPanResponderRelease: (evt) => {
4166
4724
  lastPinchDist.current = null;
@@ -4204,6 +4762,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4204
4762
  dragStateRef.current = { kind: "idle" };
4205
4763
  return;
4206
4764
  }
4765
+ if (st.kind === "resize" || st.kind === "rotate") {
4766
+ dragStateRef.current = { kind: "idle" };
4767
+ return;
4768
+ }
4207
4769
  if (st.kind === "marquee") {
4208
4770
  dragStateRef.current = { kind: "idle" };
4209
4771
  setPlacementPreview(null);
@@ -4301,6 +4863,34 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4301
4863
  requestSelectToolAfterUse();
4302
4864
  return;
4303
4865
  }
4866
+ if (st.kind === "custom-place") {
4867
+ dragStateRef.current = { kind: "idle" };
4868
+ setPlacementPreview(null);
4869
+ const change = onItemsChangeRef.current;
4870
+ if (!change) return;
4871
+ const { worldX, worldY } = screenToWorld(
4872
+ evt.nativeEvent.locationX,
4873
+ evt.nativeEvent.locationY
4874
+ );
4875
+ const center = {
4876
+ x: (st.startWorld.x + worldX) / 2,
4877
+ y: (st.startWorld.y + worldY) / 2
4878
+ };
4879
+ const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
4880
+ const normalized = normalizeRect(raw);
4881
+ const bounds = normalized.width < MIN_PLACE_SIZE || normalized.height < MIN_PLACE_SIZE ? {
4882
+ x: center.x - 60,
4883
+ y: center.y - 40,
4884
+ width: 120,
4885
+ height: 80
4886
+ } : normalized;
4887
+ const id = createShapeId();
4888
+ const item = st.placement.createItem({ id, bounds });
4889
+ change([...itemsRef.current, item]);
4890
+ onSelectionChangeRef.current?.([id]);
4891
+ requestSelectToolAfterUse();
4892
+ return;
4893
+ }
4304
4894
  if (st.kind === "tap") {
4305
4895
  dragStateRef.current = { kind: "idle" };
4306
4896
  const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
@@ -4392,7 +4982,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4392
4982
  [requestRender, size]
4393
4983
  );
4394
4984
  const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
4395
- const activeToolCursor = nativeCursorForVectorToolId(toolId);
4985
+ const activeToolCursor = cursorForToolId(toolId);
4396
4986
  const toolCursor = activeToolCursor && toolCursorPoint ? { cursor: activeToolCursor, point: toolCursorPoint } : null;
4397
4987
  return /* @__PURE__ */ jsxRuntime.jsx(
4398
4988
  reactNative.View,