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.js CHANGED
@@ -940,6 +940,10 @@ function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
940
940
  const ly = sin * dx + cos * dy;
941
941
  return { x: c.x + lx, y: c.y + ly };
942
942
  }
943
+ function itemPivotWorld(item) {
944
+ const r = normalizeRect(item.bounds);
945
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
946
+ }
943
947
  function boundsAabbForRotatedItem(item) {
944
948
  const rot = getItemRotationRad(item);
945
949
  if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
@@ -970,6 +974,7 @@ function boundsAabbForRotatedItem(item) {
970
974
  }
971
975
 
972
976
  // src/interaction/resize-handles.ts
977
+ var HANDLE_IDS = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
973
978
  function getHandleWorldPosition(bounds, id) {
974
979
  const r = normalizeRect(bounds);
975
980
  const cx = r.x + r.width / 2;
@@ -993,6 +998,30 @@ function getHandleWorldPosition(bounds, id) {
993
998
  return { x: r.x, y: cy };
994
999
  }
995
1000
  }
1001
+ function hitTestResizeHandle(bounds, worldX, worldY, radiusWorld, rotationRad = 0) {
1002
+ const r = normalizeRect(bounds);
1003
+ const pl = worldToItemLocal(
1004
+ worldX,
1005
+ worldY,
1006
+ r.x,
1007
+ r.y,
1008
+ r.width,
1009
+ r.height,
1010
+ rotationRad
1011
+ );
1012
+ const localBounds2 = { x: 0, y: 0, width: r.width, height: r.height };
1013
+ let best = null;
1014
+ let bestD = radiusWorld;
1015
+ for (const id of HANDLE_IDS) {
1016
+ const p = getHandleWorldPosition(localBounds2, id);
1017
+ const d = Math.hypot(pl.x - p.x, pl.y - p.y);
1018
+ if (d <= bestD) {
1019
+ bestD = d;
1020
+ best = id;
1021
+ }
1022
+ }
1023
+ return best;
1024
+ }
996
1025
  function getHandleWorldPositionRotated(bounds, handle, rotationRad) {
997
1026
  const r = normalizeRect(bounds);
998
1027
  const p = getHandleWorldPosition(
@@ -1013,6 +1042,10 @@ function getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld)
1013
1042
  rotationRad
1014
1043
  );
1015
1044
  }
1045
+ function hitTestRotateHandle(bounds, rotationRad, worldX, worldY, radiusWorld, handleOffsetWorld) {
1046
+ const p = getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld);
1047
+ return Math.hypot(worldX - p.x, worldY - p.y) <= radiusWorld;
1048
+ }
1016
1049
  function rectFromCorners(a, b) {
1017
1050
  const minX = Math.min(a.x, b.x);
1018
1051
  const maxX = Math.max(a.x, b.x);
@@ -1020,6 +1053,155 @@ function rectFromCorners(a, b) {
1020
1053
  const maxY = Math.max(a.y, b.y);
1021
1054
  return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1022
1055
  }
1056
+ function clampMinSize(r, min) {
1057
+ const n = normalizeRect(r);
1058
+ return {
1059
+ x: n.x,
1060
+ y: n.y,
1061
+ width: Math.max(min, n.width),
1062
+ height: Math.max(min, n.height)
1063
+ };
1064
+ }
1065
+ function computeResizeBounds(bounds, handle, currentWorld) {
1066
+ const r = normalizeRect(bounds);
1067
+ const x0 = r.x;
1068
+ const y0 = r.y;
1069
+ const x1 = r.x + r.width;
1070
+ const y1 = r.y + r.height;
1071
+ const minSize = 8;
1072
+ switch (handle) {
1073
+ case "nw":
1074
+ return clampMinSize(rectFromCorners(currentWorld, { x: x1, y: y1 }), minSize);
1075
+ case "ne":
1076
+ return clampMinSize(rectFromCorners(currentWorld, { x: x0, y: y1 }), minSize);
1077
+ case "se":
1078
+ return clampMinSize(rectFromCorners(currentWorld, { x: x0, y: y0 }), minSize);
1079
+ case "sw":
1080
+ return clampMinSize(rectFromCorners(currentWorld, { x: x1, y: y0 }), minSize);
1081
+ case "n":
1082
+ return clampMinSize(
1083
+ rectFromCorners({ x: x0, y: currentWorld.y }, { x: x1, y: y1 }),
1084
+ minSize
1085
+ );
1086
+ case "s":
1087
+ return clampMinSize(
1088
+ rectFromCorners({ x: x0, y: y0 }, { x: x1, y: currentWorld.y }),
1089
+ minSize
1090
+ );
1091
+ case "e":
1092
+ return clampMinSize(
1093
+ rectFromCorners({ x: x0, y: y0 }, { x: currentWorld.x, y: y1 }),
1094
+ minSize
1095
+ );
1096
+ case "w":
1097
+ return clampMinSize(
1098
+ rectFromCorners({ x: currentWorld.x, y: y0 }, { x: x1, y: y1 }),
1099
+ minSize
1100
+ );
1101
+ default:
1102
+ return r;
1103
+ }
1104
+ }
1105
+ function computeResizeBoundsFixedAspect(bounds, handle, currentWorld, aspect) {
1106
+ const r = normalizeRect(bounds);
1107
+ const x0 = r.x;
1108
+ const y0 = r.y;
1109
+ const x1 = r.x + r.width;
1110
+ const y1 = r.y + r.height;
1111
+ const w = r.width;
1112
+ const h = r.height;
1113
+ const minSize = 8;
1114
+ const a = Math.max(1e-9, aspect);
1115
+ function cornerAspectRect(anchor, cursor) {
1116
+ const dx = cursor.x - anchor.x;
1117
+ const dy = cursor.y - anchor.y;
1118
+ const aw = Math.abs(dx);
1119
+ const ah = Math.abs(dy);
1120
+ let W;
1121
+ let H;
1122
+ if (aw < 1e-12 && ah < 1e-12) {
1123
+ W = minSize;
1124
+ H = minSize / a;
1125
+ } else if (ah < 1e-12) {
1126
+ W = Math.max(minSize, aw);
1127
+ H = W / a;
1128
+ } else if (aw < 1e-12) {
1129
+ H = Math.max(minSize, ah);
1130
+ W = H * a;
1131
+ } else if (aw / ah > a) {
1132
+ W = aw;
1133
+ H = aw / a;
1134
+ } else {
1135
+ H = ah;
1136
+ W = ah * a;
1137
+ }
1138
+ if (W < minSize || H < minSize) {
1139
+ const scale = Math.max(minSize / W, minSize / H);
1140
+ W *= scale;
1141
+ H *= scale;
1142
+ }
1143
+ const sx = Math.sign(dx) || 1;
1144
+ const sy = Math.sign(dy) || 1;
1145
+ return rectFromCorners(anchor, {
1146
+ x: anchor.x + sx * W,
1147
+ y: anchor.y + sy * H
1148
+ });
1149
+ }
1150
+ switch (handle) {
1151
+ case "se":
1152
+ return cornerAspectRect({ x: x0, y: y0 }, currentWorld);
1153
+ case "nw":
1154
+ return cornerAspectRect({ x: x1, y: y1 }, currentWorld);
1155
+ case "ne":
1156
+ return cornerAspectRect({ x: x0, y: y1 }, currentWorld);
1157
+ case "sw":
1158
+ return cornerAspectRect({ x: x1, y: y0 }, currentWorld);
1159
+ case "e": {
1160
+ const rawW = currentWorld.x - x0;
1161
+ const aw = Math.max(minSize, Math.abs(rawW));
1162
+ const H = aw / a;
1163
+ const newY = y0 + h / 2 - H / 2;
1164
+ const sign = Math.sign(rawW) || 1;
1165
+ return normalizeRect({ x: x0, y: newY, width: sign * aw, height: H });
1166
+ }
1167
+ case "w": {
1168
+ const rawW = x1 - currentWorld.x;
1169
+ const aw = Math.max(minSize, Math.abs(rawW));
1170
+ const H = aw / a;
1171
+ const newY = y0 + h / 2 - H / 2;
1172
+ const sign = Math.sign(rawW) || 1;
1173
+ return normalizeRect({
1174
+ x: x1 - sign * aw,
1175
+ y: newY,
1176
+ width: sign * aw,
1177
+ height: H
1178
+ });
1179
+ }
1180
+ case "n": {
1181
+ const rawH = y1 - currentWorld.y;
1182
+ const ah = Math.max(minSize, Math.abs(rawH));
1183
+ const W = ah * a;
1184
+ const newX = x0 + w / 2 - W / 2;
1185
+ const sign = Math.sign(rawH) || 1;
1186
+ return normalizeRect({
1187
+ x: newX,
1188
+ y: y1 - sign * ah,
1189
+ width: W,
1190
+ height: sign * ah
1191
+ });
1192
+ }
1193
+ case "s": {
1194
+ const rawH = currentWorld.y - y0;
1195
+ const ah = Math.max(minSize, Math.abs(rawH));
1196
+ const W = ah * a;
1197
+ const newX = x0 + w / 2 - W / 2;
1198
+ const sign = Math.sign(rawH) || 1;
1199
+ return normalizeRect({ x: newX, y: y0, width: W, height: sign * ah });
1200
+ }
1201
+ default:
1202
+ return r;
1203
+ }
1204
+ }
1023
1205
 
1024
1206
  // src/scene/freehand-path.ts
1025
1207
  function smoothFreehandPointsToPathD(points) {
@@ -3581,6 +3763,211 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
3581
3763
  return ids;
3582
3764
  }
3583
3765
 
3766
+ // src/interaction/mutations.ts
3767
+ function computeNewBoundsForResize(item, sb, handle, currentWorld) {
3768
+ const rot = getItemRotationRad(item);
3769
+ if (Math.abs(rot) < 1e-12) {
3770
+ if (item.toolKind === "image") {
3771
+ let aspect;
3772
+ if (item.imageIntrinsicSize) {
3773
+ const iw = Math.max(1e-9, item.imageIntrinsicSize.width);
3774
+ const ih = Math.max(1e-9, item.imageIntrinsicSize.height);
3775
+ aspect = iw / ih;
3776
+ } else if (item.imageVectorLocalSize) {
3777
+ const lw = Math.max(1e-9, item.imageVectorLocalSize.width);
3778
+ const lh = Math.max(1e-9, item.imageVectorLocalSize.height);
3779
+ aspect = lw / lh;
3780
+ }
3781
+ return aspect !== void 0 ? computeResizeBoundsFixedAspect(sb, handle, currentWorld, aspect) : computeResizeBounds(sb, handle, currentWorld);
3782
+ }
3783
+ return computeResizeBounds(sb, handle, currentWorld);
3784
+ }
3785
+ const local = worldToItemLocal(
3786
+ currentWorld.x,
3787
+ currentWorld.y,
3788
+ sb.x,
3789
+ sb.y,
3790
+ sb.width,
3791
+ sb.height,
3792
+ rot
3793
+ );
3794
+ const localBounds2 = { x: 0, y: 0, width: sb.width, height: sb.height };
3795
+ let nbLocal;
3796
+ if (item.toolKind === "image") {
3797
+ let aspect;
3798
+ if (item.imageIntrinsicSize) {
3799
+ const iw = Math.max(1e-9, item.imageIntrinsicSize.width);
3800
+ const ih = Math.max(1e-9, item.imageIntrinsicSize.height);
3801
+ aspect = iw / ih;
3802
+ } else if (item.imageVectorLocalSize) {
3803
+ const lw = Math.max(1e-9, item.imageVectorLocalSize.width);
3804
+ const lh = Math.max(1e-9, item.imageVectorLocalSize.height);
3805
+ aspect = lw / lh;
3806
+ }
3807
+ nbLocal = aspect !== void 0 ? computeResizeBoundsFixedAspect(localBounds2, handle, local, aspect) : computeResizeBounds(localBounds2, handle, local);
3808
+ } else {
3809
+ nbLocal = computeResizeBounds(localBounds2, handle, local);
3810
+ }
3811
+ return {
3812
+ x: sb.x + nbLocal.x,
3813
+ y: sb.y + nbLocal.y,
3814
+ width: nbLocal.width,
3815
+ height: nbLocal.height
3816
+ };
3817
+ }
3818
+ function applyRotationFromPointer(item, startRotation, startPointerAngleRad, pointerAngleRad) {
3819
+ let delta = pointerAngleRad - startPointerAngleRad;
3820
+ while (delta > Math.PI) {
3821
+ delta -= 2 * Math.PI;
3822
+ }
3823
+ while (delta < -Math.PI) {
3824
+ delta += 2 * Math.PI;
3825
+ }
3826
+ return { ...item, rotation: startRotation + delta };
3827
+ }
3828
+ function resizeItemByHandle(item, start, handle, currentWorld) {
3829
+ const sb = normalizeRect(start.bounds);
3830
+ const newBoundsRaw = computeNewBoundsForResize(item, sb, handle, currentWorld);
3831
+ const nb = normalizeRect(newBoundsRaw);
3832
+ const k = item.toolKind;
3833
+ if (k === "rect") {
3834
+ const style = resolveStrokeStyle(item);
3835
+ return {
3836
+ ...item,
3837
+ x: nb.x,
3838
+ y: nb.y,
3839
+ bounds: nb,
3840
+ childrenSvg: buildRectSvg(nb.width, nb.height, style)
3841
+ };
3842
+ }
3843
+ if (k === "ellipse") {
3844
+ const style = resolveStrokeStyle(item);
3845
+ return {
3846
+ ...item,
3847
+ x: nb.x,
3848
+ y: nb.y,
3849
+ bounds: nb,
3850
+ childrenSvg: buildEllipseSvg(nb.width, nb.height, style)
3851
+ };
3852
+ }
3853
+ if (k === "architectural-cloud") {
3854
+ const style = resolveStrokeStyle(item);
3855
+ return {
3856
+ ...item,
3857
+ x: nb.x,
3858
+ y: nb.y,
3859
+ bounds: nb,
3860
+ childrenSvg: buildArchitecturalCloudSvg(nb.width, nb.height, style)
3861
+ };
3862
+ }
3863
+ if (k === "text" && item.text !== void 0) {
3864
+ const sfw = Math.max(sb.width, 1e-6);
3865
+ const sfh = Math.max(sb.height, 1e-6);
3866
+ const baseFs = item.textFontSize ?? DEFAULT_TEXT_FONT_SIZE;
3867
+ const areaRatio = nb.width * nb.height / (sfw * sfh);
3868
+ const scale = Math.sqrt(Math.max(areaRatio, 1e-12));
3869
+ const nextFs = Math.min(256, Math.max(6, baseFs * scale));
3870
+ return rebuildItemSvg({
3871
+ ...item,
3872
+ x: nb.x,
3873
+ y: nb.y,
3874
+ bounds: nb,
3875
+ textFixedBounds: true,
3876
+ textFontSize: nextFs
3877
+ });
3878
+ }
3879
+ if (k === "image") {
3880
+ if (item.imageRasterHref && item.imageIntrinsicSize) {
3881
+ return {
3882
+ ...item,
3883
+ x: nb.x,
3884
+ y: nb.y,
3885
+ bounds: nb,
3886
+ childrenSvg: buildRasterImageChildrenSvg(
3887
+ item.imageRasterHref,
3888
+ item.imageIntrinsicSize,
3889
+ nb
3890
+ )
3891
+ };
3892
+ }
3893
+ if (item.imageVectorInnerSvg && item.imageVectorLocalSize) {
3894
+ const lw = Math.max(1e-6, item.imageVectorLocalSize.width);
3895
+ const lh = Math.max(1e-6, item.imageVectorLocalSize.height);
3896
+ const arB = nb.width / Math.max(1e-9, nb.height);
3897
+ const arI = lw / lh;
3898
+ let childrenSvg;
3899
+ if (Math.abs(arB - arI) < 1e-3) {
3900
+ const s = nb.width / lw;
3901
+ childrenSvg = `<g transform="scale(${s})">${item.imageVectorInnerSvg}</g>`;
3902
+ } else {
3903
+ const s = Math.min(nb.width / lw, nb.height / lh);
3904
+ const tx = (nb.width - lw * s) / 2;
3905
+ const ty = (nb.height - lh * s) / 2;
3906
+ childrenSvg = `<g transform="translate(${tx}, ${ty}) scale(${s})">${item.imageVectorInnerSvg}</g>`;
3907
+ }
3908
+ return {
3909
+ ...item,
3910
+ x: nb.x,
3911
+ y: nb.y,
3912
+ bounds: nb,
3913
+ childrenSvg
3914
+ };
3915
+ }
3916
+ }
3917
+ if ((k === "line" || k === "arrow") && start.line) {
3918
+ const sfw = Math.max(sb.width, 1e-6);
3919
+ const sfh = Math.max(sb.height, 1e-6);
3920
+ const f = start.line;
3921
+ const u1 = f.x1 / sfw;
3922
+ const v1 = f.y1 / sfh;
3923
+ const u2 = f.x2 / sfw;
3924
+ const v2 = f.y2 / sfh;
3925
+ const newLine = {
3926
+ x1: u1 * nb.width,
3927
+ y1: v1 * nb.height,
3928
+ x2: u2 * nb.width,
3929
+ y2: v2 * nb.height
3930
+ };
3931
+ const style = resolveStrokeStyle(item);
3932
+ const childrenSvg = k === "arrow" ? buildArrowSvg(item.id, newLine, style) : buildLineSvg(newLine, style);
3933
+ return {
3934
+ ...item,
3935
+ x: nb.x,
3936
+ y: nb.y,
3937
+ bounds: nb,
3938
+ line: newLine,
3939
+ childrenSvg
3940
+ };
3941
+ }
3942
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item.pathPointsLocal && item.pathPointsLocal.length > 0) {
3943
+ const sfw = Math.max(sb.width, 1e-6);
3944
+ const sfh = Math.max(sb.height, 1e-6);
3945
+ const sx = nb.width / sfw;
3946
+ const sy = nb.height / sfh;
3947
+ const scaledPoints = item.pathPointsLocal.map((p) => ({
3948
+ x: p.x * sx,
3949
+ y: p.y * sy,
3950
+ ...p.pressure != null ? { pressure: p.pressure } : {}
3951
+ }));
3952
+ return rebuildItemSvg({
3953
+ ...item,
3954
+ x: nb.x,
3955
+ y: nb.y,
3956
+ bounds: nb,
3957
+ pathPointsLocal: scaledPoints
3958
+ });
3959
+ }
3960
+ if (k === "custom" && item.customIntrinsicSize && item.customInnerSvg) {
3961
+ return rebuildItemSvg({
3962
+ ...item,
3963
+ x: nb.x,
3964
+ y: nb.y,
3965
+ bounds: nb
3966
+ });
3967
+ }
3968
+ return { ...item, x: nb.x, y: nb.y, bounds: nb };
3969
+ }
3970
+
3584
3971
  // src/native/native-tool-cursors.ts
3585
3972
  var ICON_SIZE = 24;
3586
3973
  var CENTER_HOTSPOT = { x: 12, y: 12 };
@@ -3606,6 +3993,72 @@ function nativeCursorForVectorToolId(toolId) {
3606
3993
  return null;
3607
3994
  }
3608
3995
  }
3996
+ function nativeCrosshairToolCursor() {
3997
+ return { kind: "crosshair", size: ICON_SIZE, hotspot: CENTER_HOTSPOT };
3998
+ }
3999
+ function nativeFallbackToolCursorPoint(size) {
4000
+ if (size.width <= 0 || size.height <= 0) return null;
4001
+ return { x: size.width / 2, y: size.height / 2 };
4002
+ }
4003
+
4004
+ // src/native/native-vector-interactions.ts
4005
+ function supportsNativeResizeHandles(item) {
4006
+ const k = item?.toolKind;
4007
+ if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
4008
+ return true;
4009
+ }
4010
+ if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
4011
+ return true;
4012
+ }
4013
+ return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
4014
+ }
4015
+ function hitTestNativeSelectionHandle({
4016
+ selectedItem,
4017
+ selectedCount,
4018
+ worldPoint,
4019
+ zoom
4020
+ }) {
4021
+ if (selectedCount !== 1) return null;
4022
+ if (!selectedItem || selectedItem.locked) return null;
4023
+ if (!supportsNativeResizeHandles(selectedItem)) return null;
4024
+ const bounds = normalizeRect(selectedItem.bounds);
4025
+ const rotation = selectedItem.rotation ?? 0;
4026
+ const handleRadiusWorld = 6 / Math.max(zoom, 1e-9);
4027
+ const rotateOffsetWorld = 24 / Math.max(zoom, 1e-9);
4028
+ if (hitTestRotateHandle(
4029
+ bounds,
4030
+ rotation,
4031
+ worldPoint.x,
4032
+ worldPoint.y,
4033
+ handleRadiusWorld,
4034
+ rotateOffsetWorld
4035
+ )) {
4036
+ return { kind: "rotate" };
4037
+ }
4038
+ const handle = hitTestResizeHandle(
4039
+ bounds,
4040
+ worldPoint.x,
4041
+ worldPoint.y,
4042
+ handleRadiusWorld,
4043
+ rotation
4044
+ );
4045
+ return handle ? { kind: "resize", handle } : null;
4046
+ }
4047
+ function resolveNativeCustomPlacement(toolId, customPlacement, customPlacements) {
4048
+ if (customPlacement?.toolId === toolId) return customPlacement;
4049
+ return customPlacements?.find((placement) => placement.toolId === toolId) ?? null;
4050
+ }
4051
+ function nativeRotationDragStart(input) {
4052
+ const pivotWorld = itemPivotWorld(input.item);
4053
+ return {
4054
+ pivotWorld,
4055
+ startPointerAngleRad: Math.atan2(
4056
+ input.worldPoint.y - pivotWorld.y,
4057
+ input.worldPoint.x - pivotWorld.x
4058
+ ),
4059
+ startRotation: input.item.rotation ?? 0
4060
+ };
4061
+ }
3609
4062
  var MIN_PLACE_SIZE = 8;
3610
4063
  var MIN_ARROW_DRAG_PX = 8;
3611
4064
  var TAP_PX = 20;
@@ -3620,16 +4073,6 @@ function isPlacementTool(toolId) {
3620
4073
  function isDefaultMarkerToolStyle(style) {
3621
4074
  return style.stroke === MARKER_TOOL_STYLE.stroke && style.strokeWidth === MARKER_TOOL_STYLE.strokeWidth && style.strokeOpacity === MARKER_TOOL_STYLE.strokeOpacity;
3622
4075
  }
3623
- function supportsNativeResizeHandles(item) {
3624
- const k = item?.toolKind;
3625
- if (k === "rect" || k === "ellipse" || k === "architectural-cloud" || k === "line" || k === "arrow" || k === "image" || k === "text") {
3626
- return true;
3627
- }
3628
- if ((k === "draw" || k === "pencil" || k === "brush" || k === "marker") && item?.pathPointsLocal && item.pathPointsLocal.length > 0) {
3629
- return true;
3630
- }
3631
- return k === "custom" && !!item?.customIntrinsicSize && !!item?.customInnerSvg;
3632
- }
3633
4076
  function placementPreviewForTool(toolId, start, end) {
3634
4077
  if (toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud") {
3635
4078
  return { kind: toolId, rect: rectFromCorners(start, end) };
@@ -3697,7 +4140,11 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3697
4140
  onSelectionChange,
3698
4141
  onItemsChange,
3699
4142
  onToolChangeRequest,
4143
+ onWorldPointerDown,
3700
4144
  onCameraChange,
4145
+ customPlacement,
4146
+ customPlacements = [],
4147
+ customCrosshairToolIds = [],
3701
4148
  toolbar,
3702
4149
  showStyleInspector = false,
3703
4150
  styleInspectorPlacement = "bottom"
@@ -3710,10 +4157,18 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3710
4157
  toolLockedRef.current = toolLocked;
3711
4158
  const onToolChangeRequestRef = useRef(onToolChangeRequest);
3712
4159
  onToolChangeRequestRef.current = onToolChangeRequest;
4160
+ const onWorldPointerDownRef = useRef(onWorldPointerDown);
4161
+ onWorldPointerDownRef.current = onWorldPointerDown;
3713
4162
  const onCameraChangeRef = useRef(onCameraChange);
3714
4163
  onCameraChangeRef.current = onCameraChange;
3715
4164
  const onItemsChangeRef = useRef(onItemsChange);
3716
4165
  onItemsChangeRef.current = onItemsChange;
4166
+ const customPlacementRef = useRef(customPlacement);
4167
+ customPlacementRef.current = customPlacement;
4168
+ const customPlacementsRef = useRef(customPlacements);
4169
+ customPlacementsRef.current = customPlacements;
4170
+ const customCrosshairToolIdsRef = useRef(customCrosshairToolIds);
4171
+ customCrosshairToolIdsRef.current = customCrosshairToolIds;
3717
4172
  const onSelectionChangeRef = useRef(onSelectionChange);
3718
4173
  onSelectionChangeRef.current = onSelectionChange;
3719
4174
  const itemsRef = useRef(items);
@@ -3821,21 +4276,60 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3821
4276
  setCameraTick((n) => n + 1);
3822
4277
  onCameraChangeRef.current?.();
3823
4278
  }, []);
3824
- const onLayout = useCallback((e) => {
3825
- const { width, height } = e.nativeEvent.layout;
3826
- setSize({ width, height });
4279
+ const cursorForToolId = useCallback((nextToolId) => {
4280
+ const builtInCursor = nativeCursorForVectorToolId(nextToolId);
4281
+ if (builtInCursor) return builtInCursor;
4282
+ return customCrosshairToolIdsRef.current.includes(nextToolId) ? nativeCrosshairToolCursor() : null;
3827
4283
  }, []);
4284
+ const onLayout = useCallback(
4285
+ (e) => {
4286
+ const { width, height } = e.nativeEvent.layout;
4287
+ const nextSize = { width, height };
4288
+ setSize(nextSize);
4289
+ if (!interactive) {
4290
+ setToolCursorPoint(null);
4291
+ return;
4292
+ }
4293
+ if (!cursorForToolId(toolIdRef.current)) {
4294
+ setToolCursorPoint(null);
4295
+ return;
4296
+ }
4297
+ setToolCursorPoint(
4298
+ (current) => current ?? nativeFallbackToolCursorPoint(nextSize)
4299
+ );
4300
+ },
4301
+ [cursorForToolId, interactive]
4302
+ );
3828
4303
  const updateToolCursorPoint = useCallback(
3829
4304
  (point) => {
3830
4305
  if (!interactive) return;
3831
- if (!nativeCursorForVectorToolId(toolIdRef.current)) return;
4306
+ if (!cursorForToolId(toolIdRef.current)) return;
3832
4307
  setToolCursorPoint(point);
3833
4308
  },
3834
- [interactive]
4309
+ [cursorForToolId, interactive]
3835
4310
  );
3836
4311
  const hideToolCursor = useCallback(() => {
3837
4312
  setToolCursorPoint(null);
3838
4313
  }, []);
4314
+ const showFallbackToolCursor = useCallback(
4315
+ (nextToolId) => {
4316
+ if (!interactive) {
4317
+ setToolCursorPoint(null);
4318
+ return;
4319
+ }
4320
+ if (!cursorForToolId(nextToolId)) {
4321
+ setToolCursorPoint(null);
4322
+ return;
4323
+ }
4324
+ setToolCursorPoint(
4325
+ (current) => current ?? nativeFallbackToolCursorPoint(size)
4326
+ );
4327
+ },
4328
+ [cursorForToolId, interactive, size]
4329
+ );
4330
+ useEffect(() => {
4331
+ showFallbackToolCursor(toolId);
4332
+ }, [showFallbackToolCursor, toolId]);
3839
4333
  const handlePointerMove = useCallback(
3840
4334
  (event) => {
3841
4335
  updateToolCursorPoint(screenPointFromPointerEvent(event));
@@ -3883,6 +4377,40 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3883
4377
  return;
3884
4378
  }
3885
4379
  if (tool === "select") {
4380
+ const currentSelectedIds = selectedIdsRef.current;
4381
+ const selectedItem = currentSelectedIds.length === 1 ? itemsRef.current.find((item) => item.id === currentSelectedIds[0]) : void 0;
4382
+ const selectionHandle = hitTestNativeSelectionHandle({
4383
+ selectedItem,
4384
+ selectedCount: currentSelectedIds.length,
4385
+ worldPoint: { x: worldX, y: worldY },
4386
+ zoom: cam.zoom
4387
+ });
4388
+ if (selectionHandle && selectedItem) {
4389
+ if (selectionHandle.kind === "rotate") {
4390
+ const rotationStart = nativeRotationDragStart({
4391
+ item: selectedItem,
4392
+ worldPoint: { x: worldX, y: worldY }
4393
+ });
4394
+ dragStateRef.current = {
4395
+ kind: "rotate",
4396
+ id: selectedItem.id,
4397
+ snapshot: selectedItem,
4398
+ ...rotationStart
4399
+ };
4400
+ return;
4401
+ }
4402
+ dragStateRef.current = {
4403
+ kind: "resize",
4404
+ id: selectedItem.id,
4405
+ handle: selectionHandle.handle,
4406
+ snapshot: selectedItem,
4407
+ start: {
4408
+ bounds: selectedItem.bounds,
4409
+ line: selectedItem.line
4410
+ }
4411
+ };
4412
+ return;
4413
+ }
3886
4414
  const hit = hitTestWorldPoint(itemsRef.current, worldX, worldY, {
3887
4415
  lineHitWorld: 10 / cam.zoom,
3888
4416
  ignoreLocked: true
@@ -3975,6 +4503,25 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3975
4503
  );
3976
4504
  return;
3977
4505
  }
4506
+ const customPlacement2 = resolveNativeCustomPlacement(
4507
+ tool,
4508
+ customPlacementRef.current,
4509
+ customPlacementsRef.current
4510
+ );
4511
+ if (customPlacement2) {
4512
+ dragStateRef.current = {
4513
+ kind: "custom-place",
4514
+ tool,
4515
+ placement: customPlacement2,
4516
+ startWorld: { x: worldX, y: worldY },
4517
+ startScreen: { x: sx, y: sy }
4518
+ };
4519
+ setPlacementPreview({
4520
+ kind: "rect",
4521
+ rect: { x: worldX, y: worldY, width: 0, height: 0 }
4522
+ });
4523
+ return;
4524
+ }
3978
4525
  if (tool === "note" || tool === "text") {
3979
4526
  dragStateRef.current = {
3980
4527
  kind: "tap",
@@ -3984,6 +4531,18 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
3984
4531
  };
3985
4532
  return;
3986
4533
  }
4534
+ const handleWorldPointerDown = onWorldPointerDownRef.current;
4535
+ if (handleWorldPointerDown) {
4536
+ handleWorldPointerDown({
4537
+ toolId: tool,
4538
+ worldX,
4539
+ worldY,
4540
+ screenX: sx,
4541
+ screenY: sy
4542
+ });
4543
+ requestSelectToolAfterUse();
4544
+ return;
4545
+ }
3987
4546
  dragStateRef.current = { kind: "pan" };
3988
4547
  },
3989
4548
  onPanResponderMove: (evt) => {
@@ -4079,6 +4638,36 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4079
4638
  change(nextList);
4080
4639
  return;
4081
4640
  }
4641
+ if (st.kind === "rotate") {
4642
+ const change = onItemsChangeRef.current;
4643
+ if (!change) return;
4644
+ const angle = Math.atan2(
4645
+ worldY - st.pivotWorld.y,
4646
+ worldX - st.pivotWorld.x
4647
+ );
4648
+ const next = applyRotationFromPointer(
4649
+ st.snapshot,
4650
+ st.startRotation,
4651
+ st.startPointerAngleRad,
4652
+ angle
4653
+ );
4654
+ change(
4655
+ itemsRef.current.map((item) => item.id === st.id ? next : item)
4656
+ );
4657
+ return;
4658
+ }
4659
+ if (st.kind === "resize") {
4660
+ const change = onItemsChangeRef.current;
4661
+ if (!change) return;
4662
+ const next = resizeItemByHandle(st.snapshot, st.start, st.handle, {
4663
+ x: worldX,
4664
+ y: worldY
4665
+ });
4666
+ change(
4667
+ itemsRef.current.map((item) => item.id === st.id ? next : item)
4668
+ );
4669
+ return;
4670
+ }
4082
4671
  if (st.kind === "marquee") {
4083
4672
  const a = st.startWorld;
4084
4673
  const b = { x: worldX, y: worldY };
@@ -4117,11 +4706,21 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4117
4706
  );
4118
4707
  return;
4119
4708
  }
4709
+ if (st.kind === "custom-place") {
4710
+ setPlacementPreview({
4711
+ kind: "rect",
4712
+ rect: rectFromCorners(st.startWorld, { x: worldX, y: worldY })
4713
+ });
4714
+ return;
4715
+ }
4120
4716
  },
4121
4717
  onPanResponderRelease: (evt) => {
4122
4718
  lastPinchDist.current = null;
4123
4719
  lastPanPoint.current = null;
4124
- hideToolCursor();
4720
+ updateToolCursorPoint({
4721
+ x: evt.nativeEvent.locationX,
4722
+ y: evt.nativeEvent.locationY
4723
+ });
4125
4724
  const st = dragStateRef.current;
4126
4725
  if (st.kind === "draw") {
4127
4726
  dragStateRef.current = { kind: "idle" };
@@ -4157,6 +4756,10 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4157
4756
  dragStateRef.current = { kind: "idle" };
4158
4757
  return;
4159
4758
  }
4759
+ if (st.kind === "resize" || st.kind === "rotate") {
4760
+ dragStateRef.current = { kind: "idle" };
4761
+ return;
4762
+ }
4160
4763
  if (st.kind === "marquee") {
4161
4764
  dragStateRef.current = { kind: "idle" };
4162
4765
  setPlacementPreview(null);
@@ -4254,6 +4857,34 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4254
4857
  requestSelectToolAfterUse();
4255
4858
  return;
4256
4859
  }
4860
+ if (st.kind === "custom-place") {
4861
+ dragStateRef.current = { kind: "idle" };
4862
+ setPlacementPreview(null);
4863
+ const change = onItemsChangeRef.current;
4864
+ if (!change) return;
4865
+ const { worldX, worldY } = screenToWorld(
4866
+ evt.nativeEvent.locationX,
4867
+ evt.nativeEvent.locationY
4868
+ );
4869
+ const center = {
4870
+ x: (st.startWorld.x + worldX) / 2,
4871
+ y: (st.startWorld.y + worldY) / 2
4872
+ };
4873
+ const raw = rectFromCorners(st.startWorld, { x: worldX, y: worldY });
4874
+ const normalized = normalizeRect(raw);
4875
+ const bounds = normalized.width < MIN_PLACE_SIZE || normalized.height < MIN_PLACE_SIZE ? {
4876
+ x: center.x - 60,
4877
+ y: center.y - 40,
4878
+ width: 120,
4879
+ height: 80
4880
+ } : normalized;
4881
+ const id = createShapeId();
4882
+ const item = st.placement.createItem({ id, bounds });
4883
+ change([...itemsRef.current, item]);
4884
+ onSelectionChangeRef.current?.([id]);
4885
+ requestSelectToolAfterUse();
4886
+ return;
4887
+ }
4257
4888
  if (st.kind === "tap") {
4258
4889
  dragStateRef.current = { kind: "idle" };
4259
4890
  const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
@@ -4345,7 +4976,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
4345
4976
  [requestRender, size]
4346
4977
  );
4347
4978
  const activeStyleToolId = toolId === "draw" || toolId === "marker" ? toolId : null;
4348
- const activeToolCursor = nativeCursorForVectorToolId(toolId);
4979
+ const activeToolCursor = cursorForToolId(toolId);
4349
4980
  const toolCursor = activeToolCursor && toolCursorPoint ? { cursor: activeToolCursor, point: toolCursorPoint } : null;
4350
4981
  return /* @__PURE__ */ jsx(
4351
4982
  View,