canvu-react 0.4.4 → 0.4.6

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
@@ -1,10 +1,10 @@
1
+ import getStroke from 'perfect-freehand';
1
2
  import { Group, RoundedRect, Circle, Line, vec, Path, matchFont, Text, Canvas, Rect, Oval } from '@shopify/react-native-skia';
2
3
  import { memo, forwardRef, useState, useRef, useCallback, useMemo, useImperativeHandle } from 'react';
3
- import getStroke from 'perfect-freehand';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
- import { PanResponder, View } from 'react-native';
5
+ import { StyleSheet, PanResponder, View, ScrollView, Text as Text$1, Pressable } from 'react-native';
6
6
 
7
- // src/native/NativeInteractionOverlay.tsx
7
+ // src/scene/shape-builders.ts
8
8
 
9
9
  // src/math/rect.ts
10
10
  function rectsIntersect(a, b) {
@@ -21,141 +21,6 @@ function normalizeRect(r) {
21
21
  };
22
22
  }
23
23
 
24
- // src/math/item-transform.ts
25
- function getItemRotationRad(item) {
26
- return item.rotation ?? 0;
27
- }
28
- function itemLocalToWorld(lx, ly, itemX, itemY, w, h, rotationRad) {
29
- const c = { x: w / 2, y: h / 2 };
30
- const dlx = lx - c.x;
31
- const dly = ly - c.y;
32
- const cos = Math.cos(rotationRad);
33
- const sin = Math.sin(rotationRad);
34
- return {
35
- x: itemX + c.x + cos * dlx - sin * dly,
36
- y: itemY + c.y + sin * dlx + cos * dly
37
- };
38
- }
39
- function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
40
- const c = { x: w / 2, y: h / 2 };
41
- const vx = wx - itemX;
42
- const vy = wy - itemY;
43
- const dx = vx - c.x;
44
- const dy = vy - c.y;
45
- const cos = Math.cos(-rotationRad);
46
- const sin = Math.sin(-rotationRad);
47
- const lx = cos * dx - sin * dy;
48
- const ly = sin * dx + cos * dy;
49
- return { x: c.x + lx, y: c.y + ly };
50
- }
51
- function boundsAabbForRotatedItem(item) {
52
- const rot = getItemRotationRad(item);
53
- if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
54
- return item.bounds;
55
- }
56
- const r = normalizeRect(item.bounds);
57
- if (Math.abs(rot) < 1e-12) {
58
- return r;
59
- }
60
- const corners = [
61
- [0, 0],
62
- [r.width, 0],
63
- [r.width, r.height],
64
- [0, r.height]
65
- ];
66
- let minX = Infinity;
67
- let minY = Infinity;
68
- let maxX = -Infinity;
69
- let maxY = -Infinity;
70
- for (const [lx, ly] of corners) {
71
- const p = itemLocalToWorld(lx, ly, item.x, item.y, r.width, r.height, rot);
72
- minX = Math.min(minX, p.x);
73
- minY = Math.min(minY, p.y);
74
- maxX = Math.max(maxX, p.x);
75
- maxY = Math.max(maxY, p.y);
76
- }
77
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
78
- }
79
-
80
- // src/interaction/resize-handles.ts
81
- function getHandleWorldPosition(bounds, id) {
82
- const r = normalizeRect(bounds);
83
- const cx = r.x + r.width / 2;
84
- const cy = r.y + r.height / 2;
85
- switch (id) {
86
- case "nw":
87
- return { x: r.x, y: r.y };
88
- case "n":
89
- return { x: cx, y: r.y };
90
- case "ne":
91
- return { x: r.x + r.width, y: r.y };
92
- case "e":
93
- return { x: r.x + r.width, y: cy };
94
- case "se":
95
- return { x: r.x + r.width, y: r.y + r.height };
96
- case "s":
97
- return { x: cx, y: r.y + r.height };
98
- case "sw":
99
- return { x: r.x, y: r.y + r.height };
100
- case "w":
101
- return { x: r.x, y: cy };
102
- }
103
- }
104
- function getHandleWorldPositionRotated(bounds, handle, rotationRad) {
105
- const r = normalizeRect(bounds);
106
- const p = getHandleWorldPosition(
107
- { x: 0, y: 0, width: r.width, height: r.height },
108
- handle
109
- );
110
- return itemLocalToWorld(p.x, p.y, r.x, r.y, r.width, r.height, rotationRad);
111
- }
112
- function getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld) {
113
- const r = normalizeRect(bounds);
114
- return itemLocalToWorld(
115
- r.width / 2,
116
- -handleOffsetWorld,
117
- r.x,
118
- r.y,
119
- r.width,
120
- r.height,
121
- rotationRad
122
- );
123
- }
124
-
125
- // src/scene/freehand-path.ts
126
- function smoothFreehandPointsToPathD(points) {
127
- const n = points.length;
128
- if (n === 0) return "";
129
- if (n === 1) {
130
- const p = points[0];
131
- if (!p) return "";
132
- return `M ${p.x} ${p.y}`;
133
- }
134
- if (n === 2) {
135
- const a = points[0];
136
- const b = points[1];
137
- if (!a || !b) return "";
138
- return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
139
- }
140
- const p0 = points[0];
141
- if (!p0) return "";
142
- let d = `M ${p0.x} ${p0.y}`;
143
- let i = 1;
144
- for (; i < n - 2; i++) {
145
- const pi = points[i];
146
- const pi1 = points[i + 1];
147
- if (!pi || !pi1) continue;
148
- const xc = (pi.x + pi1.x) / 2;
149
- const yc = (pi.y + pi1.y) / 2;
150
- d += ` Q ${pi.x} ${pi.y} ${xc} ${yc}`;
151
- }
152
- const pLast = points[i];
153
- const pEnd = points[i + 1];
154
- if (!pLast || !pEnd) return d;
155
- d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
156
- return d;
157
- }
158
-
159
24
  // src/scene/custom-shape.ts
160
25
  function buildCustomShapeChildrenSvg(inner, intrinsic, bounds) {
161
26
  const b = normalizeRect(bounds);
@@ -780,6 +645,15 @@ function createShapeId() {
780
645
  const uid = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : String(Date.now());
781
646
  return `user-shape-${uid}`;
782
647
  }
648
+ function lineEndpointsToLocal(bounds, worldEndA, worldEndB) {
649
+ const b = normalizeRect(bounds);
650
+ return {
651
+ x1: worldEndA.x - b.x,
652
+ y1: worldEndA.y - b.y,
653
+ x2: worldEndB.x - b.x,
654
+ y2: worldEndB.y - b.y
655
+ };
656
+ }
783
657
  function rebuildItemSvg(item) {
784
658
  const style = resolveStrokeStyle(item);
785
659
  const k = item.toolKind;
@@ -883,6 +757,85 @@ function rebuildItemSvg(item) {
883
757
  }
884
758
  return item;
885
759
  }
760
+ function createRectangleItem(id, bounds, style) {
761
+ const r = normalizeRect(bounds);
762
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
763
+ return rebuildItemSvg({
764
+ id,
765
+ x: r.x,
766
+ y: r.y,
767
+ bounds: { ...r },
768
+ toolKind: "rect",
769
+ stroke: s.stroke,
770
+ strokeWidth: s.strokeWidth,
771
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
772
+ childrenSvg: ""
773
+ });
774
+ }
775
+ function createEllipseItem(id, bounds, style) {
776
+ const r = normalizeRect(bounds);
777
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
778
+ return rebuildItemSvg({
779
+ id,
780
+ x: r.x,
781
+ y: r.y,
782
+ bounds: { ...r },
783
+ toolKind: "ellipse",
784
+ stroke: s.stroke,
785
+ strokeWidth: s.strokeWidth,
786
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
787
+ childrenSvg: ""
788
+ });
789
+ }
790
+ function createArchitecturalCloudItem(id, bounds, style) {
791
+ const r = normalizeRect(bounds);
792
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
793
+ return rebuildItemSvg({
794
+ id,
795
+ x: r.x,
796
+ y: r.y,
797
+ bounds: { ...r },
798
+ toolKind: "architectural-cloud",
799
+ stroke: s.stroke,
800
+ strokeWidth: s.strokeWidth,
801
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
802
+ childrenSvg: ""
803
+ });
804
+ }
805
+ function createLineItem(id, bounds, line, toolKind, style, arrowBind) {
806
+ const r = normalizeRect(bounds);
807
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
808
+ return rebuildItemSvg({
809
+ id,
810
+ x: r.x,
811
+ y: r.y,
812
+ bounds: { ...r },
813
+ toolKind,
814
+ line: { ...line },
815
+ stroke: s.stroke,
816
+ strokeWidth: s.strokeWidth,
817
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
818
+ ...{},
819
+ childrenSvg: ""
820
+ });
821
+ }
822
+ function createTextItem(id, bounds, text = "", style, textFontSize) {
823
+ const r = normalizeRect(bounds);
824
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
825
+ return rebuildItemSvg({
826
+ id,
827
+ x: r.x,
828
+ y: r.y,
829
+ bounds: { ...r },
830
+ toolKind: "text",
831
+ text,
832
+ stroke: s.stroke,
833
+ strokeWidth: s.strokeWidth,
834
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
835
+ ...{ textFontSize } ,
836
+ childrenSvg: ""
837
+ });
838
+ }
886
839
  function createFreehandStrokeItem(id, pointsWorld, toolKind, style) {
887
840
  if (pointsWorld.length === 0) return null;
888
841
  const merged = {
@@ -941,6 +894,148 @@ function buildRasterImageChildrenSvg(dataUrl, intrinsic, bounds) {
941
894
  return `<g transform="translate(${tx}, ${ty}) scale(${s})"><image href="${href}" x="0" y="0" width="${iw}" height="${ih}" /></g>`;
942
895
  }
943
896
 
897
+ // src/math/item-transform.ts
898
+ function getItemRotationRad(item) {
899
+ return item.rotation ?? 0;
900
+ }
901
+ function itemLocalToWorld(lx, ly, itemX, itemY, w, h, rotationRad) {
902
+ const c = { x: w / 2, y: h / 2 };
903
+ const dlx = lx - c.x;
904
+ const dly = ly - c.y;
905
+ const cos = Math.cos(rotationRad);
906
+ const sin = Math.sin(rotationRad);
907
+ return {
908
+ x: itemX + c.x + cos * dlx - sin * dly,
909
+ y: itemY + c.y + sin * dlx + cos * dly
910
+ };
911
+ }
912
+ function worldToItemLocal(wx, wy, itemX, itemY, w, h, rotationRad) {
913
+ const c = { x: w / 2, y: h / 2 };
914
+ const vx = wx - itemX;
915
+ const vy = wy - itemY;
916
+ const dx = vx - c.x;
917
+ const dy = vy - c.y;
918
+ const cos = Math.cos(-rotationRad);
919
+ const sin = Math.sin(-rotationRad);
920
+ const lx = cos * dx - sin * dy;
921
+ const ly = sin * dx + cos * dy;
922
+ return { x: c.x + lx, y: c.y + ly };
923
+ }
924
+ function boundsAabbForRotatedItem(item) {
925
+ const rot = getItemRotationRad(item);
926
+ if (Math.abs(rot) < 1e-12 && item.bounds.width >= 0 && item.bounds.height >= 0) {
927
+ return item.bounds;
928
+ }
929
+ const r = normalizeRect(item.bounds);
930
+ if (Math.abs(rot) < 1e-12) {
931
+ return r;
932
+ }
933
+ const corners = [
934
+ [0, 0],
935
+ [r.width, 0],
936
+ [r.width, r.height],
937
+ [0, r.height]
938
+ ];
939
+ let minX = Infinity;
940
+ let minY = Infinity;
941
+ let maxX = -Infinity;
942
+ let maxY = -Infinity;
943
+ for (const [lx, ly] of corners) {
944
+ const p = itemLocalToWorld(lx, ly, item.x, item.y, r.width, r.height, rot);
945
+ minX = Math.min(minX, p.x);
946
+ minY = Math.min(minY, p.y);
947
+ maxX = Math.max(maxX, p.x);
948
+ maxY = Math.max(maxY, p.y);
949
+ }
950
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
951
+ }
952
+
953
+ // src/interaction/resize-handles.ts
954
+ function getHandleWorldPosition(bounds, id) {
955
+ const r = normalizeRect(bounds);
956
+ const cx = r.x + r.width / 2;
957
+ const cy = r.y + r.height / 2;
958
+ switch (id) {
959
+ case "nw":
960
+ return { x: r.x, y: r.y };
961
+ case "n":
962
+ return { x: cx, y: r.y };
963
+ case "ne":
964
+ return { x: r.x + r.width, y: r.y };
965
+ case "e":
966
+ return { x: r.x + r.width, y: cy };
967
+ case "se":
968
+ return { x: r.x + r.width, y: r.y + r.height };
969
+ case "s":
970
+ return { x: cx, y: r.y + r.height };
971
+ case "sw":
972
+ return { x: r.x, y: r.y + r.height };
973
+ case "w":
974
+ return { x: r.x, y: cy };
975
+ }
976
+ }
977
+ function getHandleWorldPositionRotated(bounds, handle, rotationRad) {
978
+ const r = normalizeRect(bounds);
979
+ const p = getHandleWorldPosition(
980
+ { x: 0, y: 0, width: r.width, height: r.height },
981
+ handle
982
+ );
983
+ return itemLocalToWorld(p.x, p.y, r.x, r.y, r.width, r.height, rotationRad);
984
+ }
985
+ function getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld) {
986
+ const r = normalizeRect(bounds);
987
+ return itemLocalToWorld(
988
+ r.width / 2,
989
+ -handleOffsetWorld,
990
+ r.x,
991
+ r.y,
992
+ r.width,
993
+ r.height,
994
+ rotationRad
995
+ );
996
+ }
997
+ function rectFromCorners(a, b) {
998
+ const minX = Math.min(a.x, b.x);
999
+ const maxX = Math.max(a.x, b.x);
1000
+ const minY = Math.min(a.y, b.y);
1001
+ const maxY = Math.max(a.y, b.y);
1002
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
1003
+ }
1004
+
1005
+ // src/scene/freehand-path.ts
1006
+ function smoothFreehandPointsToPathD(points) {
1007
+ const n = points.length;
1008
+ if (n === 0) return "";
1009
+ if (n === 1) {
1010
+ const p = points[0];
1011
+ if (!p) return "";
1012
+ return `M ${p.x} ${p.y}`;
1013
+ }
1014
+ if (n === 2) {
1015
+ const a = points[0];
1016
+ const b = points[1];
1017
+ if (!a || !b) return "";
1018
+ return `M ${a.x} ${a.y} L ${b.x} ${b.y}`;
1019
+ }
1020
+ const p0 = points[0];
1021
+ if (!p0) return "";
1022
+ let d = `M ${p0.x} ${p0.y}`;
1023
+ let i = 1;
1024
+ for (; i < n - 2; i++) {
1025
+ const pi = points[i];
1026
+ const pi1 = points[i + 1];
1027
+ if (!pi || !pi1) continue;
1028
+ const xc = (pi.x + pi1.x) / 2;
1029
+ const yc = (pi.y + pi1.y) / 2;
1030
+ d += ` Q ${pi.x} ${pi.y} ${xc} ${yc}`;
1031
+ }
1032
+ const pLast = points[i];
1033
+ const pEnd = points[i + 1];
1034
+ if (!pLast || !pEnd) return d;
1035
+ d += ` Q ${pLast.x} ${pLast.y} ${pEnd.x} ${pEnd.y}`;
1036
+ return d;
1037
+ }
1038
+
944
1039
  // src/native/skia-transform.ts
945
1040
  function parseNum(s) {
946
1041
  return Number(s);
@@ -1765,9 +1860,9 @@ function NativeInteractionOverlay({
1765
1860
  const previewElements = useMemo(() => {
1766
1861
  if (!placementPreview) return null;
1767
1862
  const p = placementPreview;
1768
- if (p.kind === "rect" || p.kind === "ellipse") {
1863
+ if (p.kind === "rect" || p.kind === "ellipse" || p.kind === "architectural-cloud") {
1769
1864
  const r = normalizeRect(p.rect);
1770
- return p.kind === "rect" ? /* @__PURE__ */ jsx(
1865
+ return p.kind === "rect" || p.kind === "architectural-cloud" ? /* @__PURE__ */ jsx(
1771
1866
  Rect,
1772
1867
  {
1773
1868
  x: r.x,
@@ -2099,6 +2194,171 @@ function NativeSceneRenderer({
2099
2194
  if (width <= 0 || height <= 0) return null;
2100
2195
  return /* @__PURE__ */ jsx(Canvas, { style: { width, height }, children: /* @__PURE__ */ jsx(Group, { transform: cameraTransform, children: visible.map((item) => /* @__PURE__ */ jsx(MemoShape, { item }, item.id)) }) });
2101
2196
  }
2197
+ var DEFAULT_NATIVE_VECTOR_TOOLS = [
2198
+ { id: "hand", label: "Hand", shortLabel: "H" },
2199
+ { id: "select", label: "Select", shortLabel: "V" },
2200
+ { id: "draw", label: "Draw", shortLabel: "D" },
2201
+ { id: "marker", label: "Marker", shortLabel: "M" },
2202
+ { id: "eraser", label: "Eraser", shortLabel: "E" },
2203
+ { id: "text", label: "Text", shortLabel: "T" },
2204
+ { id: "note", label: "Note", shortLabel: "N" },
2205
+ { id: "rect", label: "Rectangle", shortLabel: "R" },
2206
+ { id: "ellipse", label: "Ellipse", shortLabel: "O" },
2207
+ { id: "architectural-cloud", label: "Cloud", shortLabel: "C" },
2208
+ { id: "line", label: "Line", shortLabel: "L" },
2209
+ { id: "arrow", label: "Arrow", shortLabel: "A" }
2210
+ ];
2211
+ function NativeVectorToolbar({
2212
+ value,
2213
+ onChange,
2214
+ tools = DEFAULT_NATIVE_VECTOR_TOOLS,
2215
+ disabled = false,
2216
+ disabledToolIds = [],
2217
+ accessibilityLabel = "Canvas tools",
2218
+ style,
2219
+ contentContainerStyle,
2220
+ toolButtonStyle,
2221
+ activeToolButtonStyle,
2222
+ toolLabelStyle,
2223
+ activeToolLabelStyle,
2224
+ renderToolIcon,
2225
+ renderToolButton
2226
+ }) {
2227
+ const disabledIds = useMemo(() => new Set(disabledToolIds), [disabledToolIds]);
2228
+ return /* @__PURE__ */ jsx(View, { accessibilityLabel, style: [styles.shell, style], children: /* @__PURE__ */ jsx(
2229
+ ScrollView,
2230
+ {
2231
+ horizontal: true,
2232
+ showsHorizontalScrollIndicator: false,
2233
+ contentContainerStyle: [styles.content, contentContainerStyle],
2234
+ children: tools.map((tool) => {
2235
+ const selected = tool.id === value;
2236
+ const toolDisabled = disabled || tool.disabled || disabledIds.has(tool.id);
2237
+ const foregroundColor = selected ? "#fafaf9" : "#18181b";
2238
+ const onSelect = () => {
2239
+ if (!toolDisabled) {
2240
+ onChange(tool.id);
2241
+ }
2242
+ };
2243
+ const input = {
2244
+ tool,
2245
+ selected,
2246
+ disabled: toolDisabled,
2247
+ foregroundColor,
2248
+ onSelect
2249
+ };
2250
+ if (renderToolButton) {
2251
+ return /* @__PURE__ */ jsx(View, { children: renderToolButton(input) }, tool.id);
2252
+ }
2253
+ const icon = renderToolIcon?.(input) ?? /* @__PURE__ */ jsx(
2254
+ Text$1,
2255
+ {
2256
+ style: [
2257
+ styles.shortLabel,
2258
+ { color: foregroundColor },
2259
+ toolLabelStyle,
2260
+ selected ? activeToolLabelStyle : void 0
2261
+ ],
2262
+ children: tool.shortLabel ?? tool.label.slice(0, 1).toUpperCase()
2263
+ }
2264
+ );
2265
+ return /* @__PURE__ */ jsxs(
2266
+ Pressable,
2267
+ {
2268
+ accessibilityLabel: tool.accessibilityLabel ?? tool.label,
2269
+ accessibilityRole: "button",
2270
+ accessibilityState: { selected, disabled: toolDisabled },
2271
+ disabled: toolDisabled,
2272
+ onPress: onSelect,
2273
+ style: ({ pressed }) => [
2274
+ styles.toolButton,
2275
+ toolButtonStyle,
2276
+ selected ? styles.activeToolButton : void 0,
2277
+ selected ? activeToolButtonStyle : void 0,
2278
+ pressed && !toolDisabled ? styles.pressedToolButton : void 0,
2279
+ toolDisabled ? styles.disabledToolButton : void 0
2280
+ ],
2281
+ children: [
2282
+ /* @__PURE__ */ jsx(View, { style: styles.iconSlot, children: icon }),
2283
+ /* @__PURE__ */ jsx(
2284
+ Text$1,
2285
+ {
2286
+ numberOfLines: 1,
2287
+ style: [
2288
+ styles.toolLabel,
2289
+ { color: foregroundColor },
2290
+ toolLabelStyle,
2291
+ selected ? activeToolLabelStyle : void 0
2292
+ ],
2293
+ children: tool.label
2294
+ }
2295
+ )
2296
+ ]
2297
+ },
2298
+ tool.id
2299
+ );
2300
+ })
2301
+ }
2302
+ ) });
2303
+ }
2304
+ var styles = StyleSheet.create({
2305
+ shell: {
2306
+ borderRadius: 10,
2307
+ borderWidth: StyleSheet.hairlineWidth,
2308
+ borderColor: "rgba(24, 24, 27, 0.14)",
2309
+ backgroundColor: "rgba(255, 255, 255, 0.96)",
2310
+ shadowColor: "#18181b",
2311
+ shadowOpacity: 0.12,
2312
+ shadowRadius: 16,
2313
+ shadowOffset: { width: 0, height: 8 },
2314
+ elevation: 6
2315
+ },
2316
+ content: {
2317
+ alignItems: "center",
2318
+ gap: 6,
2319
+ paddingHorizontal: 6,
2320
+ paddingVertical: 6
2321
+ },
2322
+ toolButton: {
2323
+ minWidth: 58,
2324
+ height: 52,
2325
+ alignItems: "center",
2326
+ justifyContent: "center",
2327
+ gap: 3,
2328
+ borderRadius: 8,
2329
+ paddingHorizontal: 8,
2330
+ borderWidth: StyleSheet.hairlineWidth,
2331
+ borderColor: "transparent",
2332
+ backgroundColor: "transparent"
2333
+ },
2334
+ activeToolButton: {
2335
+ borderColor: "rgba(24, 24, 27, 0.24)",
2336
+ backgroundColor: "#18181b"
2337
+ },
2338
+ pressedToolButton: {
2339
+ backgroundColor: "rgba(24, 24, 27, 0.08)"
2340
+ },
2341
+ disabledToolButton: {
2342
+ opacity: 0.42
2343
+ },
2344
+ iconSlot: {
2345
+ height: 22,
2346
+ minWidth: 22,
2347
+ alignItems: "center",
2348
+ justifyContent: "center"
2349
+ },
2350
+ shortLabel: {
2351
+ fontSize: 16,
2352
+ fontWeight: "700",
2353
+ lineHeight: 20
2354
+ },
2355
+ toolLabel: {
2356
+ maxWidth: 68,
2357
+ fontSize: 10,
2358
+ fontWeight: "600",
2359
+ lineHeight: 12
2360
+ }
2361
+ });
2102
2362
 
2103
2363
  // src/camera/camera.ts
2104
2364
  var Camera2D = class {
@@ -2286,6 +2546,37 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
2286
2546
  }
2287
2547
  return ids;
2288
2548
  }
2549
+ var MIN_PLACE_SIZE = 8;
2550
+ var MIN_ARROW_DRAG_PX = 8;
2551
+ var TAP_PX = 20;
2552
+ function isPlacementTool(toolId) {
2553
+ return toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud" || toolId === "line" || toolId === "arrow";
2554
+ }
2555
+ function placementPreviewForTool(toolId, start, end) {
2556
+ if (toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud") {
2557
+ return { kind: toolId, rect: rectFromCorners(start, end) };
2558
+ }
2559
+ return { kind: toolId, start, end };
2560
+ }
2561
+ function defaultPlacementWorld(toolId, center) {
2562
+ const cx = center.x;
2563
+ const cy = center.y;
2564
+ if (toolId === "rect") {
2565
+ return { raw: { x: cx - 60, y: cy - 40, width: 120, height: 80 } };
2566
+ }
2567
+ if (toolId === "ellipse") {
2568
+ return { raw: { x: cx - 70, y: cy - 45, width: 140, height: 90 } };
2569
+ }
2570
+ if (toolId === "architectural-cloud") {
2571
+ return { raw: { x: cx - 90, y: cy - 50, width: 180, height: 100 } };
2572
+ }
2573
+ const start = { x: cx - 50, y: cy };
2574
+ const end = { x: cx + 50, y: cy };
2575
+ return {
2576
+ raw: rectFromCorners(start, end),
2577
+ lineWorld: [start, end]
2578
+ };
2579
+ }
2289
2580
  function collectIdsInRect(items, marquee) {
2290
2581
  const m = normalizeRect(marquee);
2291
2582
  const out = [];
@@ -2463,10 +2754,31 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
2463
2754
  setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
2464
2755
  return;
2465
2756
  }
2757
+ if (isPlacementTool(tool)) {
2758
+ dragStateRef.current = {
2759
+ kind: "place",
2760
+ tool,
2761
+ startWorld: { x: worldX, y: worldY },
2762
+ startScreen: { x: sx, y: sy }
2763
+ };
2764
+ setPlacementPreview(
2765
+ placementPreviewForTool(
2766
+ tool,
2767
+ { x: worldX, y: worldY },
2768
+ {
2769
+ x: worldX,
2770
+ y: worldY
2771
+ }
2772
+ )
2773
+ );
2774
+ return;
2775
+ }
2466
2776
  if (tool === "note" || tool === "text") {
2467
2777
  dragStateRef.current = {
2468
2778
  kind: "tap",
2469
- startWorld: { x: worldX, y: worldY }
2779
+ tool,
2780
+ startWorld: { x: worldX, y: worldY },
2781
+ startScreen: { x: sx, y: sy }
2470
2782
  };
2471
2783
  return;
2472
2784
  }
@@ -2578,6 +2890,15 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
2578
2890
  setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
2579
2891
  return;
2580
2892
  }
2893
+ if (st.kind === "place") {
2894
+ setPlacementPreview(
2895
+ placementPreviewForTool(st.tool, st.startWorld, {
2896
+ x: worldX,
2897
+ y: worldY
2898
+ })
2899
+ );
2900
+ return;
2901
+ }
2581
2902
  },
2582
2903
  onPanResponderRelease: (evt) => {
2583
2904
  lastPinchDist.current = null;
@@ -2633,12 +2954,83 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
2633
2954
  dragStateRef.current = { kind: "idle" };
2634
2955
  return;
2635
2956
  }
2957
+ if (st.kind === "place") {
2958
+ dragStateRef.current = { kind: "idle" };
2959
+ setPlacementPreview(null);
2960
+ const change = onItemsChangeRef.current;
2961
+ if (!change) return;
2962
+ const { worldX, worldY } = screenToWorld(
2963
+ evt.nativeEvent.locationX,
2964
+ evt.nativeEvent.locationY
2965
+ );
2966
+ const a = st.startWorld;
2967
+ const b = { x: worldX, y: worldY };
2968
+ const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
2969
+ const screenDy = evt.nativeEvent.locationY - st.startScreen.y;
2970
+ if (st.tool === "arrow" && Math.hypot(screenDx, screenDy) < MIN_ARROW_DRAG_PX) {
2971
+ return;
2972
+ }
2973
+ let raw = rectFromCorners(a, b);
2974
+ let br = normalizeRect(raw);
2975
+ let lineStart = a;
2976
+ let lineEnd = b;
2977
+ if (br.width < MIN_PLACE_SIZE || br.height < MIN_PLACE_SIZE) {
2978
+ const center = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
2979
+ const defaults = defaultPlacementWorld(st.tool, center);
2980
+ raw = defaults.raw;
2981
+ br = normalizeRect(raw);
2982
+ if (defaults.lineWorld) {
2983
+ const [defaultStart, defaultEnd] = defaults.lineWorld;
2984
+ lineStart = defaultStart;
2985
+ lineEnd = defaultEnd;
2986
+ }
2987
+ }
2988
+ const id = createShapeId();
2989
+ if (st.tool === "rect") {
2990
+ change([...itemsRef.current, createRectangleItem(id, raw)]);
2991
+ onSelectionChangeRef.current?.([id]);
2992
+ return;
2993
+ }
2994
+ if (st.tool === "ellipse") {
2995
+ change([...itemsRef.current, createEllipseItem(id, raw)]);
2996
+ onSelectionChangeRef.current?.([id]);
2997
+ return;
2998
+ }
2999
+ if (st.tool === "architectural-cloud") {
3000
+ change([...itemsRef.current, createArchitecturalCloudItem(id, raw)]);
3001
+ onSelectionChangeRef.current?.([id]);
3002
+ return;
3003
+ }
3004
+ const line = lineEndpointsToLocal(br, lineStart, lineEnd);
3005
+ change([...itemsRef.current, createLineItem(id, br, line, st.tool)]);
3006
+ onSelectionChangeRef.current?.([id]);
3007
+ return;
3008
+ }
2636
3009
  if (st.kind === "tap") {
2637
3010
  dragStateRef.current = { kind: "idle" };
3011
+ const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
3012
+ const screenDy = evt.nativeEvent.locationY - st.startScreen.y;
3013
+ if (Math.hypot(screenDx, screenDy) > TAP_PX) return;
2638
3014
  const change = onItemsChangeRef.current;
2639
3015
  if (!change) return;
2640
- const tool = toolIdRef.current;
2641
- if (tool === "note") {
3016
+ if (st.tool === "text") {
3017
+ const id = createShapeId();
3018
+ const item = createTextItem(
3019
+ id,
3020
+ {
3021
+ x: st.startWorld.x - 4,
3022
+ y: st.startWorld.y - 18,
3023
+ width: 160,
3024
+ height: 26
3025
+ },
3026
+ "Text",
3027
+ void 0,
3028
+ 18
3029
+ );
3030
+ change([...itemsRef.current, item]);
3031
+ onSelectionChangeRef.current?.([id]);
3032
+ }
3033
+ if (st.tool === "note") {
2642
3034
  const id = createShapeId();
2643
3035
  const note = {
2644
3036
  id,
@@ -2654,6 +3046,7 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
2654
3046
  toolKind: "custom"
2655
3047
  };
2656
3048
  change([...itemsRef.current, note]);
3049
+ onSelectionChangeRef.current?.([id]);
2657
3050
  }
2658
3051
  return;
2659
3052
  }
@@ -2741,6 +3134,6 @@ var NativeVectorViewport = forwardRef(function NativeVectorViewport2({
2741
3134
  );
2742
3135
  });
2743
3136
 
2744
- export { NativeInteractionOverlay, NativeSceneRenderer, NativeShapeRenderer, NativeVectorViewport, parseSvgFragment };
3137
+ export { DEFAULT_NATIVE_VECTOR_TOOLS, NativeInteractionOverlay, NativeSceneRenderer, NativeShapeRenderer, NativeVectorToolbar, NativeVectorViewport, createFreehandStrokeItem, createShapeId, parseSvgFragment };
2745
3138
  //# sourceMappingURL=native.js.map
2746
3139
  //# sourceMappingURL=native.js.map