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.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,73 @@ 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
+ var NATIVE_SELECTION_HANDLE_HIT_RADIUS_PX = 24;
4012
+ function supportsNativeResizeHandles(item) {
4013
+ const k = item?.toolKind;
4014
+ if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
4015
+ return true;
4016
+ }
4017
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
4018
+ return true;
4019
+ }
4020
+ return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
4021
+ }
4022
+ function hitTestNativeSelectionHandle({
4023
+ selectedItem,
4024
+ selectedCount,
4025
+ worldPoint,
4026
+ zoom
4027
+ }) {
4028
+ if (selectedCount !== 1) return null;
4029
+ if (!selectedItem || selectedItem.locked) return null;
4030
+ if (!supportsNativeResizeHandles(selectedItem)) return null;
4031
+ const bounds = normalizeRect(selectedItem.bounds);
4032
+ const rotation = selectedItem.rotation ?? 0;
4033
+ const handleRadiusWorld = NATIVE_SELECTION_HANDLE_HIT_RADIUS_PX / Math.max(zoom, 1e-9);
4034
+ const rotateOffsetWorld = 24 / Math.max(zoom, 1e-9);
4035
+ if (hitTestRotateHandle(
4036
+ bounds,
4037
+ rotation,
4038
+ worldPoint.x,
4039
+ worldPoint.y,
4040
+ handleRadiusWorld,
4041
+ rotateOffsetWorld
4042
+ )) {
4043
+ return { kind: "rotate" };
4044
+ }
4045
+ const handle = hitTestResizeHandle(
4046
+ bounds,
4047
+ worldPoint.x,
4048
+ worldPoint.y,
4049
+ handleRadiusWorld,
4050
+ rotation
4051
+ );
4052
+ return handle ? { kind: "resize", handle } : null;
4053
+ }
4054
+ function resolveNativeCustomPlacement(toolId, customPlacement, customPlacements) {
4055
+ if (customPlacement?.toolId === toolId) return customPlacement;
4056
+ return customPlacements?.find((placement) => placement.toolId === toolId) ?? null;
4057
+ }
4058
+ function nativeRotationDragStart(input) {
4059
+ const pivotWorld = itemPivotWorld(input.item);
4060
+ return {
4061
+ pivotWorld,
4062
+ startPointerAngleRad: Math.atan2(
4063
+ input.worldPoint.y - pivotWorld.y,
4064
+ input.worldPoint.x - pivotWorld.x
4065
+ ),
4066
+ startRotation: input.item.rotation ?? 0
4067
+ };
4068
+ }
3619
4069
  var MIN_PLACE_SIZE = 8;
3620
4070
  var MIN_ARROW_DRAG_PX = 8;
3621
4071
  var TAP_PX = 20;
@@ -3630,16 +4080,6 @@ function isPlacementTool(toolId) {
3630
4080
  function isDefaultMarkerToolStyle(style) {
3631
4081
  return style.stroke === MARKER_TOOL_STYLE.stroke && style.strokeWidth === MARKER_TOOL_STYLE.strokeWidth && style.strokeOpacity === MARKER_TOOL_STYLE.strokeOpacity;
3632
4082
  }
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
4083
  function placementPreviewForTool(toolId, start, end) {
3644
4084
  if (toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud") {
3645
4085
  return { kind: toolId, rect: rectFromCorners(start, end) };
@@ -3707,7 +4147,11 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3707
4147
  onSelectionChange,
3708
4148
  onItemsChange,
3709
4149
  onToolChangeRequest,
4150
+ onWorldPointerDown,
3710
4151
  onCameraChange,
4152
+ customPlacement,
4153
+ customPlacements = [],
4154
+ customCrosshairToolIds = [],
3711
4155
  toolbar,
3712
4156
  showStyleInspector = false,
3713
4157
  styleInspectorPlacement = "bottom"
@@ -3720,10 +4164,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3720
4164
  toolLockedRef.current = toolLocked;
3721
4165
  const onToolChangeRequestRef = react.useRef(onToolChangeRequest);
3722
4166
  onToolChangeRequestRef.current = onToolChangeRequest;
4167
+ const onWorldPointerDownRef = react.useRef(onWorldPointerDown);
4168
+ onWorldPointerDownRef.current = onWorldPointerDown;
3723
4169
  const onCameraChangeRef = react.useRef(onCameraChange);
3724
4170
  onCameraChangeRef.current = onCameraChange;
3725
4171
  const onItemsChangeRef = react.useRef(onItemsChange);
3726
4172
  onItemsChangeRef.current = onItemsChange;
4173
+ const customPlacementRef = react.useRef(customPlacement);
4174
+ customPlacementRef.current = customPlacement;
4175
+ const customPlacementsRef = react.useRef(customPlacements);
4176
+ customPlacementsRef.current = customPlacements;
4177
+ const customCrosshairToolIdsRef = react.useRef(customCrosshairToolIds);
4178
+ customCrosshairToolIdsRef.current = customCrosshairToolIds;
3727
4179
  const onSelectionChangeRef = react.useRef(onSelectionChange);
3728
4180
  onSelectionChangeRef.current = onSelectionChange;
3729
4181
  const itemsRef = react.useRef(items);
@@ -3831,6 +4283,11 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3831
4283
  setCameraTick((n) => n + 1);
3832
4284
  onCameraChangeRef.current?.();
3833
4285
  }, []);
4286
+ const cursorForToolId = react.useCallback((nextToolId) => {
4287
+ const builtInCursor = nativeCursorForVectorToolId(nextToolId);
4288
+ if (builtInCursor) return builtInCursor;
4289
+ return customCrosshairToolIdsRef.current.includes(nextToolId) ? nativeCrosshairToolCursor() : null;
4290
+ }, []);
3834
4291
  const onLayout = react.useCallback(
3835
4292
  (e) => {
3836
4293
  const { width, height } = e.nativeEvent.layout;
@@ -3840,7 +4297,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3840
4297
  setToolCursorPoint(null);
3841
4298
  return;
3842
4299
  }
3843
- if (!nativeCursorForVectorToolId(toolIdRef.current)) {
4300
+ if (!cursorForToolId(toolIdRef.current)) {
3844
4301
  setToolCursorPoint(null);
3845
4302
  return;
3846
4303
  }
@@ -3848,15 +4305,15 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3848
4305
  (current) => current ?? nativeFallbackToolCursorPoint(nextSize)
3849
4306
  );
3850
4307
  },
3851
- [interactive]
4308
+ [cursorForToolId, interactive]
3852
4309
  );
3853
4310
  const updateToolCursorPoint = react.useCallback(
3854
4311
  (point) => {
3855
4312
  if (!interactive) return;
3856
- if (!nativeCursorForVectorToolId(toolIdRef.current)) return;
4313
+ if (!cursorForToolId(toolIdRef.current)) return;
3857
4314
  setToolCursorPoint(point);
3858
4315
  },
3859
- [interactive]
4316
+ [cursorForToolId, interactive]
3860
4317
  );
3861
4318
  const hideToolCursor = react.useCallback(() => {
3862
4319
  setToolCursorPoint(null);
@@ -3867,7 +4324,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3867
4324
  setToolCursorPoint(null);
3868
4325
  return;
3869
4326
  }
3870
- if (!nativeCursorForVectorToolId(nextToolId)) {
4327
+ if (!cursorForToolId(nextToolId)) {
3871
4328
  setToolCursorPoint(null);
3872
4329
  return;
3873
4330
  }
@@ -3875,17 +4332,11 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3875
4332
  (current) => current ?? nativeFallbackToolCursorPoint(size)
3876
4333
  );
3877
4334
  },
3878
- [interactive, size]
4335
+ [cursorForToolId, interactive, size]
3879
4336
  );
3880
4337
  react.useEffect(() => {
3881
4338
  showFallbackToolCursor(toolId);
3882
4339
  }, [showFallbackToolCursor, toolId]);
3883
- const handlePointerMove = react.useCallback(
3884
- (event) => {
3885
- updateToolCursorPoint(screenPointFromPointerEvent(event));
3886
- },
3887
- [updateToolCursorPoint]
3888
- );
3889
4340
  const selectedItems = react.useMemo(
3890
4341
  () => items.filter((it) => selectedIds.includes(it.id)),
3891
4342
  [items, selectedIds]
@@ -3898,6 +4349,153 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3898
4349
  const showResizeHandles = interactive && selectedItems.length === 1 && !selectedItems[0]?.locked && supportsNativeResizeHandles(selectedItems[0]);
3899
4350
  const lastPinchDist = react.useRef(null);
3900
4351
  const lastPanPoint = react.useRef(null);
4352
+ const applyDragMoveAtScreenPoint = react.useCallback(
4353
+ (point, pagePoint) => {
4354
+ const cam = cameraRef.current;
4355
+ if (!cam) return;
4356
+ updateToolCursorPoint(point);
4357
+ const { worldX, worldY } = screenToWorld(point.x, point.y);
4358
+ const st = dragStateRef.current;
4359
+ if (st.kind === "pan") {
4360
+ const current = pagePoint ?? point;
4361
+ if (lastPanPoint.current) {
4362
+ const dx = current.x - lastPanPoint.current.x;
4363
+ const dy = current.y - lastPanPoint.current.y;
4364
+ cam.x += dx;
4365
+ cam.y += dy;
4366
+ requestRender();
4367
+ }
4368
+ lastPanPoint.current = current;
4369
+ return;
4370
+ }
4371
+ lastPanPoint.current = null;
4372
+ if (st.kind === "draw") {
4373
+ const pts = st.points;
4374
+ const last = pts[pts.length - 1];
4375
+ const dx = worldX - (last?.x ?? worldX);
4376
+ const dy = worldY - (last?.y ?? worldY);
4377
+ const shouldAppendPoint = Math.hypot(dx, dy) > 0.5 / cam.zoom;
4378
+ if (shouldAppendPoint) {
4379
+ pts.push({ x: worldX, y: worldY });
4380
+ }
4381
+ if (st.tool === "laser") {
4382
+ if (shouldAppendPoint) {
4383
+ setLaserTrail((prev) => [
4384
+ ...prev,
4385
+ { x: worldX, y: worldY, t: Date.now() }
4386
+ ]);
4387
+ }
4388
+ return;
4389
+ }
4390
+ setPlacementPreview({
4391
+ kind: "stroke",
4392
+ tool: st.tool,
4393
+ points: [...pts],
4394
+ style: { ...strokeStyleRef.current }
4395
+ });
4396
+ return;
4397
+ }
4398
+ if (st.kind === "move") {
4399
+ const dx = worldX - st.startWorld.x;
4400
+ const dy = worldY - st.startWorld.y;
4401
+ const change = onItemsChangeRef.current;
4402
+ if (!change) return;
4403
+ const nextList = itemsRef.current.map((it) => {
4404
+ const snap = st.snapshots[it.id];
4405
+ if (!snap) return it;
4406
+ return {
4407
+ ...snap,
4408
+ x: snap.x + dx,
4409
+ y: snap.y + dy,
4410
+ bounds: {
4411
+ ...snap.bounds,
4412
+ x: snap.bounds.x + dx,
4413
+ y: snap.bounds.y + dy
4414
+ }
4415
+ };
4416
+ });
4417
+ change(nextList);
4418
+ return;
4419
+ }
4420
+ if (st.kind === "rotate") {
4421
+ const change = onItemsChangeRef.current;
4422
+ if (!change) return;
4423
+ const angle = Math.atan2(worldY - st.pivotWorld.y, worldX - st.pivotWorld.x);
4424
+ const next = applyRotationFromPointer(
4425
+ st.snapshot,
4426
+ st.startRotation,
4427
+ st.startPointerAngleRad,
4428
+ angle
4429
+ );
4430
+ change(itemsRef.current.map((item) => item.id === st.id ? next : item));
4431
+ return;
4432
+ }
4433
+ if (st.kind === "resize") {
4434
+ const change = onItemsChangeRef.current;
4435
+ if (!change) return;
4436
+ const next = resizeItemByHandle(st.snapshot, st.start, st.handle, {
4437
+ x: worldX,
4438
+ y: worldY
4439
+ });
4440
+ change(itemsRef.current.map((item) => item.id === st.id ? next : item));
4441
+ return;
4442
+ }
4443
+ if (st.kind === "marquee") {
4444
+ const a = st.startWorld;
4445
+ const b = { x: worldX, y: worldY };
4446
+ const rect = {
4447
+ x: Math.min(a.x, b.x),
4448
+ y: Math.min(a.y, b.y),
4449
+ width: Math.abs(b.x - a.x),
4450
+ height: Math.abs(b.y - a.y)
4451
+ };
4452
+ setPlacementPreview({ kind: "marquee", rect });
4453
+ return;
4454
+ }
4455
+ if (st.kind === "erase") {
4456
+ setEraserTrail((prev) => [...prev, { x: worldX, y: worldY, t: Date.now() }]);
4457
+ const toErase = collectEraserTargetsAtWorldPoint(
4458
+ itemsRef.current,
4459
+ worldX,
4460
+ worldY,
4461
+ { lineHitWorld: 10 / cam.zoom, ignoreLocked: true }
4462
+ );
4463
+ for (const id of toErase) {
4464
+ eraserPreviewIdSetRef.current.add(id);
4465
+ }
4466
+ setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
4467
+ return;
4468
+ }
4469
+ if (st.kind === "place") {
4470
+ setPlacementPreview(
4471
+ placementPreviewForTool(st.tool, st.startWorld, {
4472
+ x: worldX,
4473
+ y: worldY
4474
+ })
4475
+ );
4476
+ return;
4477
+ }
4478
+ if (st.kind === "custom-place") {
4479
+ setPlacementPreview({
4480
+ kind: "rect",
4481
+ rect: rectFromCorners(st.startWorld, { x: worldX, y: worldY })
4482
+ });
4483
+ return;
4484
+ }
4485
+ },
4486
+ [requestRender, screenToWorld, updateToolCursorPoint]
4487
+ );
4488
+ const handlePointerMove = react.useCallback(
4489
+ (event) => {
4490
+ const point = screenPointFromPointerEvent(event);
4491
+ if (dragStateRef.current.kind !== "idle") {
4492
+ applyDragMoveAtScreenPoint(point, point);
4493
+ return;
4494
+ }
4495
+ updateToolCursorPoint(point);
4496
+ },
4497
+ [applyDragMoveAtScreenPoint, updateToolCursorPoint]
4498
+ );
3901
4499
  const panResponder = react.useMemo(
3902
4500
  () => reactNative.PanResponder.create({
3903
4501
  onStartShouldSetPanResponder: () => true,
@@ -3927,6 +4525,40 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
3927
4525
  return;
3928
4526
  }
3929
4527
  if (tool === "select") {
4528
+ const currentSelectedIds = selectedIdsRef.current;
4529
+ const selectedItem = currentSelectedIds.length === 1 ? itemsRef.current.find((item) => item.id === currentSelectedIds[0]) : void 0;
4530
+ const selectionHandle = hitTestNativeSelectionHandle({
4531
+ selectedItem,
4532
+ selectedCount: currentSelectedIds.length,
4533
+ worldPoint: { x: worldX, y: worldY },
4534
+ zoom: cam.zoom
4535
+ });
4536
+ if (selectionHandle && selectedItem) {
4537
+ if (selectionHandle.kind === "rotate") {
4538
+ const rotationStart = nativeRotationDragStart({
4539
+ item: selectedItem,
4540
+ worldPoint: { x: worldX, y: worldY }
4541
+ });
4542
+ dragStateRef.current = {
4543
+ kind: "rotate",
4544
+ id: selectedItem.id,
4545
+ snapshot: selectedItem,
4546
+ ...rotationStart
4547
+ };
4548
+ return;
4549
+ }
4550
+ dragStateRef.current = {
4551
+ kind: "resize",
4552
+ id: selectedItem.id,
4553
+ handle: selectionHandle.handle,
4554
+ snapshot: selectedItem,
4555
+ start: {
4556
+ bounds: selectedItem.bounds,
4557
+ line: selectedItem.line
4558
+ }
4559
+ };
4560
+ return;
4561
+ }
3930
4562
  const hit = hitTestWorldPoint(itemsRef.current, worldX, worldY, {
3931
4563
  lineHitWorld: 10 / cam.zoom,
3932
4564
  ignoreLocked: true
@@ -4019,6 +4651,25 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4019
4651
  );
4020
4652
  return;
4021
4653
  }
4654
+ const customPlacement2 = resolveNativeCustomPlacement(
4655
+ tool,
4656
+ customPlacementRef.current,
4657
+ customPlacementsRef.current
4658
+ );
4659
+ if (customPlacement2) {
4660
+ dragStateRef.current = {
4661
+ kind: "custom-place",
4662
+ tool,
4663
+ placement: customPlacement2,
4664
+ startWorld: { x: worldX, y: worldY },
4665
+ startScreen: { x: sx, y: sy }
4666
+ };
4667
+ setPlacementPreview({
4668
+ kind: "rect",
4669
+ rect: { x: worldX, y: worldY, width: 0, height: 0 }
4670
+ });
4671
+ return;
4672
+ }
4022
4673
  if (tool === "note" || tool === "text") {
4023
4674
  dragStateRef.current = {
4024
4675
  kind: "tap",
@@ -4028,6 +4679,18 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4028
4679
  };
4029
4680
  return;
4030
4681
  }
4682
+ const handleWorldPointerDown = onWorldPointerDownRef.current;
4683
+ if (handleWorldPointerDown) {
4684
+ handleWorldPointerDown({
4685
+ toolId: tool,
4686
+ worldX,
4687
+ worldY,
4688
+ screenX: sx,
4689
+ screenY: sy
4690
+ });
4691
+ requestSelectToolAfterUse();
4692
+ return;
4693
+ }
4031
4694
  dragStateRef.current = { kind: "pan" };
4032
4695
  },
4033
4696
  onPanResponderMove: (evt) => {
@@ -4059,108 +4722,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4059
4722
  return;
4060
4723
  }
4061
4724
  lastPinchDist.current = null;
4062
- updateToolCursorPoint({ x: sx, y: sy });
4063
- const { worldX, worldY } = screenToWorld(sx, sy);
4064
- const st = dragStateRef.current;
4065
- if (st.kind === "pan") {
4066
- const current = { x: pageX, y: pageY };
4067
- if (lastPanPoint.current) {
4068
- const dx = current.x - lastPanPoint.current.x;
4069
- const dy = current.y - lastPanPoint.current.y;
4070
- cam.x += dx;
4071
- cam.y += dy;
4072
- requestRender();
4073
- }
4074
- lastPanPoint.current = current;
4075
- return;
4076
- }
4077
- lastPanPoint.current = null;
4078
- if (st.kind === "draw") {
4079
- const pts = st.points;
4080
- const last = pts[pts.length - 1];
4081
- const dx = worldX - (last?.x ?? worldX);
4082
- const dy = worldY - (last?.y ?? worldY);
4083
- const shouldAppendPoint = Math.hypot(dx, dy) > 0.5 / cam.zoom;
4084
- if (shouldAppendPoint) {
4085
- pts.push({ x: worldX, y: worldY });
4086
- }
4087
- if (st.tool === "laser") {
4088
- if (shouldAppendPoint) {
4089
- setLaserTrail((prev) => [
4090
- ...prev,
4091
- { x: worldX, y: worldY, t: Date.now() }
4092
- ]);
4093
- }
4094
- return;
4095
- }
4096
- setPlacementPreview({
4097
- kind: "stroke",
4098
- tool: st.tool,
4099
- points: [...pts],
4100
- style: { ...strokeStyleRef.current }
4101
- });
4102
- return;
4103
- }
4104
- if (st.kind === "move") {
4105
- const dx = worldX - st.startWorld.x;
4106
- const dy = worldY - st.startWorld.y;
4107
- const change = onItemsChangeRef.current;
4108
- if (!change) return;
4109
- const nextList = itemsRef.current.map((it) => {
4110
- const snap = st.snapshots[it.id];
4111
- if (!snap) return it;
4112
- return {
4113
- ...snap,
4114
- x: snap.x + dx,
4115
- y: snap.y + dy,
4116
- bounds: {
4117
- ...snap.bounds,
4118
- x: snap.bounds.x + dx,
4119
- y: snap.bounds.y + dy
4120
- }
4121
- };
4122
- });
4123
- change(nextList);
4124
- return;
4125
- }
4126
- if (st.kind === "marquee") {
4127
- const a = st.startWorld;
4128
- const b = { x: worldX, y: worldY };
4129
- const rect = {
4130
- x: Math.min(a.x, b.x),
4131
- y: Math.min(a.y, b.y),
4132
- width: Math.abs(b.x - a.x),
4133
- height: Math.abs(b.y - a.y)
4134
- };
4135
- setPlacementPreview({ kind: "marquee", rect });
4136
- return;
4137
- }
4138
- if (st.kind === "erase") {
4139
- setEraserTrail((prev) => [
4140
- ...prev,
4141
- { x: worldX, y: worldY, t: Date.now() }
4142
- ]);
4143
- const toErase = collectEraserTargetsAtWorldPoint(
4144
- itemsRef.current,
4145
- worldX,
4146
- worldY,
4147
- { lineHitWorld: 10 / cam.zoom, ignoreLocked: true }
4148
- );
4149
- for (const id of toErase) {
4150
- eraserPreviewIdSetRef.current.add(id);
4151
- }
4152
- setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
4153
- return;
4154
- }
4155
- if (st.kind === "place") {
4156
- setPlacementPreview(
4157
- placementPreviewForTool(st.tool, st.startWorld, {
4158
- x: worldX,
4159
- y: worldY
4160
- })
4161
- );
4162
- return;
4163
- }
4725
+ applyDragMoveAtScreenPoint({ x: sx, y: sy }, { x: pageX, y: pageY });
4164
4726
  },
4165
4727
  onPanResponderRelease: (evt) => {
4166
4728
  lastPinchDist.current = null;
@@ -4204,6 +4766,10 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4204
4766
  dragStateRef.current = { kind: "idle" };
4205
4767
  return;
4206
4768
  }
4769
+ if (st.kind === "resize" || st.kind === "rotate") {
4770
+ dragStateRef.current = { kind: "idle" };
4771
+ return;
4772
+ }
4207
4773
  if (st.kind === "marquee") {
4208
4774
  dragStateRef.current = { kind: "idle" };
4209
4775
  setPlacementPreview(null);
@@ -4301,6 +4867,34 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4301
4867
  requestSelectToolAfterUse();
4302
4868
  return;
4303
4869
  }
4870
+ if (st.kind === "custom-place") {
4871
+ dragStateRef.current = { kind: "idle" };
4872
+ setPlacementPreview(null);
4873
+ const change = onItemsChangeRef.current;
4874
+ if (!change) return;
4875
+ const { worldX, worldY } = screenToWorld(
4876
+ evt.nativeEvent.locationX,
4877
+ evt.nativeEvent.locationY
4878
+ );
4879
+ const center = {
4880
+ x: (st.startWorld.x + worldX) / 2,
4881
+ y: (st.startWorld.y + worldY) / 2
4882
+ };
4883
+ const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
4884
+ const normalized = normalizeRect(raw);
4885
+ const bounds = normalized.width < MIN_PLACE_SIZE || normalized.height < MIN_PLACE_SIZE ? {
4886
+ x: center.x - 60,
4887
+ y: center.y - 40,
4888
+ width: 120,
4889
+ height: 80
4890
+ } : normalized;
4891
+ const id = createShapeId();
4892
+ const item = st.placement.createItem({ id, bounds });
4893
+ change([...itemsRef.current, item]);
4894
+ onSelectionChangeRef.current?.([id]);
4895
+ requestSelectToolAfterUse();
4896
+ return;
4897
+ }
4304
4898
  if (st.kind === "tap") {
4305
4899
  dragStateRef.current = { kind: "idle" };
4306
4900
  const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
@@ -4362,6 +4956,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4362
4956
  }
4363
4957
  }),
4364
4958
  [
4959
+ applyDragMoveAtScreenPoint,
4365
4960
  screenToWorld,
4366
4961
  requestRender,
4367
4962
  requestSelectToolAfterUse,
@@ -4392,7 +4987,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
4392
4987
  [requestRender, size]
4393
4988
  );
4394
4989
  const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
4395
- const activeToolCursor = nativeCursorForVectorToolId(toolId);
4990
+ const activeToolCursor = cursorForToolId(toolId);
4396
4991
  const toolCursor = activeToolCursor && toolCursorPoint ? { cursor: activeToolCursor, point: toolCursorPoint } : null;
4397
4992
  return /* @__PURE__ */ jsxRuntime.jsx(
4398
4993
  reactNative.View,