canvu-react 0.4.16 → 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,6 +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
+ }
4005
+ function nativeFallbackToolCursorPoint(size) {
4006
+ if (size.width <= 0 || size.height <= 0) return null;
4007
+ return { x: size.width / 2, y: size.height / 2 };
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
+ }
3615
4068
  var MIN_PLACE_SIZE = 8;
3616
4069
  var MIN_ARROW_DRAG_PX = 8;
3617
4070
  var TAP_PX = 20;
@@ -3626,16 +4079,6 @@ function isPlacementTool(toolId) {
3626
4079
  function isDefaultMarkerToolStyle(style) {
3627
4080
  return style.stroke === MARKER_TOOL_STYLE.stroke && style.strokeWidth === MARKER_TOOL_STYLE.strokeWidth && style.strokeOpacity === MARKER_TOOL_STYLE.strokeOpacity;
3628
4081
  }
3629
- function supportsNativeResizeHandles(item) {
3630
- const k = item?.toolKind;
3631
- if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
3632
- return true;
3633
- }
3634
- if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
3635
- return true;
3636
- }
3637
- return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
3638
- }
3639
4082
  function placementPreviewForTool(toolId, start, end) {
3640
4083
  if (toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud") {
3641
4084
  return { kind: toolId, rect: rectFromCorners(start, end) };
@@ -3703,7 +4146,11 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3703
4146
  onSelectionChange,
3704
4147
  onItemsChange,
3705
4148
  onToolChangeRequest,
4149
+ onWorldPointerDown,
3706
4150
  onCameraChange,
4151
+ customPlacement,
4152
+ customPlacements = [],
4153
+ customCrosshairToolIds = [],
3707
4154
  toolbar,
3708
4155
  showStyleInspector = false,
3709
4156
  styleInspectorPlacement = "bottom"
@@ -3716,10 +4163,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3716
4163
  toolLockedRef.current = toolLocked;
3717
4164
  const onToolChangeRequestRef = react.useRef(onToolChangeRequest);
3718
4165
  onToolChangeRequestRef.current = onToolChangeRequest;
4166
+ const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
4167
+ onWorldPointerDownRef.current = onWorldPointerDown;
3719
4168
  const onCameraChangeRef = react.useRef(onCameraChange);
3720
4169
  onCameraChangeRef.current = onCameraChange;
3721
4170
  const onItemsChangeRef = react.useRef(onItemsChange);
3722
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;
3723
4178
  const onSelectionChangeRef = react.useRef(onSelectionChange);
3724
4179
  onSelectionChangeRef.current = onSelectionChange;
3725
4180
  const itemsRef = react.useRef(items);
@@ -3827,21 +4282,60 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3827
4282
  setCameraTick((n) => n + 1);
3828
4283
  onCameraChangeRef.current?.();
3829
4284
  }, []);
3830
- const onLayout = react.useCallback((e) => {
3831
- const { width, height } = e.nativeEvent.layout;
3832
- setSize({ width, height });
4285
+ const cursorForToolId = react.useCallback((nextToolId) => {
4286
+ const builtInCursor = nativeCursorForVectorToolId(nextToolId);
4287
+ if (builtInCursor) return builtInCursor;
4288
+ return customCrosshairToolIdsRef.current.includes(nextToolId) ? nativeCrosshairToolCursor() : null;
3833
4289
  }, []);
4290
+ const onLayout = react.useCallback(
4291
+ (e) => {
4292
+ const { width, height } = e.nativeEvent.layout;
4293
+ const nextSize = { width, height };
4294
+ setSize(nextSize);
4295
+ if (!interactive) {
4296
+ setToolCursorPoint(null);
4297
+ return;
4298
+ }
4299
+ if (!cursorForToolId(toolIdRef.current)) {
4300
+ setToolCursorPoint(null);
4301
+ return;
4302
+ }
4303
+ setToolCursorPoint(
4304
+ (current) => current ?? nativeFallbackToolCursorPoint(nextSize)
4305
+ );
4306
+ },
4307
+ [cursorForToolId, interactive]
4308
+ );
3834
4309
  const updateToolCursorPoint = react.useCallback(
3835
4310
  (point) => {
3836
4311
  if (!interactive) return;
3837
- if (!nativeCursorForVectorToolId(toolIdRef.current)) return;
4312
+ if (!cursorForToolId(toolIdRef.current)) return;
3838
4313
  setToolCursorPoint(point);
3839
4314
  },
3840
- [interactive]
4315
+ [cursorForToolId, interactive]
3841
4316
  );
3842
4317
  const hideToolCursor = react.useCallback(() => {
3843
4318
  setToolCursorPoint(null);
3844
4319
  }, []);
4320
+ const showFallbackToolCursor = react.useCallback(
4321
+ (nextToolId) => {
4322
+ if (!interactive) {
4323
+ setToolCursorPoint(null);
4324
+ return;
4325
+ }
4326
+ if (!cursorForToolId(nextToolId)) {
4327
+ setToolCursorPoint(null);
4328
+ return;
4329
+ }
4330
+ setToolCursorPoint(
4331
+ (current) => current ?? nativeFallbackToolCursorPoint(size)
4332
+ );
4333
+ },
4334
+ [cursorForToolId, interactive, size]
4335
+ );
4336
+ react.useEffect(() => {
4337
+ showFallbackToolCursor(toolId);
4338
+ }, [showFallbackToolCursor, toolId]);
3845
4339
  const handlePointerMove = react.useCallback(
3846
4340
  (event) => {
3847
4341
  updateToolCursorPoint(screenPointFromPointerEvent(event));
@@ -3889,6 +4383,40 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3889
4383
  return;
3890
4384
  }
3891
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
+ }
3892
4420
  const hit = hitTestWorldPoint(itemsRef.current, worldX, worldY, {
3893
4421
  lineHitWorld: 10 / cam.zoom,
3894
4422
  ignoreLocked: true
@@ -3981,6 +4509,25 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3981
4509
  );
3982
4510
  return;
3983
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
+ }
3984
4531
  if (tool === "note" || tool === "text") {
3985
4532
  dragStateRef.current = {
3986
4533
  kind: "tap",
@@ -3990,6 +4537,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3990
4537
  };
3991
4538
  return;
3992
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
+ }
3993
4552
  dragStateRef.current = { kind: "pan" };
3994
4553
  },
3995
4554
  onPanResponderMove: (evt) => {
@@ -4085,6 +4644,36 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4085
4644
  change(nextList);
4086
4645
  return;
4087
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
+ }
4088
4677
  if (st.kind === "marquee") {
4089
4678
  const a = st.startWorld;
4090
4679
  const b = { x: worldX, y: worldY };
@@ -4123,11 +4712,21 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4123
4712
  );
4124
4713
  return;
4125
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
+ }
4126
4722
  },
4127
4723
  onPanResponderRelease: (evt) => {
4128
4724
  lastPinchDist.current = null;
4129
4725
  lastPanPoint.current = null;
4130
- hideToolCursor();
4726
+ updateToolCursorPoint({
4727
+ x: evt.nativeEvent.locationX,
4728
+ y: evt.nativeEvent.locationY
4729
+ });
4131
4730
  const st = dragStateRef.current;
4132
4731
  if (st.kind === "draw") {
4133
4732
  dragStateRef.current = { kind: "idle" };
@@ -4163,6 +4762,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4163
4762
  dragStateRef.current = { kind: "idle" };
4164
4763
  return;
4165
4764
  }
4765
+ if (st.kind === "resize" || st.kind === "rotate") {
4766
+ dragStateRef.current = { kind: "idle" };
4767
+ return;
4768
+ }
4166
4769
  if (st.kind === "marquee") {
4167
4770
  dragStateRef.current = { kind: "idle" };
4168
4771
  setPlacementPreview(null);
@@ -4260,6 +4863,34 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4260
4863
  requestSelectToolAfterUse();
4261
4864
  return;
4262
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
+ }
4263
4894
  if (st.kind === "tap") {
4264
4895
  dragStateRef.current = { kind: "idle" };
4265
4896
  const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
@@ -4351,7 +4982,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4351
4982
  [requestRender, size]
4352
4983
  );
4353
4984
  const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
4354
- const activeToolCursor = nativeCursorForVectorToolId(toolId);
4985
+ const activeToolCursor = cursorForToolId(toolId);
4355
4986
  const toolCursor = activeToolCursor && toolCursorPoint ? { cursor: activeToolCursor, point: toolCursorPoint } : null;
4356
4987
  return /* @__PURE__ */ jsxRuntime.jsx(
4357
4988
  reactNative.View,