canvu-react 0.4.4 → 0.4.5

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
@@ -127,6 +127,13 @@ function getRotationHandleWorldPosition(bounds, rotationRad, handleOffsetWorld)
127
127
  rotationRad
128
128
  );
129
129
  }
130
+ function rectFromCorners(a, b) {
131
+ const minX = Math.min(a.x, b.x);
132
+ const maxX = Math.max(a.x, b.x);
133
+ const minY = Math.min(a.y, b.y);
134
+ const maxY = Math.max(a.y, b.y);
135
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
136
+ }
130
137
 
131
138
  // src/scene/freehand-path.ts
132
139
  function smoothFreehandPointsToPathD(points) {
@@ -786,6 +793,15 @@ function createShapeId() {
786
793
  const uid = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : String(Date.now());
787
794
  return `user-shape-${uid}`;
788
795
  }
796
+ function lineEndpointsToLocal(bounds, worldEndA, worldEndB) {
797
+ const b = normalizeRect(bounds);
798
+ return {
799
+ x1: worldEndA.x - b.x,
800
+ y1: worldEndA.y - b.y,
801
+ x2: worldEndB.x - b.x,
802
+ y2: worldEndB.y - b.y
803
+ };
804
+ }
789
805
  function rebuildItemSvg(item) {
790
806
  const style = resolveStrokeStyle(item);
791
807
  const k = item.toolKind;
@@ -889,6 +905,85 @@ function rebuildItemSvg(item) {
889
905
  }
890
906
  return item;
891
907
  }
908
+ function createRectangleItem(id, bounds, style) {
909
+ const r = normalizeRect(bounds);
910
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
911
+ return rebuildItemSvg({
912
+ id,
913
+ x: r.x,
914
+ y: r.y,
915
+ bounds: { ...r },
916
+ toolKind: "rect",
917
+ stroke: s.stroke,
918
+ strokeWidth: s.strokeWidth,
919
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
920
+ childrenSvg: ""
921
+ });
922
+ }
923
+ function createEllipseItem(id, bounds, style) {
924
+ const r = normalizeRect(bounds);
925
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
926
+ return rebuildItemSvg({
927
+ id,
928
+ x: r.x,
929
+ y: r.y,
930
+ bounds: { ...r },
931
+ toolKind: "ellipse",
932
+ stroke: s.stroke,
933
+ strokeWidth: s.strokeWidth,
934
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
935
+ childrenSvg: ""
936
+ });
937
+ }
938
+ function createArchitecturalCloudItem(id, bounds, style) {
939
+ const r = normalizeRect(bounds);
940
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
941
+ return rebuildItemSvg({
942
+ id,
943
+ x: r.x,
944
+ y: r.y,
945
+ bounds: { ...r },
946
+ toolKind: "architectural-cloud",
947
+ stroke: s.stroke,
948
+ strokeWidth: s.strokeWidth,
949
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
950
+ childrenSvg: ""
951
+ });
952
+ }
953
+ function createLineItem(id, bounds, line, toolKind, style, arrowBind) {
954
+ const r = normalizeRect(bounds);
955
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
956
+ return rebuildItemSvg({
957
+ id,
958
+ x: r.x,
959
+ y: r.y,
960
+ bounds: { ...r },
961
+ toolKind,
962
+ line: { ...line },
963
+ stroke: s.stroke,
964
+ strokeWidth: s.strokeWidth,
965
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
966
+ ...{},
967
+ childrenSvg: ""
968
+ });
969
+ }
970
+ function createTextItem(id, bounds, text = "", style, textFontSize) {
971
+ const r = normalizeRect(bounds);
972
+ const s = { ...DEFAULT_STROKE_STYLE, ...style };
973
+ return rebuildItemSvg({
974
+ id,
975
+ x: r.x,
976
+ y: r.y,
977
+ bounds: { ...r },
978
+ toolKind: "text",
979
+ text,
980
+ stroke: s.stroke,
981
+ strokeWidth: s.strokeWidth,
982
+ ...s.strokeOpacity != null ? { strokeOpacity: s.strokeOpacity } : {},
983
+ ...{ textFontSize } ,
984
+ childrenSvg: ""
985
+ });
986
+ }
892
987
  function createFreehandStrokeItem(id, pointsWorld, toolKind, style) {
893
988
  if (pointsWorld.length === 0) return null;
894
989
  const merged = {
@@ -1771,9 +1866,9 @@ function NativeInteractionOverlay({
1771
1866
  const previewElements = react.useMemo(() => {
1772
1867
  if (!placementPreview) return null;
1773
1868
  const p = placementPreview;
1774
- if (p.kind === "rect" || p.kind === "ellipse") {
1869
+ if (p.kind === "rect" || p.kind === "ellipse" || p.kind === "architectural-cloud") {
1775
1870
  const r = normalizeRect(p.rect);
1776
- return p.kind === "rect" ? /* @__PURE__ */ jsxRuntime.jsx(
1871
+ return p.kind === "rect" || p.kind === "architectural-cloud" ? /* @__PURE__ */ jsxRuntime.jsx(
1777
1872
  reactNativeSkia.Rect,
1778
1873
  {
1779
1874
  x: r.x,
@@ -2105,6 +2200,171 @@ function NativeSceneRenderer({
2105
2200
  if (width <= 0 || height <= 0) return null;
2106
2201
  return /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.Canvas, { style: { width, height }, children: /* @__PURE__ */ jsxRuntime.jsx(reactNativeSkia.Group, { transform: cameraTransform, children: visible.map((item) => /* @__PURE__ */ jsxRuntime.jsx(MemoShape, { item }, item.id)) }) });
2107
2202
  }
2203
+ var DEFAULT_NATIVE_VECTOR_TOOLS = [
2204
+ { id: "hand", label: "Hand", shortLabel: "H" },
2205
+ { id: "select", label: "Select", shortLabel: "V" },
2206
+ { id: "draw", label: "Draw", shortLabel: "D" },
2207
+ { id: "marker", label: "Marker", shortLabel: "M" },
2208
+ { id: "eraser", label: "Eraser", shortLabel: "E" },
2209
+ { id: "text", label: "Text", shortLabel: "T" },
2210
+ { id: "note", label: "Note", shortLabel: "N" },
2211
+ { id: "rect", label: "Rectangle", shortLabel: "R" },
2212
+ { id: "ellipse", label: "Ellipse", shortLabel: "O" },
2213
+ { id: "architectural-cloud", label: "Cloud", shortLabel: "C" },
2214
+ { id: "line", label: "Line", shortLabel: "L" },
2215
+ { id: "arrow", label: "Arrow", shortLabel: "A" }
2216
+ ];
2217
+ function NativeVectorToolbar({
2218
+ value,
2219
+ onChange,
2220
+ tools = DEFAULT_NATIVE_VECTOR_TOOLS,
2221
+ disabled = false,
2222
+ disabledToolIds = [],
2223
+ accessibilityLabel = "Canvas tools",
2224
+ style,
2225
+ contentContainerStyle,
2226
+ toolButtonStyle,
2227
+ activeToolButtonStyle,
2228
+ toolLabelStyle,
2229
+ activeToolLabelStyle,
2230
+ renderToolIcon,
2231
+ renderToolButton
2232
+ }) {
2233
+ const disabledIds = react.useMemo(() => new Set(disabledToolIds), [disabledToolIds]);
2234
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { accessibilityLabel, style: [styles.shell, style], children: /* @__PURE__ */ jsxRuntime.jsx(
2235
+ reactNative.ScrollView,
2236
+ {
2237
+ horizontal: true,
2238
+ showsHorizontalScrollIndicator: false,
2239
+ contentContainerStyle: [styles.content, contentContainerStyle],
2240
+ children: tools.map((tool) => {
2241
+ const selected = tool.id === value;
2242
+ const toolDisabled = disabled || tool.disabled || disabledIds.has(tool.id);
2243
+ const foregroundColor = selected ? "#fafaf9" : "#18181b";
2244
+ const onSelect = () => {
2245
+ if (!toolDisabled) {
2246
+ onChange(tool.id);
2247
+ }
2248
+ };
2249
+ const input = {
2250
+ tool,
2251
+ selected,
2252
+ disabled: toolDisabled,
2253
+ foregroundColor,
2254
+ onSelect
2255
+ };
2256
+ if (renderToolButton) {
2257
+ return /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { children: renderToolButton(input) }, tool.id);
2258
+ }
2259
+ const icon = renderToolIcon?.(input) ?? /* @__PURE__ */ jsxRuntime.jsx(
2260
+ reactNative.Text,
2261
+ {
2262
+ style: [
2263
+ styles.shortLabel,
2264
+ { color: foregroundColor },
2265
+ toolLabelStyle,
2266
+ selected ? activeToolLabelStyle : void 0
2267
+ ],
2268
+ children: tool.shortLabel ?? tool.label.slice(0, 1).toUpperCase()
2269
+ }
2270
+ );
2271
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2272
+ reactNative.Pressable,
2273
+ {
2274
+ accessibilityLabel: tool.accessibilityLabel ?? tool.label,
2275
+ accessibilityRole: "button",
2276
+ accessibilityState: { selected, disabled: toolDisabled },
2277
+ disabled: toolDisabled,
2278
+ onPress: onSelect,
2279
+ style: ({ pressed }) => [
2280
+ styles.toolButton,
2281
+ toolButtonStyle,
2282
+ selected ? styles.activeToolButton : void 0,
2283
+ selected ? activeToolButtonStyle : void 0,
2284
+ pressed && !toolDisabled ? styles.pressedToolButton : void 0,
2285
+ toolDisabled ? styles.disabledToolButton : void 0
2286
+ ],
2287
+ children: [
2288
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles.iconSlot, children: icon }),
2289
+ /* @__PURE__ */ jsxRuntime.jsx(
2290
+ reactNative.Text,
2291
+ {
2292
+ numberOfLines: 1,
2293
+ style: [
2294
+ styles.toolLabel,
2295
+ { color: foregroundColor },
2296
+ toolLabelStyle,
2297
+ selected ? activeToolLabelStyle : void 0
2298
+ ],
2299
+ children: tool.label
2300
+ }
2301
+ )
2302
+ ]
2303
+ },
2304
+ tool.id
2305
+ );
2306
+ })
2307
+ }
2308
+ ) });
2309
+ }
2310
+ var styles = reactNative.StyleSheet.create({
2311
+ shell: {
2312
+ borderRadius: 10,
2313
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
2314
+ borderColor: "rgba(24, 24, 27, 0.14)",
2315
+ backgroundColor: "rgba(255, 255, 255, 0.96)",
2316
+ shadowColor: "#18181b",
2317
+ shadowOpacity: 0.12,
2318
+ shadowRadius: 16,
2319
+ shadowOffset: { width: 0, height: 8 },
2320
+ elevation: 6
2321
+ },
2322
+ content: {
2323
+ alignItems: "center",
2324
+ gap: 6,
2325
+ paddingHorizontal: 6,
2326
+ paddingVertical: 6
2327
+ },
2328
+ toolButton: {
2329
+ minWidth: 58,
2330
+ height: 52,
2331
+ alignItems: "center",
2332
+ justifyContent: "center",
2333
+ gap: 3,
2334
+ borderRadius: 8,
2335
+ paddingHorizontal: 8,
2336
+ borderWidth: reactNative.StyleSheet.hairlineWidth,
2337
+ borderColor: "transparent",
2338
+ backgroundColor: "transparent"
2339
+ },
2340
+ activeToolButton: {
2341
+ borderColor: "rgba(24, 24, 27, 0.24)",
2342
+ backgroundColor: "#18181b"
2343
+ },
2344
+ pressedToolButton: {
2345
+ backgroundColor: "rgba(24, 24, 27, 0.08)"
2346
+ },
2347
+ disabledToolButton: {
2348
+ opacity: 0.42
2349
+ },
2350
+ iconSlot: {
2351
+ height: 22,
2352
+ minWidth: 22,
2353
+ alignItems: "center",
2354
+ justifyContent: "center"
2355
+ },
2356
+ shortLabel: {
2357
+ fontSize: 16,
2358
+ fontWeight: "700",
2359
+ lineHeight: 20
2360
+ },
2361
+ toolLabel: {
2362
+ maxWidth: 68,
2363
+ fontSize: 10,
2364
+ fontWeight: "600",
2365
+ lineHeight: 12
2366
+ }
2367
+ });
2108
2368
 
2109
2369
  // src/camera/camera.ts
2110
2370
  var Camera2D = class {
@@ -2292,6 +2552,37 @@ function collectEraserTargetsAtWorldPoint(items, worldX, worldY, options) {
2292
2552
  }
2293
2553
  return ids;
2294
2554
  }
2555
+ var MIN_PLACE_SIZE = 8;
2556
+ var MIN_ARROW_DRAG_PX = 8;
2557
+ var TAP_PX = 20;
2558
+ function isPlacementTool(toolId) {
2559
+ return toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud" || toolId === "line" || toolId === "arrow";
2560
+ }
2561
+ function placementPreviewForTool(toolId, start, end) {
2562
+ if (toolId === "rect" || toolId === "ellipse" || toolId === "architectural-cloud") {
2563
+ return { kind: toolId, rect: rectFromCorners(start, end) };
2564
+ }
2565
+ return { kind: toolId, start, end };
2566
+ }
2567
+ function defaultPlacementWorld(toolId, center) {
2568
+ const cx = center.x;
2569
+ const cy = center.y;
2570
+ if (toolId === "rect") {
2571
+ return { raw: { x: cx - 60, y: cy - 40, width: 120, height: 80 } };
2572
+ }
2573
+ if (toolId === "ellipse") {
2574
+ return { raw: { x: cx - 70, y: cy - 45, width: 140, height: 90 } };
2575
+ }
2576
+ if (toolId === "architectural-cloud") {
2577
+ return { raw: { x: cx - 90, y: cy - 50, width: 180, height: 100 } };
2578
+ }
2579
+ const start = { x: cx - 50, y: cy };
2580
+ const end = { x: cx + 50, y: cy };
2581
+ return {
2582
+ raw: rectFromCorners(start, end),
2583
+ lineWorld: [start, end]
2584
+ };
2585
+ }
2295
2586
  function collectIdsInRect(items, marquee) {
2296
2587
  const m = normalizeRect(marquee);
2297
2588
  const out = [];
@@ -2469,10 +2760,31 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2469
2760
  setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
2470
2761
  return;
2471
2762
  }
2763
+ if (isPlacementTool(tool)) {
2764
+ dragStateRef.current = {
2765
+ kind: "place",
2766
+ tool,
2767
+ startWorld: { x: worldX, y: worldY },
2768
+ startScreen: { x: sx, y: sy }
2769
+ };
2770
+ setPlacementPreview(
2771
+ placementPreviewForTool(
2772
+ tool,
2773
+ { x: worldX, y: worldY },
2774
+ {
2775
+ x: worldX,
2776
+ y: worldY
2777
+ }
2778
+ )
2779
+ );
2780
+ return;
2781
+ }
2472
2782
  if (tool === "note" || tool === "text") {
2473
2783
  dragStateRef.current = {
2474
2784
  kind: "tap",
2475
- startWorld: { x: worldX, y: worldY }
2785
+ tool,
2786
+ startWorld: { x: worldX, y: worldY },
2787
+ startScreen: { x: sx, y: sy }
2476
2788
  };
2477
2789
  return;
2478
2790
  }
@@ -2584,6 +2896,15 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2584
2896
  setEraserPreviewIds(Array.from(eraserPreviewIdSetRef.current));
2585
2897
  return;
2586
2898
  }
2899
+ if (st.kind === "place") {
2900
+ setPlacementPreview(
2901
+ placementPreviewForTool(st.tool, st.startWorld, {
2902
+ x: worldX,
2903
+ y: worldY
2904
+ })
2905
+ );
2906
+ return;
2907
+ }
2587
2908
  },
2588
2909
  onPanResponderRelease: (evt) => {
2589
2910
  lastPinchDist.current = null;
@@ -2639,12 +2960,83 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2639
2960
  dragStateRef.current = { kind: "idle" };
2640
2961
  return;
2641
2962
  }
2963
+ if (st.kind === "place") {
2964
+ dragStateRef.current = { kind: "idle" };
2965
+ setPlacementPreview(null);
2966
+ const change = onItemsChangeRef.current;
2967
+ if (!change) return;
2968
+ const { worldX, worldY } = screenToWorld(
2969
+ evt.nativeEvent.locationX,
2970
+ evt.nativeEvent.locationY
2971
+ );
2972
+ const a = st.startWorld;
2973
+ const b = { x: worldX, y: worldY };
2974
+ const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
2975
+ const screenDy = evt.nativeEvent.locationY - st.startScreen.y;
2976
+ if (st.tool === "arrow" && Math.hypot(screenDx, screenDy) < MIN_ARROW_DRAG_PX) {
2977
+ return;
2978
+ }
2979
+ let raw = rectFromCorners(a, b);
2980
+ let br = normalizeRect(raw);
2981
+ let lineStart = a;
2982
+ let lineEnd = b;
2983
+ if (br.width < MIN_PLACE_SIZE || br.height < MIN_PLACE_SIZE) {
2984
+ const center = { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
2985
+ const defaults = defaultPlacementWorld(st.tool, center);
2986
+ raw = defaults.raw;
2987
+ br = normalizeRect(raw);
2988
+ if (defaults.lineWorld) {
2989
+ const [defaultStart, defaultEnd] = defaults.lineWorld;
2990
+ lineStart = defaultStart;
2991
+ lineEnd = defaultEnd;
2992
+ }
2993
+ }
2994
+ const id = createShapeId();
2995
+ if (st.tool === "rect") {
2996
+ change([...itemsRef.current, createRectangleItem(id, raw)]);
2997
+ onSelectionChangeRef.current?.([id]);
2998
+ return;
2999
+ }
3000
+ if (st.tool === "ellipse") {
3001
+ change([...itemsRef.current, createEllipseItem(id, raw)]);
3002
+ onSelectionChangeRef.current?.([id]);
3003
+ return;
3004
+ }
3005
+ if (st.tool === "architectural-cloud") {
3006
+ change([...itemsRef.current, createArchitecturalCloudItem(id, raw)]);
3007
+ onSelectionChangeRef.current?.([id]);
3008
+ return;
3009
+ }
3010
+ const line = lineEndpointsToLocal(br, lineStart, lineEnd);
3011
+ change([...itemsRef.current, createLineItem(id, br, line, st.tool)]);
3012
+ onSelectionChangeRef.current?.([id]);
3013
+ return;
3014
+ }
2642
3015
  if (st.kind === "tap") {
2643
3016
  dragStateRef.current = { kind: "idle" };
3017
+ const screenDx = evt.nativeEvent.locationX - st.startScreen.x;
3018
+ const screenDy = evt.nativeEvent.locationY - st.startScreen.y;
3019
+ if (Math.hypot(screenDx, screenDy) > TAP_PX) return;
2644
3020
  const change = onItemsChangeRef.current;
2645
3021
  if (!change) return;
2646
- const tool = toolIdRef.current;
2647
- if (tool === "note") {
3022
+ if (st.tool === "text") {
3023
+ const id = createShapeId();
3024
+ const item = createTextItem(
3025
+ id,
3026
+ {
3027
+ x: st.startWorld.x - 4,
3028
+ y: st.startWorld.y - 18,
3029
+ width: 160,
3030
+ height: 26
3031
+ },
3032
+ "Text",
3033
+ void 0,
3034
+ 18
3035
+ );
3036
+ change([...itemsRef.current, item]);
3037
+ onSelectionChangeRef.current?.([id]);
3038
+ }
3039
+ if (st.tool === "note") {
2648
3040
  const id = createShapeId();
2649
3041
  const note = {
2650
3042
  id,
@@ -2660,6 +3052,7 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2660
3052
  toolKind: "custom"
2661
3053
  };
2662
3054
  change([...itemsRef.current, note]);
3055
+ onSelectionChangeRef.current?.([id]);
2663
3056
  }
2664
3057
  return;
2665
3058
  }
@@ -2747,9 +3140,11 @@ var NativeVectorViewport = react.forwardRef(function NativeVectorViewport2({
2747
3140
  );
2748
3141
  });
2749
3142
 
3143
+ exports.DEFAULT_NATIVE_VECTOR_TOOLS = DEFAULT_NATIVE_VECTOR_TOOLS;
2750
3144
  exports.NativeInteractionOverlay = NativeInteractionOverlay;
2751
3145
  exports.NativeSceneRenderer = NativeSceneRenderer;
2752
3146
  exports.NativeShapeRenderer = NativeShapeRenderer;
3147
+ exports.NativeVectorToolbar = NativeVectorToolbar;
2753
3148
  exports.NativeVectorViewport = NativeVectorViewport;
2754
3149
  exports.parseSvgFragment = parseSvgFragment;
2755
3150
  //# sourceMappingURL=native.cjs.map