@bwp-web/canvas 0.8.2 → 0.9.0

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/index.js CHANGED
@@ -85,7 +85,7 @@ function Canvas({
85
85
  }
86
86
 
87
87
  // src/hooks/useEditCanvas.ts
88
- import { useCallback, useEffect as useEffect2, useRef as useRef2, useState } from "react";
88
+ import { useCallback, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState } from "react";
89
89
  import { Polygon as Polygon4 } from "fabric";
90
90
 
91
91
  // src/viewport.ts
@@ -2455,6 +2455,7 @@ function useEditCanvas(options) {
2455
2455
  const vertexEditCleanupRef = useRef2(null);
2456
2456
  const keyboardCleanupRef = useRef2(null);
2457
2457
  const historyRef = useRef2(null);
2458
+ const isInitialLoadRef = useRef2(false);
2458
2459
  const optionsRef = useRef2(options);
2459
2460
  optionsRef.current = options;
2460
2461
  const savedSelectabilityRef = useRef2(
@@ -2467,6 +2468,11 @@ function useEditCanvas(options) {
2467
2468
  const [isDirty, setIsDirty] = useState(false);
2468
2469
  const [canUndo, setCanUndo] = useState(false);
2469
2470
  const [canRedo, setCanRedo] = useState(false);
2471
+ const [objects, setObjects] = useState([]);
2472
+ const [isLoading, setIsLoading] = useState(false);
2473
+ const [lockLightMode, setLockLightModeState] = useState(
2474
+ void 0
2475
+ );
2470
2476
  const setMode = useCallback((setup) => {
2471
2477
  vertexEditCleanupRef.current?.();
2472
2478
  vertexEditCleanupRef.current = null;
@@ -2551,11 +2557,14 @@ function useEditCanvas(options) {
2551
2557
  canvas.on("selection:created", (e) => setSelected(e.selected ?? []));
2552
2558
  canvas.on("selection:updated", (e) => setSelected(e.selected ?? []));
2553
2559
  canvas.on("selection:cleared", () => setSelected([]));
2554
- if (opts?.trackChanges) {
2555
- canvas.on("object:added", () => setIsDirty(true));
2556
- canvas.on("object:removed", () => setIsDirty(true));
2557
- canvas.on("object:modified", () => setIsDirty(true));
2558
- canvas.on("background:modified", () => setIsDirty(true));
2560
+ if (opts?.trackChanges !== false) {
2561
+ const markDirtyIfNotLoading = () => {
2562
+ if (!isInitialLoadRef.current) setIsDirty(true);
2563
+ };
2564
+ canvas.on("object:added", markDirtyIfNotLoading);
2565
+ canvas.on("object:removed", markDirtyIfNotLoading);
2566
+ canvas.on("object:modified", markDirtyIfNotLoading);
2567
+ canvas.on("background:modified", markDirtyIfNotLoading);
2559
2568
  }
2560
2569
  if (opts?.history) {
2561
2570
  const syncHistoryState = () => {
@@ -2593,8 +2602,31 @@ function useEditCanvas(options) {
2593
2602
  }
2594
2603
  }
2595
2604
  function invokeOnReady() {
2596
- const onReadyResult = opts?.onReady?.(canvas);
2597
- Promise.resolve(onReadyResult).then(() => {
2605
+ const initPromise = (async () => {
2606
+ if (opts?.canvasData) {
2607
+ setIsLoading(true);
2608
+ isInitialLoadRef.current = true;
2609
+ try {
2610
+ const loaded = await loadCanvas(canvas, opts.canvasData, {
2611
+ filter: opts.filter,
2612
+ borderRadius: opts.borderRadius
2613
+ });
2614
+ setObjects(loaded);
2615
+ } finally {
2616
+ isInitialLoadRef.current = false;
2617
+ setIsLoading(false);
2618
+ }
2619
+ }
2620
+ })();
2621
+ initPromise.then(async () => {
2622
+ const onReadyResult = opts?.onReady?.(canvas);
2623
+ await Promise.resolve(onReadyResult);
2624
+ if (opts?.invertBackground !== void 0) {
2625
+ setBackgroundInverted(canvas, opts.invertBackground);
2626
+ }
2627
+ if (canvas.lockLightMode !== void 0) {
2628
+ setLockLightModeState(canvas.lockLightMode);
2629
+ }
2598
2630
  if (opts?.autoFitToBackground !== false && canvas.backgroundImage) {
2599
2631
  fitViewportToBackground(canvas);
2600
2632
  syncZoom(canvasRef, setZoom);
@@ -2624,37 +2656,25 @@ function useEditCanvas(options) {
2624
2656
  alignmentCleanupRef.current = null;
2625
2657
  }
2626
2658
  }, [options?.enableAlignment]);
2659
+ useEffect2(() => {
2660
+ const canvas = canvasRef.current;
2661
+ if (!canvas || options?.invertBackground === void 0) return;
2662
+ setBackgroundInverted(canvas, options.invertBackground);
2663
+ }, [options?.invertBackground]);
2664
+ const setLockLightMode = useCallback((value) => {
2665
+ const canvas = canvasRef.current;
2666
+ if (canvas) {
2667
+ canvas.lockLightMode = value;
2668
+ }
2669
+ setLockLightModeState(value);
2670
+ }, []);
2627
2671
  const setViewportMode = useCallback((mode) => {
2628
2672
  viewportRef.current?.setMode(mode);
2629
2673
  setViewportModeState(mode);
2630
2674
  }, []);
2631
2675
  const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit } = useViewportActions(canvasRef, viewportRef, setZoom);
2632
- const setBackground = useCallback(
2633
- async (url, bgOpts) => {
2634
- const canvas = canvasRef.current;
2635
- if (!canvas) throw new Error("Canvas not ready");
2636
- const opts = optionsRef.current;
2637
- const resizeOpts = opts?.backgroundResize !== false ? typeof opts?.backgroundResize === "object" ? { ...opts.backgroundResize, ...bgOpts } : { ...bgOpts } : bgOpts?.preserveContrast ? { preserveContrast: true } : void 0;
2638
- const img = await setBackgroundImage(canvas, url, resizeOpts);
2639
- if (opts?.autoFitToBackground !== false) {
2640
- fitViewportToBackground(canvas);
2641
- syncZoom(canvasRef, setZoom);
2642
- }
2643
- return img;
2644
- },
2645
- []
2646
- );
2647
- return {
2648
- /** Pass this to `<Canvas onReady={...} />` */
2649
- onReady,
2650
- /** Ref to the underlying Fabric canvas instance. */
2651
- canvasRef,
2652
- /** Current zoom level (reactive). */
2653
- zoom,
2654
- /** Currently selected objects (reactive). */
2655
- selected,
2656
- /** Viewport controls. */
2657
- viewport: {
2676
+ const viewport = useMemo2(
2677
+ () => ({
2658
2678
  /** Current viewport mode (reactive). */
2659
2679
  mode: viewportMode,
2660
2680
  /** Switch between 'select' and 'pan' viewport modes. */
@@ -2669,65 +2689,129 @@ function useEditCanvas(options) {
2669
2689
  panToObject,
2670
2690
  /** Zoom and pan to fit a specific object in the viewport. */
2671
2691
  zoomToFit
2692
+ }),
2693
+ [
2694
+ viewportMode,
2695
+ setViewportMode,
2696
+ resetViewport2,
2697
+ zoomIn,
2698
+ zoomOut,
2699
+ panToObject,
2700
+ zoomToFit
2701
+ ]
2702
+ );
2703
+ const resetDirty = useCallback(() => setIsDirty(false), []);
2704
+ const markDirty = useCallback(() => setIsDirty(true), []);
2705
+ const undo = useCallback(async () => {
2706
+ const h = historyRef.current;
2707
+ if (!h) return;
2708
+ await h.undo();
2709
+ setCanUndo(h.canUndo());
2710
+ setCanRedo(h.canRedo());
2711
+ }, []);
2712
+ const redo = useCallback(async () => {
2713
+ const h = historyRef.current;
2714
+ if (!h) return;
2715
+ await h.redo();
2716
+ setCanUndo(h.canUndo());
2717
+ setCanRedo(h.canRedo());
2718
+ }, []);
2719
+ const setBackground = useCallback(
2720
+ async (url, bgOpts) => {
2721
+ const canvas = canvasRef.current;
2722
+ if (!canvas) throw new Error("Canvas not ready");
2723
+ const opts = optionsRef.current;
2724
+ const resizeOpts = opts?.backgroundResize !== false ? typeof opts?.backgroundResize === "object" ? { ...opts.backgroundResize, ...bgOpts } : { ...bgOpts } : bgOpts?.preserveContrast ? { preserveContrast: true } : void 0;
2725
+ const img = await setBackgroundImage(canvas, url, resizeOpts);
2726
+ if (opts?.autoFitToBackground !== false) {
2727
+ fitViewportToBackground(canvas);
2728
+ syncZoom(canvasRef, setZoom);
2729
+ }
2730
+ return img;
2672
2731
  },
2673
- /** Whether vertex edit mode is currently active (reactive). */
2674
- isEditingVertices,
2675
- /**
2676
- * Activate an interaction mode or return to select mode.
2677
- *
2678
- * Pass a setup function to activate a creation mode:
2679
- * ```ts
2680
- * canvas.setMode((c, viewport) =>
2681
- * enableClickToCreate(c, factory, { viewport })
2682
- * );
2683
- * ```
2684
- *
2685
- * Pass `null` to deactivate and return to select mode:
2686
- * ```ts
2687
- * canvas.setMode(null);
2688
- * ```
2689
- */
2690
- setMode,
2691
- /**
2692
- * Set a background image from a URL. Automatically resizes if the image
2693
- * exceeds the configured limits (opt out via `backgroundResize: false`),
2694
- * and fits the viewport after setting if `autoFitToBackground` is enabled.
2695
- *
2696
- * Pass `{ preserveContrast: true }` to keep the current background contrast
2697
- * when replacing the image.
2698
- */
2699
- setBackground,
2700
- /** Whether the canvas has been modified since the last `resetDirty()` call. Requires `trackChanges: true`. */
2701
- isDirty,
2702
- /** Reset the dirty flag (e.g., after a successful save). */
2703
- resetDirty: useCallback(() => setIsDirty(false), []),
2704
- /** Manually mark the canvas as dirty (e.g., after a custom operation not tracked automatically). */
2705
- markDirty: useCallback(() => setIsDirty(true), []),
2706
- /** Undo the last change. Requires `history: true`. */
2707
- undo: useCallback(async () => {
2708
- const h = historyRef.current;
2709
- if (!h) return;
2710
- await h.undo();
2711
- setCanUndo(h.canUndo());
2712
- setCanRedo(h.canRedo());
2713
- }, []),
2714
- /** Redo a previously undone change. Requires `history: true`. */
2715
- redo: useCallback(async () => {
2716
- const h = historyRef.current;
2717
- if (!h) return;
2718
- await h.redo();
2719
- setCanUndo(h.canUndo());
2720
- setCanRedo(h.canRedo());
2721
- }, []),
2722
- /** Whether an undo operation is available (reactive). Requires `history: true`. */
2723
- canUndo,
2724
- /** Whether a redo operation is available (reactive). Requires `history: true`. */
2725
- canRedo
2726
- };
2732
+ []
2733
+ );
2734
+ return useMemo2(
2735
+ () => ({
2736
+ /** Pass this to `<Canvas onReady={...} />` */
2737
+ onReady,
2738
+ /** Ref to the underlying Fabric canvas instance. */
2739
+ canvasRef,
2740
+ /** Current zoom level (reactive). */
2741
+ zoom,
2742
+ /** Loaded objects (reactive). Populated when `canvasData` is provided. */
2743
+ objects,
2744
+ /** Whether canvas data is currently being loaded. */
2745
+ isLoading,
2746
+ /** Currently selected objects (reactive). */
2747
+ selected,
2748
+ /** Viewport controls. */
2749
+ viewport,
2750
+ /** Whether vertex edit mode is currently active (reactive). */
2751
+ isEditingVertices,
2752
+ /**
2753
+ * Activate an interaction mode or return to select mode.
2754
+ *
2755
+ * Pass a setup function to activate a creation mode:
2756
+ * ```ts
2757
+ * canvas.setMode((c, viewport) =>
2758
+ * enableClickToCreate(c, factory, { viewport })
2759
+ * );
2760
+ * ```
2761
+ *
2762
+ * Pass `null` to deactivate and return to select mode:
2763
+ * ```ts
2764
+ * canvas.setMode(null);
2765
+ * ```
2766
+ */
2767
+ setMode,
2768
+ /**
2769
+ * Set a background image from a URL. Automatically resizes if the image
2770
+ * exceeds the configured limits (opt out via `backgroundResize: false`),
2771
+ * and fits the viewport after setting if `autoFitToBackground` is enabled.
2772
+ *
2773
+ * Pass `{ preserveContrast: true }` to keep the current background contrast
2774
+ * when replacing the image.
2775
+ */
2776
+ setBackground,
2777
+ /** Whether the canvas has been modified since the last `resetDirty()` call. Enabled by default (disable via `trackChanges: false`). */
2778
+ isDirty,
2779
+ /** Reset the dirty flag (e.g., after a successful save). */
2780
+ resetDirty,
2781
+ /** Manually mark the canvas as dirty (e.g., after a custom operation not tracked automatically). */
2782
+ markDirty,
2783
+ /** Undo the last change. Requires `history: true`. */
2784
+ undo,
2785
+ /** Redo a previously undone change. Requires `history: true`. */
2786
+ redo,
2787
+ /** Whether an undo operation is available (reactive). Requires `history: true`. */
2788
+ canUndo,
2789
+ /** Whether a redo operation is available (reactive). Requires `history: true`. */
2790
+ canRedo,
2791
+ /** Whether the canvas is locked to light mode. Read from loaded canvas data. */
2792
+ lockLightMode,
2793
+ /** Update lockLightMode on both the canvas instance and React state. */
2794
+ setLockLightMode
2795
+ }),
2796
+ // Only reactive state in deps — refs and stable callbacks are omitted
2797
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2798
+ [
2799
+ zoom,
2800
+ objects,
2801
+ isLoading,
2802
+ selected,
2803
+ viewport,
2804
+ isEditingVertices,
2805
+ isDirty,
2806
+ canUndo,
2807
+ canRedo,
2808
+ lockLightMode
2809
+ ]
2810
+ );
2727
2811
  }
2728
2812
 
2729
2813
  // src/hooks/useViewCanvas.ts
2730
- import { useCallback as useCallback2, useRef as useRef3, useState as useState2 } from "react";
2814
+ import { useCallback as useCallback2, useEffect as useEffect3, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
2731
2815
  function lockCanvas(canvas) {
2732
2816
  canvas.selection = false;
2733
2817
  canvas.forEachObject((obj) => {
@@ -2740,6 +2824,8 @@ function useViewCanvas(options) {
2740
2824
  const optionsRef = useRef3(options);
2741
2825
  optionsRef.current = options;
2742
2826
  const [zoom, setZoom] = useState2(1);
2827
+ const [objects, setObjects] = useState2([]);
2828
+ const [isLoading, setIsLoading] = useState2(false);
2743
2829
  const onReady = useCallback2(
2744
2830
  (canvas) => {
2745
2831
  canvasRef.current = canvas;
@@ -2766,20 +2852,57 @@ function useViewCanvas(options) {
2766
2852
  canvas.on("mouse:wheel", () => {
2767
2853
  setZoom(canvas.getZoom());
2768
2854
  });
2769
- const onReadyResult = opts?.onReady?.(canvas);
2770
- if (opts?.autoFitToBackground !== false) {
2771
- Promise.resolve(onReadyResult).then(() => {
2772
- if (canvas.backgroundImage) {
2773
- fitViewportToBackground(canvas);
2774
- syncZoom(canvasRef, setZoom);
2855
+ const initPromise = (async () => {
2856
+ if (opts?.canvasData) {
2857
+ setIsLoading(true);
2858
+ try {
2859
+ const loaded = await loadCanvas(canvas, opts.canvasData, {
2860
+ filter: opts.filter,
2861
+ borderRadius: opts.borderRadius
2862
+ });
2863
+ lockCanvas(canvas);
2864
+ setObjects(loaded);
2865
+ } finally {
2866
+ setIsLoading(false);
2775
2867
  }
2776
- });
2777
- }
2868
+ }
2869
+ })();
2870
+ initPromise.then(async () => {
2871
+ const onReadyResult = opts?.onReady?.(canvas);
2872
+ await Promise.resolve(onReadyResult);
2873
+ if (opts?.invertBackground !== void 0) {
2874
+ setBackgroundInverted(canvas, opts.invertBackground);
2875
+ }
2876
+ if (opts?.autoFitToBackground !== false && canvas.backgroundImage) {
2877
+ fitViewportToBackground(canvas);
2878
+ syncZoom(canvasRef, setZoom);
2879
+ }
2880
+ });
2778
2881
  },
2779
2882
  // onReady and panAndZoom are intentionally excluded — we only initialize once
2780
2883
  []
2781
2884
  );
2885
+ useEffect3(() => {
2886
+ const canvas = canvasRef.current;
2887
+ if (!canvas || options?.invertBackground === void 0) return;
2888
+ setBackgroundInverted(canvas, options.invertBackground);
2889
+ }, [options?.invertBackground]);
2782
2890
  const { resetViewport: resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit } = useViewportActions(canvasRef, viewportRef, setZoom);
2891
+ const viewport = useMemo3(
2892
+ () => ({
2893
+ /** Reset viewport to default (no pan, zoom = 1), or fit to background if one is set. */
2894
+ reset: resetViewport2,
2895
+ /** Zoom in toward the canvas center. Default step: 0.2. */
2896
+ zoomIn,
2897
+ /** Zoom out from the canvas center. Default step: 0.2. */
2898
+ zoomOut,
2899
+ /** Pan the viewport to center on a specific object. */
2900
+ panToObject,
2901
+ /** Zoom and pan to fit a specific object in the viewport. */
2902
+ zoomToFit
2903
+ }),
2904
+ [resetViewport2, zoomIn, zoomOut, panToObject, zoomToFit]
2905
+ );
2783
2906
  const findObject = (id) => {
2784
2907
  const c = canvasRef.current;
2785
2908
  if (!c) return void 0;
@@ -2795,9 +2918,9 @@ function useViewCanvas(options) {
2795
2918
  (styles) => {
2796
2919
  const c = canvasRef.current;
2797
2920
  if (!c) return;
2798
- const objects = c.getObjects();
2921
+ const objects2 = c.getObjects();
2799
2922
  const idMap = /* @__PURE__ */ new Map();
2800
- for (const obj of objects) {
2923
+ for (const obj of objects2) {
2801
2924
  if (obj.data?.id) idMap.set(obj.data.id, obj);
2802
2925
  }
2803
2926
  let updated = false;
@@ -2827,42 +2950,58 @@ function useViewCanvas(options) {
2827
2950
  },
2828
2951
  []
2829
2952
  );
2830
- return {
2831
- /** Pass this to `<Canvas onReady={...} />` */
2832
- onReady,
2833
- /** Ref to the underlying Fabric canvas instance. */
2834
- canvasRef,
2835
- /** Current zoom level (reactive). */
2836
- zoom,
2837
- /** Viewport controls. */
2838
- viewport: {
2839
- /** Reset viewport to default (no pan, zoom = 1), or fit to background if one is set. */
2840
- reset: resetViewport2,
2841
- /** Zoom in toward the canvas center. Default step: 0.2. */
2842
- zoomIn,
2843
- /** Zoom out from the canvas center. Default step: 0.2. */
2844
- zoomOut,
2845
- /** Pan the viewport to center on a specific object. */
2846
- panToObject,
2847
- /** Zoom and pan to fit a specific object in the viewport. */
2848
- zoomToFit
2849
- },
2850
- /** Update a single object's visual style by its `data.id`. */
2851
- setObjectStyle,
2852
- /** Batch-update multiple objects' visual styles in one render. Keyed by `data.id`. */
2853
- setObjectStyles,
2854
- /** Apply a visual style to all objects whose `data.type` matches. */
2855
- setObjectStyleByType
2856
- };
2953
+ return useMemo3(
2954
+ () => ({
2955
+ /** Pass this to `<Canvas onReady={...} />` */
2956
+ onReady,
2957
+ /** Ref to the underlying Fabric canvas instance. */
2958
+ canvasRef,
2959
+ /** Current zoom level (reactive). */
2960
+ zoom,
2961
+ /** Loaded objects (reactive). Populated when `canvasData` is provided. */
2962
+ objects,
2963
+ /** Whether canvas data is currently being loaded. */
2964
+ isLoading,
2965
+ /** Viewport controls. */
2966
+ viewport,
2967
+ /** Update a single object's visual style by its `data.id`. */
2968
+ setObjectStyle,
2969
+ /** Batch-update multiple objects' visual styles in one render. Keyed by `data.id`. */
2970
+ setObjectStyles,
2971
+ /** Apply a visual style to all objects whose `data.type` matches. */
2972
+ setObjectStyleByType
2973
+ }),
2974
+ // Only reactive state in deps — refs and stable callbacks are omitted
2975
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2976
+ [zoom, objects, isLoading, viewport]
2977
+ );
2857
2978
  }
2858
2979
 
2859
2980
  // src/hooks/useCanvasEvents.ts
2860
- import { useEffect as useEffect3, useRef as useRef4 } from "react";
2861
- function useCanvasEvents(canvasRef, events) {
2981
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
2982
+
2983
+ // src/context/CanvasRefContext.ts
2984
+ import { createContext, useContext } from "react";
2985
+ var CanvasRefContext = createContext(null);
2986
+ function useCanvasRefContext() {
2987
+ return useContext(CanvasRefContext);
2988
+ }
2989
+
2990
+ // src/context/useCanvasRef.ts
2991
+ function useCanvasRef() {
2992
+ return useCanvasRefContext();
2993
+ }
2994
+
2995
+ // src/hooks/useCanvasEvents.ts
2996
+ function useCanvasEvents(canvasRefOrEvents, maybeEvents) {
2997
+ const isContextOverload = maybeEvents === void 0;
2998
+ const contextCanvasRef = useCanvasRef();
2999
+ const resolvedCanvasRef = isContextOverload ? contextCanvasRef : canvasRefOrEvents;
3000
+ const events = isContextOverload ? canvasRefOrEvents : maybeEvents;
2862
3001
  const eventsRef = useRef4(events);
2863
3002
  eventsRef.current = events;
2864
- useEffect3(() => {
2865
- const canvas = canvasRef.current;
3003
+ useEffect4(() => {
3004
+ const canvas = resolvedCanvasRef?.current;
2866
3005
  if (!canvas) return;
2867
3006
  const wrappers = /* @__PURE__ */ new Map();
2868
3007
  for (const key of Object.keys(eventsRef.current)) {
@@ -2878,12 +3017,16 @@ function useCanvasEvents(canvasRef, events) {
2878
3017
  canvas.off(name, handler);
2879
3018
  });
2880
3019
  };
2881
- }, [canvasRef]);
3020
+ }, [resolvedCanvasRef]);
2882
3021
  }
2883
3022
 
2884
3023
  // src/hooks/useCanvasTooltip.ts
2885
- import { useEffect as useEffect4, useRef as useRef5, useState as useState3 } from "react";
2886
- function useCanvasTooltip(canvasRef, options) {
3024
+ import { useEffect as useEffect5, useRef as useRef5, useState as useState3 } from "react";
3025
+ function useCanvasTooltip(canvasRefOrOptions, maybeOptions) {
3026
+ const isContextOverload = maybeOptions === void 0;
3027
+ const contextCanvasRef = useCanvasRef();
3028
+ const resolvedCanvasRef = isContextOverload ? contextCanvasRef : canvasRefOrOptions;
3029
+ const options = isContextOverload ? canvasRefOrOptions : maybeOptions;
2887
3030
  const [state, setState] = useState3({
2888
3031
  visible: false,
2889
3032
  content: null,
@@ -2892,8 +3035,8 @@ function useCanvasTooltip(canvasRef, options) {
2892
3035
  const hoveredObjectRef = useRef5(null);
2893
3036
  const optionsRef = useRef5(options);
2894
3037
  optionsRef.current = options;
2895
- useEffect4(() => {
2896
- const canvas = canvasRef.current;
3038
+ useEffect5(() => {
3039
+ const canvas = resolvedCanvasRef?.current;
2897
3040
  if (!canvas) return;
2898
3041
  function calculatePosition(target) {
2899
3042
  const bounds = target.getBoundingRect();
@@ -2941,19 +3084,24 @@ function useCanvasTooltip(canvasRef, options) {
2941
3084
  canvas.off("after:render", updatePosition);
2942
3085
  canvas.off("mouse:wheel", updatePosition);
2943
3086
  };
2944
- }, [canvasRef]);
3087
+ }, [resolvedCanvasRef]);
2945
3088
  return state;
2946
3089
  }
2947
3090
 
2948
3091
  // src/hooks/useCanvasClick.ts
2949
- import { useEffect as useEffect5, useRef as useRef6 } from "react";
2950
- function useCanvasClick(canvasRef, onClick, options) {
3092
+ import { useEffect as useEffect6, useRef as useRef6 } from "react";
3093
+ function useCanvasClick(canvasRefOrOnClick, onClickOrOptions, maybeOptions) {
3094
+ const isContextOverload = typeof canvasRefOrOnClick === "function";
3095
+ const contextCanvasRef = useCanvasRef();
3096
+ const resolvedCanvasRef = isContextOverload ? contextCanvasRef : canvasRefOrOnClick;
3097
+ const onClick = isContextOverload ? canvasRefOrOnClick : onClickOrOptions;
3098
+ const options = isContextOverload ? onClickOrOptions : maybeOptions;
2951
3099
  const onClickRef = useRef6(onClick);
2952
3100
  onClickRef.current = onClick;
2953
3101
  const optionsRef = useRef6(options);
2954
3102
  optionsRef.current = options;
2955
- useEffect5(() => {
2956
- const canvas = canvasRef.current;
3103
+ useEffect6(() => {
3104
+ const canvas = resolvedCanvasRef?.current;
2957
3105
  if (!canvas) return;
2958
3106
  let mouseDown = null;
2959
3107
  let isPanning = false;
@@ -2993,66 +3141,169 @@ function useCanvasClick(canvasRef, onClick, options) {
2993
3141
  canvas.off("mouse:move", handleMouseMove);
2994
3142
  canvas.off("mouse:up", handleMouseUp);
2995
3143
  };
2996
- }, [canvasRef]);
3144
+ }, [resolvedCanvasRef]);
2997
3145
  }
2998
3146
 
2999
3147
  // src/context/EditCanvasContext.tsx
3000
- import { createContext, useContext } from "react";
3148
+ import { createContext as createContext2, useContext as useContext2, useMemo as useMemo4 } from "react";
3001
3149
  import { jsx as jsx2 } from "react/jsx-runtime";
3002
- var EditCanvasContext = createContext(null);
3150
+ var EditViewportContext = createContext2(null);
3151
+ var EditStateContext = createContext2(null);
3003
3152
  function EditCanvasProvider({
3004
3153
  options,
3005
3154
  children
3006
3155
  }) {
3007
3156
  const canvas = useEditCanvas(options);
3008
- return /* @__PURE__ */ jsx2(EditCanvasContext.Provider, { value: canvas, children });
3157
+ const viewportValue = useMemo4(
3158
+ () => ({ zoom: canvas.zoom, viewport: canvas.viewport }),
3159
+ [canvas.zoom, canvas.viewport]
3160
+ );
3161
+ const stateValue = useMemo4(
3162
+ () => ({
3163
+ onReady: canvas.onReady,
3164
+ objects: canvas.objects,
3165
+ isLoading: canvas.isLoading,
3166
+ selected: canvas.selected,
3167
+ isEditingVertices: canvas.isEditingVertices,
3168
+ setMode: canvas.setMode,
3169
+ setBackground: canvas.setBackground,
3170
+ isDirty: canvas.isDirty,
3171
+ resetDirty: canvas.resetDirty,
3172
+ markDirty: canvas.markDirty,
3173
+ undo: canvas.undo,
3174
+ redo: canvas.redo,
3175
+ canUndo: canvas.canUndo,
3176
+ canRedo: canvas.canRedo,
3177
+ lockLightMode: canvas.lockLightMode,
3178
+ setLockLightMode: canvas.setLockLightMode
3179
+ }),
3180
+ // Only reactive state — stable callbacks omitted
3181
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3182
+ [
3183
+ canvas.objects,
3184
+ canvas.isLoading,
3185
+ canvas.selected,
3186
+ canvas.isEditingVertices,
3187
+ canvas.isDirty,
3188
+ canvas.canUndo,
3189
+ canvas.canRedo,
3190
+ canvas.lockLightMode
3191
+ ]
3192
+ );
3193
+ return /* @__PURE__ */ jsx2(CanvasRefContext.Provider, { value: canvas.canvasRef, children: /* @__PURE__ */ jsx2(EditViewportContext.Provider, { value: viewportValue, children: /* @__PURE__ */ jsx2(EditStateContext.Provider, { value: stateValue, children }) }) });
3009
3194
  }
3010
3195
  function useEditCanvasContext() {
3011
- const ctx = useContext(EditCanvasContext);
3012
- if (ctx === null) {
3196
+ const canvasRef = useContext2(CanvasRefContext);
3197
+ const viewport = useContext2(EditViewportContext);
3198
+ const state = useContext2(EditStateContext);
3199
+ if (canvasRef === null || viewport === null || state === null) {
3013
3200
  throw new Error(
3014
3201
  "useEditCanvasContext must be used within an <EditCanvasProvider>"
3015
3202
  );
3016
3203
  }
3204
+ return useMemo4(
3205
+ () => ({ canvasRef, ...viewport, ...state }),
3206
+ [canvasRef, viewport, state]
3207
+ );
3208
+ }
3209
+ function useEditCanvasViewport() {
3210
+ const ctx = useContext2(EditViewportContext);
3211
+ if (ctx === null) {
3212
+ throw new Error(
3213
+ "useEditCanvasViewport must be used within an <EditCanvasProvider>"
3214
+ );
3215
+ }
3216
+ return ctx;
3217
+ }
3218
+ function useEditCanvasState() {
3219
+ const ctx = useContext2(EditStateContext);
3220
+ if (ctx === null) {
3221
+ throw new Error(
3222
+ "useEditCanvasState must be used within an <EditCanvasProvider>"
3223
+ );
3224
+ }
3017
3225
  return ctx;
3018
3226
  }
3019
3227
 
3020
3228
  // src/context/ViewCanvasContext.tsx
3021
- import { createContext as createContext2, useContext as useContext2 } from "react";
3229
+ import { createContext as createContext3, useContext as useContext3, useMemo as useMemo5 } from "react";
3022
3230
  import { jsx as jsx3 } from "react/jsx-runtime";
3023
- var ViewCanvasContext = createContext2(null);
3231
+ var ViewViewportContext = createContext3(null);
3232
+ var ViewStateContext = createContext3(null);
3024
3233
  function ViewCanvasProvider({
3025
3234
  options,
3026
3235
  children
3027
3236
  }) {
3028
3237
  const canvas = useViewCanvas(options);
3029
- return /* @__PURE__ */ jsx3(ViewCanvasContext.Provider, { value: canvas, children });
3238
+ const viewportValue = useMemo5(
3239
+ () => ({ zoom: canvas.zoom, viewport: canvas.viewport }),
3240
+ [canvas.zoom, canvas.viewport]
3241
+ );
3242
+ const stateValue = useMemo5(
3243
+ () => ({
3244
+ onReady: canvas.onReady,
3245
+ objects: canvas.objects,
3246
+ isLoading: canvas.isLoading,
3247
+ setObjectStyle: canvas.setObjectStyle,
3248
+ setObjectStyles: canvas.setObjectStyles,
3249
+ setObjectStyleByType: canvas.setObjectStyleByType
3250
+ }),
3251
+ // Only reactive state — stable callbacks omitted
3252
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3253
+ [canvas.objects, canvas.isLoading]
3254
+ );
3255
+ return /* @__PURE__ */ jsx3(CanvasRefContext.Provider, { value: canvas.canvasRef, children: /* @__PURE__ */ jsx3(ViewViewportContext.Provider, { value: viewportValue, children: /* @__PURE__ */ jsx3(ViewStateContext.Provider, { value: stateValue, children }) }) });
3030
3256
  }
3031
3257
  function useViewCanvasContext() {
3032
- const ctx = useContext2(ViewCanvasContext);
3033
- if (ctx === null) {
3258
+ const canvasRef = useContext3(CanvasRefContext);
3259
+ const viewport = useContext3(ViewViewportContext);
3260
+ const state = useContext3(ViewStateContext);
3261
+ if (canvasRef === null || viewport === null || state === null) {
3034
3262
  throw new Error(
3035
3263
  "useViewCanvasContext must be used within a <ViewCanvasProvider>"
3036
3264
  );
3037
3265
  }
3266
+ return useMemo5(
3267
+ () => ({ canvasRef, ...viewport, ...state }),
3268
+ [canvasRef, viewport, state]
3269
+ );
3270
+ }
3271
+ function useViewCanvasViewport() {
3272
+ const ctx = useContext3(ViewViewportContext);
3273
+ if (ctx === null) {
3274
+ throw new Error(
3275
+ "useViewCanvasViewport must be used within a <ViewCanvasProvider>"
3276
+ );
3277
+ }
3278
+ return ctx;
3279
+ }
3280
+ function useViewCanvasState() {
3281
+ const ctx = useContext3(ViewStateContext);
3282
+ if (ctx === null) {
3283
+ throw new Error(
3284
+ "useViewCanvasState must be used within a <ViewCanvasProvider>"
3285
+ );
3286
+ }
3038
3287
  return ctx;
3039
3288
  }
3040
3289
 
3041
3290
  // src/overlay/ObjectOverlay.tsx
3042
- import { useEffect as useEffect6, useRef as useRef7 } from "react";
3291
+ import { useEffect as useEffect7, useRef as useRef7 } from "react";
3043
3292
  import { Stack } from "@mui/material";
3044
3293
  import { util as util4 } from "fabric";
3045
3294
  import { jsx as jsx4 } from "react/jsx-runtime";
3046
3295
  function ObjectOverlay({
3047
- canvasRef,
3296
+ canvasRef: canvasRefProp,
3048
3297
  object,
3049
3298
  sx,
3050
3299
  children,
3051
3300
  ...rest
3052
3301
  }) {
3302
+ const contextCanvasRef = useCanvasRef();
3303
+ const canvasRef = canvasRefProp ?? contextCanvasRef;
3053
3304
  const stackRef = useRef7(null);
3054
- useEffect6(() => {
3055
- const canvas = canvasRef.current;
3305
+ useEffect7(() => {
3306
+ const canvas = canvasRef?.current;
3056
3307
  if (!canvas || !object) return;
3057
3308
  function update() {
3058
3309
  const el = stackRef.current;
@@ -3106,7 +3357,7 @@ function ObjectOverlay({
3106
3357
 
3107
3358
  // src/overlay/OverlayContent.tsx
3108
3359
  import { Stack as Stack2 } from "@mui/material";
3109
- import { useEffect as useEffect7, useRef as useRef8 } from "react";
3360
+ import { useEffect as useEffect8, useRef as useRef8 } from "react";
3110
3361
  import { jsx as jsx5 } from "react/jsx-runtime";
3111
3362
  function OverlayContent({
3112
3363
  children,
@@ -3117,7 +3368,7 @@ function OverlayContent({
3117
3368
  }) {
3118
3369
  const outerRef = useRef8(null);
3119
3370
  const innerRef = useRef8(null);
3120
- useEffect7(() => {
3371
+ useEffect8(() => {
3121
3372
  const outer = outerRef.current;
3122
3373
  const inner = innerRef.current;
3123
3374
  if (!outer || !inner) return;
@@ -3178,7 +3429,7 @@ function OverlayContent({
3178
3429
 
3179
3430
  // src/overlay/FixedSizeContent.tsx
3180
3431
  import { Stack as Stack3 } from "@mui/material";
3181
- import { useEffect as useEffect8, useRef as useRef9 } from "react";
3432
+ import { useEffect as useEffect9, useRef as useRef9 } from "react";
3182
3433
  import { jsx as jsx6 } from "react/jsx-runtime";
3183
3434
  function FixedSizeContent({
3184
3435
  children,
@@ -3189,7 +3440,7 @@ function FixedSizeContent({
3189
3440
  }) {
3190
3441
  const ref = useRef9(null);
3191
3442
  const totalContentHeightRef = useRef9(0);
3192
- useEffect8(() => {
3443
+ useEffect9(() => {
3193
3444
  const el = ref.current;
3194
3445
  if (!el) return;
3195
3446
  let clipAncestor = el.parentElement;
@@ -3252,7 +3503,7 @@ function FixedSizeContent({
3252
3503
 
3253
3504
  // src/overlay/OverlayBadge.tsx
3254
3505
  import { Stack as Stack4 } from "@mui/material";
3255
- import { useEffect as useEffect9, useRef as useRef10 } from "react";
3506
+ import { useEffect as useEffect10, useRef as useRef10 } from "react";
3256
3507
  import { jsx as jsx7 } from "react/jsx-runtime";
3257
3508
  function toPx(v) {
3258
3509
  if (v === void 0) return void 0;
@@ -3297,7 +3548,7 @@ function OverlayBadge({
3297
3548
  }) {
3298
3549
  const ref = useRef10(null);
3299
3550
  const baseSize = useRef10(null);
3300
- useEffect9(() => {
3551
+ useEffect10(() => {
3301
3552
  const el = ref.current;
3302
3553
  if (!el) return;
3303
3554
  const ancestor = el.parentElement;
@@ -3439,11 +3690,16 @@ export {
3439
3690
  snapCursorPoint,
3440
3691
  useCanvasClick,
3441
3692
  useCanvasEvents,
3693
+ useCanvasRef,
3442
3694
  useCanvasTooltip,
3443
3695
  useEditCanvas,
3444
3696
  useEditCanvasContext,
3697
+ useEditCanvasState,
3698
+ useEditCanvasViewport,
3445
3699
  useViewCanvas,
3446
3700
  useViewCanvasContext,
3701
+ useViewCanvasState,
3702
+ useViewCanvasViewport,
3447
3703
  util5 as util
3448
3704
  };
3449
3705
  //# sourceMappingURL=index.js.map