@react-three/fiber 10.0.0-canary.da7a768 → 10.0.0-canary.dc0ec15

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.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as webgpu from 'three/webgpu';
2
- import { CubeRenderTarget, RenderTarget, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, ShaderMaterial, Uniform, SRGBColorSpace, RGBAFormat, UnsignedByteType, Vector3, Vector2, TextureLoader, Texture as Texture$1, CubeReflectionMapping, EquirectangularReflectionMapping, CubeTextureLoader, CanvasTarget, Raycaster, OrthographicCamera, PerspectiveCamera, Scene, PCFShadowMap, VSMShadowMap, BasicShadowMap, ACESFilmicToneMapping, WebGPURenderer, HalfFloatType, Color } from 'three/webgpu';
2
+ import { CubeRenderTarget, RenderTarget, Frustum, Matrix4, Group, BoxGeometry, MeshBasicNodeMaterial, Mesh, Node, NodeUpdateType, Layers, ShaderMaterial, Uniform, SRGBColorSpace, RGBAFormat, UnsignedByteType, TextureLoader, Texture as Texture$1, CubeReflectionMapping, EquirectangularReflectionMapping, CubeTextureLoader, Vector3, Vector2, CanvasTarget, Raycaster, OrthographicCamera, PerspectiveCamera, Scene, PCFShadowMap, VSMShadowMap, BasicShadowMap, ACESFilmicToneMapping, WebGPURenderer, HalfFloatType, Color } from 'three/webgpu';
3
3
  import { WebGLCubeRenderTarget, WebGLRenderTarget, WebGLRenderer } from 'three';
4
4
  import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
5
  import * as React from 'react';
6
- import React__default, { useRef, useMemo, useLayoutEffect, useEffect, useContext, useImperativeHandle, useCallback, useState } from 'react';
6
+ import React__default, { useContext, useRef, useMemo, useLayoutEffect, useEffect, useImperativeHandle, useCallback, useState } from 'react';
7
7
  import useMeasure from 'react-use-measure';
8
8
  import { useFiber, useContextBridge, traverseFiber, FiberProvider } from 'its-fine';
9
9
  import { createWithEqualityFn } from 'zustand/traditional';
@@ -12,6 +12,7 @@ export { Scheduler, getScheduler } from '@pmndrs/scheduler';
12
12
  import Tb, { unstable_getCurrentPriorityLevel, unstable_IdlePriority, unstable_LowPriority, unstable_NormalPriority, unstable_UserBlockingPriority, unstable_ImmediatePriority, unstable_scheduleCallback } from 'scheduler';
13
13
  import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js';
14
14
  import { HDRLoader } from 'three/examples/jsm/loaders/HDRLoader.js';
15
+ import { HDRCubeTextureLoader } from 'three/examples/jsm/loaders/HDRCubeTextureLoader.js';
15
16
  import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
16
17
  import { UltraHDRLoader } from 'three/examples/jsm/loaders/UltraHDRLoader.js';
17
18
  import { GainMapLoader } from '@monogrid/gainmap-js';
@@ -293,7 +294,6 @@ function prepare(target, root, type, props) {
293
294
  }
294
295
  function invalidateInstance(instance) {
295
296
  if (!instance.parent) return;
296
- instance.props.onUpdate?.(instance.object);
297
297
  const state = instance.root?.getState?.();
298
298
  if (state && state.internal.frames === 0) state.invalidate();
299
299
  }
@@ -509,8 +509,10 @@ function hasVisibilityHandlers(handlers) {
509
509
  }
510
510
 
511
511
  const FROM_REF = Symbol.for("@react-three/fiber.fromRef");
512
- function fromRef(ref) {
513
- return { [FROM_REF]: ref };
512
+ function fromRef(ref, transform) {
513
+ const marker = { [FROM_REF]: ref };
514
+ if (transform) marker.transform = transform;
515
+ return marker;
514
516
  }
515
517
  function isFromRef(value) {
516
518
  return value !== null && typeof value === "object" && FROM_REF in value;
@@ -524,6 +526,63 @@ function isOnce(value) {
524
526
  return value !== null && typeof value === "object" && ONCE in value;
525
527
  }
526
528
 
529
+ const shownNotices = /* @__PURE__ */ new Set();
530
+ function notifyDepreciated({ heading, body, link }) {
531
+ if (typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== void 0) && process.env.R3F_SHOW_DEPRECATION_WARNINGS !== "true") {
532
+ return;
533
+ }
534
+ if (shownNotices.has(heading)) return;
535
+ shownNotices.add(heading);
536
+ const caller = getCallerFrame();
537
+ if (caller) {
538
+ console.log();
539
+ }
540
+ const boxStyle = "background: #ff9800; color: #1a1a1a; padding: 8px 12px; border-radius: 4px; font-weight: 500;";
541
+ if (caller) {
542
+ console.log(`%c\u26A0\uFE0F ${heading}`, boxStyle);
543
+ }
544
+ if (body || link) {
545
+ let message = "";
546
+ if (body) message += body;
547
+ if (link) message += (body ? "\n\n" : "") + `More info: ${link}`;
548
+ console.warn(`%c${message}`, "font-weight: bold;");
549
+ }
550
+ }
551
+ function getCallerFrame(depth = 3) {
552
+ const stack = new Error().stack;
553
+ if (!stack) return null;
554
+ const lines = stack.split("\n");
555
+ const frame = lines[depth];
556
+ if (!frame) return null;
557
+ let match = frame.match(/^\s*at (?:(.+?) )?\(?(.+?):(\d+):(\d+)\)?$/);
558
+ if (!match) {
559
+ match = frame.match(/^(?:(.+?)@)?(.+?):(\d+):(\d+)$/);
560
+ }
561
+ if (!match) return null;
562
+ const [, fn, url, line] = match;
563
+ return {
564
+ functionName: fn ?? "<anonymous>",
565
+ location: formatLocation(url, Number(line))
566
+ };
567
+ }
568
+ function formatLocation(url, line) {
569
+ const clean = url.split("?")[0];
570
+ const file = clean.split("/").pop() ?? clean;
571
+ return `${file}:${line}`;
572
+ }
573
+ function notifyAlpha({ message, link }) {
574
+ if (typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== void 0) && process.env.R3F_SHOW_ALPHA_WARNINGS !== "true") {
575
+ return;
576
+ }
577
+ if (shownNotices.has(message)) return;
578
+ shownNotices.add(message);
579
+ const boxStyle = "background: #6366f1; color: #ffffff; padding: 6px 10px; border-radius: 4px; font-weight: 500;";
580
+ console.log(`%c\u{1F52C} ${message}`, boxStyle);
581
+ {
582
+ console.log(`%cMore info: ${link}`, "color: #6366f1; font-weight: normal;");
583
+ }
584
+ }
585
+
527
586
  const RESERVED_PROPS = [
528
587
  "children",
529
588
  "key",
@@ -533,7 +592,6 @@ const RESERVED_PROPS = [
533
592
  "dispose",
534
593
  "attach",
535
594
  "object",
536
- "onUpdate",
537
595
  // Behavior flags
538
596
  "dispose"
539
597
  ];
@@ -611,11 +669,9 @@ function diffProps(instance, newProps) {
611
669
  for (const prop in instance.props) {
612
670
  if (RESERVED_PROPS.includes(prop) || newProps.hasOwnProperty(prop)) continue;
613
671
  const { root, key } = resolve(instance.object, prop);
614
- if (root.constructor && root.constructor.length === 0) {
672
+ if (typeof root?.constructor === "function") {
615
673
  const ctor = getMemoizedPrototype(root);
616
674
  if (!is.und(ctor)) changedProps[prop] = ctor[key];
617
- } else {
618
- changedProps[prop] = 0;
619
675
  }
620
676
  }
621
677
  return changedProps;
@@ -627,6 +683,13 @@ function applyProps(object, props) {
627
683
  for (const prop in props) {
628
684
  const value = props[prop];
629
685
  if (RESERVED_PROPS.includes(prop)) continue;
686
+ if (prop === "onUpdate") {
687
+ notifyDepreciated({
688
+ heading: "onUpdate is no longer an R3F prop",
689
+ body: "v10 removed the onUpdate hook: R3F no longer calls it after prop updates. The value is assigned to the object by name. On a Texture that sets three's Texture.onUpdate, which the renderer calls after a GPU upload, not after prop changes. On a node it replaces the Node.onUpdate() method. On other objects it is never called.\n\nUse an effect keyed on the props that change, read the object in useFrame, or use a ref callback.",
690
+ link: "https://docs.pmnd.rs/react-three-fiber/migration/v10#onupdate-removed"
691
+ });
692
+ }
630
693
  if (instance && EVENT_REGEX.test(prop)) {
631
694
  if (typeof value === "function") instance.handlers[prop] = value;
632
695
  else delete instance.handlers[prop];
@@ -732,682 +795,378 @@ function applyProps(object, props) {
732
795
  return object;
733
796
  }
734
797
 
735
- const shownNotices = /* @__PURE__ */ new Set();
736
- function notifyDepreciated({ heading, body, link }) {
737
- if (typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== void 0) && process.env.R3F_SHOW_DEPRECATION_WARNINGS !== "true") {
738
- return;
739
- }
740
- if (shownNotices.has(heading)) return;
741
- shownNotices.add(heading);
742
- const caller = getCallerFrame();
743
- if (caller) {
744
- console.log();
745
- }
746
- const boxStyle = "background: #ff9800; color: #1a1a1a; padding: 8px 12px; border-radius: 4px; font-weight: 500;";
747
- if (caller) {
748
- console.log(`%c\u26A0\uFE0F ${heading}`, boxStyle);
749
- }
750
- if (body || link) {
751
- let message = "";
752
- if (body) message += body;
753
- if (link) message += (body ? "\n\n" : "") + `More info: ${link}`;
754
- console.warn(`%c${message}`, "font-weight: bold;");
755
- }
756
- }
757
- function getCallerFrame(depth = 3) {
758
- const stack = new Error().stack;
759
- if (!stack) return null;
760
- const lines = stack.split("\n");
761
- const frame = lines[depth];
762
- if (!frame) return null;
763
- let match = frame.match(/^\s*at (?:(.+?) )?\(?(.+?):(\d+):(\d+)\)?$/);
764
- if (!match) {
765
- match = frame.match(/^(?:(.+?)@)?(.+?):(\d+):(\d+)$/);
766
- }
767
- if (!match) return null;
768
- const [, fn, url, line] = match;
769
- return {
770
- functionName: fn ?? "<anonymous>",
771
- location: formatLocation(url, Number(line))
772
- };
798
+ const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
799
+ const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React.createContext(null));
800
+
801
+ function useStore() {
802
+ const store = useContext(context);
803
+ if (!store) throw new Error("R3F: Hooks can only be used within the Canvas component!");
804
+ return store;
773
805
  }
774
- function formatLocation(url, line) {
775
- const clean = url.split("?")[0];
776
- const file = clean.split("/").pop() ?? clean;
777
- return `${file}:${line}`;
806
+ function useThree(selector = (state) => state, equalityFn) {
807
+ return useStore()(selector, equalityFn);
778
808
  }
779
- function notifyAlpha({ message, link }) {
780
- if (typeof process !== "undefined" && (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== void 0) && process.env.R3F_SHOW_ALPHA_WARNINGS !== "true") {
781
- return;
782
- }
783
- if (shownNotices.has(message)) return;
784
- shownNotices.add(message);
785
- const boxStyle = "background: #6366f1; color: #ffffff; padding: 6px 10px; border-radius: 4px; font-weight: 500;";
786
- console.log(`%c\u{1F52C} ${message}`, boxStyle);
787
- {
788
- console.log(`%cMore info: ${link}`, "color: #6366f1; font-weight: normal;");
809
+
810
+ const memoizedLoaders = /* @__PURE__ */ new WeakMap();
811
+ const isConstructor$1 = (value) => typeof value === "function" && value?.prototype?.constructor === value;
812
+ function getLoader$1(Proto) {
813
+ if (isConstructor$1(Proto)) {
814
+ let loader = memoizedLoaders.get(Proto);
815
+ if (!loader) {
816
+ loader = new Proto();
817
+ memoizedLoaders.set(Proto, loader);
818
+ }
819
+ return loader;
789
820
  }
821
+ return Proto;
790
822
  }
791
-
792
- const INTERNAL_LIB_MARKERS = ["zustand", "setState", "Object.assign"];
793
- function isInternalRendererAccess(stack, callerDepth = 2) {
794
- if (!stack) return false;
795
- const caller = stack.split("\n")[callerDepth] ?? "";
796
- return INTERNAL_LIB_MARKERS.some((m) => caller.includes(m));
823
+ function loadingFn(extensions, onProgress) {
824
+ return function(Proto, input) {
825
+ const loader = getLoader$1(Proto);
826
+ if (extensions) extensions(loader);
827
+ if ("loadAsync" in loader && typeof loader.loadAsync === "function") {
828
+ return loader.loadAsync(input, onProgress).then((data) => {
829
+ if (isObject3D(data?.scene)) Object.assign(data, buildGraph(data.scene));
830
+ return data;
831
+ });
832
+ }
833
+ return new Promise(
834
+ (res, reject) => loader.load(
835
+ input,
836
+ (data) => {
837
+ if (isObject3D(data?.scene)) Object.assign(data, buildGraph(data.scene));
838
+ res(data);
839
+ },
840
+ onProgress,
841
+ (error) => reject(new Error(`Could not load ${input}: ${error?.message}`))
842
+ )
843
+ );
844
+ };
845
+ }
846
+ function useLoader(loader, input, extensions, onProgress, cacheKey) {
847
+ const inputs = Array.isArray(input) ? input : [input];
848
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
849
+ const fn = loadingFn(extensions, onProgress);
850
+ const results = cacheKeys.map(
851
+ (key, index) => suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
852
+ );
853
+ const stableRef = useRef(null);
854
+ const previous = stableRef.current;
855
+ const stable = previous !== null && previous.length === results.length && previous.every((value, i) => value === results[i]) ? previous : results;
856
+ stableRef.current = stable;
857
+ return Array.isArray(input) ? stable : stable[0];
797
858
  }
859
+ useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
860
+ const inputs = Array.isArray(input) ? input : [input];
861
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
862
+ cacheKeys.forEach(
863
+ (key, index) => preload(() => loadingFn(extensions, onProgress)(loader, inputs[index]), [loader, key])
864
+ );
865
+ };
866
+ useLoader.clear = function(loader, input, cacheKey) {
867
+ const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
868
+ cacheKeys.forEach((key) => clear([loader, key]));
869
+ };
870
+ useLoader.loader = getLoader$1;
798
871
 
799
- const R3F_CONTEXT = Symbol.for("@react-three/fiber.context");
800
- const context = globalThis[R3F_CONTEXT] ?? (globalThis[R3F_CONTEXT] = React.createContext(null));
801
- const createStore = (invalidate, advance) => {
802
- const rootStore = createWithEqualityFn((set, get) => {
803
- const position = new Vector3();
804
- const defaultTarget = new Vector3();
805
- const tempTarget = new Vector3();
806
- function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
807
- const { width, height, top, left } = size;
808
- const aspect = width / height;
809
- if (target.isVector3) tempTarget.copy(target);
810
- else tempTarget.set(...target);
811
- const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
812
- if (isOrthographicCamera(camera)) {
813
- return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect };
814
- } else {
815
- const fov = camera.fov * Math.PI / 180;
816
- const h = 2 * Math.tan(fov / 2) * distance;
817
- const w = h * (width / height);
818
- return { width: w, height: h, top, left, factor: width / w, distance, aspect };
872
+ function useFrame(callback, priorityOrOptions) {
873
+ const store = React.useContext(context);
874
+ const isInsideCanvas = store !== null;
875
+ const scheduler = getScheduler();
876
+ const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
877
+ id: priorityOrOptions.id,
878
+ phase: priorityOrOptions.phase,
879
+ priority: priorityOrOptions.priority,
880
+ fps: priorityOrOptions.fps,
881
+ drop: priorityOrOptions.drop,
882
+ enabled: priorityOrOptions.enabled,
883
+ before: priorityOrOptions.before,
884
+ after: priorityOrOptions.after
885
+ }) : "";
886
+ const options = React.useMemo(() => {
887
+ return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
888
+ }, [optionsKey]);
889
+ const reactId = React.useId();
890
+ const id = options.id ?? reactId;
891
+ const callbackRef = useMutableCallback(callback);
892
+ const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
893
+ useIsomorphicLayoutEffect(() => {
894
+ if (!callback) return;
895
+ if (isInsideCanvas) {
896
+ const state = store.getState();
897
+ const rootId = state.internal.rootId;
898
+ if (isLegacyPriority) {
899
+ state.internal.priority++;
900
+ let parentRoot = state.previousRoot;
901
+ while (parentRoot) {
902
+ const parentState = parentRoot.getState();
903
+ if (parentState?.internal) parentState.internal.priority++;
904
+ parentRoot = parentState?.previousRoot;
905
+ }
906
+ notifyDepreciated({
907
+ heading: "useFrame with numeric priority is deprecated",
908
+ body: 'Using useFrame(callback, number) to control render order is deprecated.\n\nFor custom rendering, use: useFrame(callback, { phase: "render" })\nFor execution order within update phase, use: useFrame(callback, { priority: number })',
909
+ link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
910
+ });
819
911
  }
820
- }
821
- let performanceTimeout = void 0;
822
- const setPerformanceCurrent = (current) => set((state2) => ({ performance: { ...state2.performance, current } }));
823
- const pointer = new Vector2();
824
- const rootState = {
825
- set,
826
- get,
827
- // Mock objects that have to be configured
828
- // primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
829
- primaryStore: null,
830
- gl: null,
831
- renderer: null,
832
- camera: null,
833
- frustum: new Frustum(),
834
- autoUpdateFrustum: true,
835
- raycaster: null,
836
- events: {
837
- priority: 1,
838
- enabled: true,
839
- connected: false,
840
- frameTimedRaycasts: true,
841
- alwaysFireOnScroll: true,
842
- updateOnFrame: false
843
- },
844
- scene: null,
845
- rootScene: null,
846
- xr: null,
847
- inspector: null,
848
- invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
849
- advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
850
- textureColorSpace: SRGBColorSpace,
851
- isLegacy: false,
852
- webGPUSupported: false,
853
- isNative: false,
854
- controls: null,
855
- pointer,
856
- mouse: pointer,
857
- frameloop: "always",
858
- onPointerMissed: void 0,
859
- onDragOverMissed: void 0,
860
- onDropMissed: void 0,
861
- performance: {
862
- current: 1,
863
- min: 0.5,
864
- max: 1,
865
- debounce: 200,
866
- regress: () => {
867
- const state2 = get();
868
- if (performanceTimeout) clearTimeout(performanceTimeout);
869
- if (state2.performance.current !== state2.performance.min) setPerformanceCurrent(state2.performance.min);
870
- performanceTimeout = setTimeout(
871
- () => setPerformanceCurrent(get().performance.max),
872
- state2.performance.debounce
873
- );
874
- }
875
- },
876
- size: { width: 0, height: 0, top: 0, left: 0 },
877
- viewport: {
878
- initialDpr: 0,
879
- dpr: 0,
880
- width: 0,
881
- height: 0,
882
- top: 0,
883
- left: 0,
884
- aspect: 0,
885
- distance: 0,
886
- factor: 0,
887
- getCurrentViewport
888
- },
889
- setEvents: (events) => set((state2) => ({ ...state2, events: { ...state2.events, ...events } })),
890
- setSize: (width, height, top, left) => {
891
- const state2 = get();
892
- if (width === void 0) {
893
- set({ _sizeImperative: false });
894
- if (state2._sizeProps) {
895
- const { width: propW, height: propH } = state2._sizeProps;
896
- if (propW !== void 0 || propH !== void 0) {
897
- const currentSize = state2.size;
898
- const newSize = {
899
- width: propW ?? currentSize.width,
900
- height: propH ?? currentSize.height,
901
- top: currentSize.top,
902
- left: currentSize.left
903
- };
904
- set((s) => ({
905
- size: newSize,
906
- viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
907
- }));
908
- getScheduler().invalidate();
912
+ const wrappedCallback = (frameState, delta) => {
913
+ const localState = store.getState();
914
+ const mergedState = {
915
+ ...localState,
916
+ time: frameState.time,
917
+ delta: frameState.delta,
918
+ elapsed: frameState.elapsed,
919
+ frame: frameState.frame
920
+ };
921
+ callbackRef.current?.(mergedState, delta);
922
+ };
923
+ const unregister = scheduler.register(wrappedCallback, {
924
+ id,
925
+ rootId,
926
+ ...options
927
+ });
928
+ return () => {
929
+ unregister();
930
+ if (isLegacyPriority) {
931
+ const currentState = store.getState();
932
+ if (currentState.internal) {
933
+ currentState.internal.priority--;
934
+ let parentRoot = currentState.previousRoot;
935
+ while (parentRoot) {
936
+ const parentState = parentRoot.getState();
937
+ if (parentState?.internal) parentState.internal.priority--;
938
+ parentRoot = parentState?.previousRoot;
909
939
  }
910
940
  }
911
- return;
912
941
  }
913
- const w = width;
914
- const h = height ?? width;
915
- const t = top ?? state2.size.top;
916
- const l = left ?? state2.size.left;
917
- const size = { width: w, height: h, top: t, left: l };
918
- set((s) => ({
919
- size,
920
- viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
921
- _sizeImperative: true
922
- }));
923
- getScheduler().invalidate();
942
+ };
943
+ } else {
944
+ return scheduler.register(
945
+ (state, delta) => {
946
+ const frameState = state;
947
+ if (!frameState.renderer) return;
948
+ callbackRef.current?.(frameState, delta);
949
+ },
950
+ { id, ...options }
951
+ );
952
+ }
953
+ }, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
954
+ const isPaused = React.useSyncExternalStore(
955
+ // Subscribe function
956
+ React.useCallback(
957
+ (onStoreChange) => {
958
+ return getScheduler().subscribeJobState(id, onStoreChange);
924
959
  },
925
- setDpr: (dpr) => set((state2) => {
926
- const resolved = calculateDpr(dpr);
927
- return { viewport: { ...state2.viewport, dpr: resolved, initialDpr: state2.viewport.initialDpr || resolved } };
928
- }),
929
- setFrameloop: (frameloop = "always") => {
930
- set(() => ({ frameloop }));
931
- getScheduler().frameloop = frameloop;
960
+ [id]
961
+ ),
962
+ // getSnapshot function
963
+ React.useCallback(() => getScheduler().isJobPaused(id), [id]),
964
+ // getServerSnapshot function (SSR)
965
+ React.useCallback(() => false, [])
966
+ );
967
+ const controls = React.useMemo(() => {
968
+ const scheduler2 = getScheduler();
969
+ return {
970
+ /** The job's unique ID */
971
+ id,
972
+ /**
973
+ * Access to the global scheduler for frame loop control.
974
+ * Use for controlling the entire frame loop, adding phases, etc.
975
+ */
976
+ scheduler: scheduler2,
977
+ /**
978
+ * The root that currently owns this job. Resolve on access because ambient
979
+ * jobs can be adopted by a Canvas after the controls object is created.
980
+ */
981
+ get rootId() {
982
+ return getScheduler().getJobRootId(id);
932
983
  },
933
- setError: (error) => set(() => ({ error })),
934
- error: null,
935
- //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
936
- uniforms: {},
937
- nodes: {},
938
- buffers: {},
939
- gpuStorage: {},
940
- textures: /* @__PURE__ */ new Map(),
941
- _textureRefs: /* @__PURE__ */ new Map(),
942
- renderPipeline: null,
943
- passes: {},
944
- _hmrVersion: 0,
945
- _sizeImperative: false,
946
- _sizeProps: null,
947
- previousRoot: void 0,
948
- internal: {
949
- // Events
950
- interaction: [],
951
- subscribers: [],
952
- // Per-pointer state (new unified structure)
953
- pointerMap: /* @__PURE__ */ new Map(),
954
- pointerDirty: /* @__PURE__ */ new Map(),
955
- lastEvent: React.createRef(),
956
- // Deprecated but kept for backwards compatibility
957
- hovered: /* @__PURE__ */ new Map(),
958
- initialClick: [0, 0],
959
- initialHits: [],
960
- capturedMap: /* @__PURE__ */ new Map(),
961
- // Visibility tracking (onFramed, onOccluded, onVisible)
962
- visibilityRegistry: /* @__PURE__ */ new Map(),
963
- // Occlusion system (WebGPU only)
964
- occlusionEnabled: false,
965
- occlusionObserver: null,
966
- occlusionCache: /* @__PURE__ */ new Map(),
967
- helperGroup: null,
968
- // Updates
969
- active: false,
970
- frames: 0,
971
- priority: 0,
972
- subscribe: (ref, priority, store) => {
973
- const internal = get().internal;
974
- internal.priority = internal.priority + (priority > 0 ? 1 : 0);
975
- internal.subscribers.push({ ref, priority, store });
976
- internal.subscribers = internal.subscribers.sort((a, b) => a.priority - b.priority);
977
- return () => {
978
- const internal2 = get().internal;
979
- if (internal2?.subscribers) {
980
- internal2.priority = internal2.priority - (priority > 0 ? 1 : 0);
981
- internal2.subscribers = internal2.subscribers.filter((s) => s.ref !== ref);
982
- }
983
- };
984
- },
985
- // Renderer Storage (single source of truth)
986
- actualRenderer: null,
987
- // Scheduler for useFrameNext (initialized in renderer.tsx)
988
- scheduler: null
989
- }
984
+ /**
985
+ * Manually step this job only.
986
+ * Bypasses FPS limiting - always runs.
987
+ * @param timestamp Optional timestamp (defaults to performance.now())
988
+ */
989
+ step: (timestamp) => {
990
+ getScheduler().stepJob(id, timestamp);
991
+ },
992
+ /**
993
+ * Manually step ALL jobs in the scheduler.
994
+ * Useful for frameloop='never' mode.
995
+ * @param timestamp Optional timestamp (defaults to performance.now())
996
+ */
997
+ stepAll: (timestamp) => {
998
+ getScheduler().step(timestamp);
999
+ },
1000
+ /**
1001
+ * Request frames for this job's owning root without waking sibling roots.
1002
+ */
1003
+ invalidate: (frames, stackFrames) => {
1004
+ const scheduler3 = getScheduler();
1005
+ const rootId = scheduler3.getJobRootId(id);
1006
+ if (rootId) scheduler3.invalidateRoot(rootId, frames, stackFrames);
1007
+ },
1008
+ /**
1009
+ * Pause this job (set enabled=false).
1010
+ * Job remains registered but won't run.
1011
+ */
1012
+ pause: () => {
1013
+ getScheduler().pauseJob(id);
1014
+ },
1015
+ /**
1016
+ * Resume this job (set enabled=true).
1017
+ */
1018
+ resume: () => {
1019
+ getScheduler().resumeJob(id);
1020
+ },
1021
+ /**
1022
+ * Reactive paused state - automatically updates when pause/resume is called.
1023
+ * No need for forceUpdate() in your components.
1024
+ */
1025
+ isPaused
990
1026
  };
991
- return rootState;
992
- });
993
- const state = rootStore.getState();
994
- Object.defineProperty(state, "gl", {
995
- get() {
996
- const currentState = rootStore.getState();
997
- if (!currentState.isLegacy && currentState.internal.actualRenderer) {
998
- const stack = new Error().stack || "";
999
- if (!isInternalRendererAccess(stack)) {
1000
- const cleanedStack = stack.split("\n").slice(2).join("\n") || "Stack trace unavailable";
1001
- notifyDepreciated({
1002
- heading: "Accessing state.gl in WebGPU mode",
1003
- body: "Please use state.renderer instead. state.gl is deprecated and will be removed in future versions.\n\nFor backwards compatibility, state.gl currently maps to state.renderer, but this may cause issues with libraries expecting WebGLRenderer.\n\nAccessed from:\n" + cleanedStack
1004
- });
1005
- }
1006
- }
1007
- return currentState.internal.actualRenderer;
1008
- },
1009
- set(value) {
1010
- rootStore.getState().internal.actualRenderer = value;
1011
- },
1012
- enumerable: true,
1013
- configurable: true
1014
- });
1015
- Object.defineProperty(state, "renderer", {
1016
- get() {
1017
- return rootStore.getState().internal.actualRenderer;
1018
- },
1019
- set(value) {
1020
- rootStore.getState().internal.actualRenderer = value;
1021
- },
1022
- enumerable: true,
1023
- configurable: true
1024
- });
1025
- let oldScene = state.scene;
1026
- rootStore.subscribe(() => {
1027
- const currentState = rootStore.getState();
1028
- const { scene, rootScene, set } = currentState;
1029
- if (scene !== oldScene) {
1030
- oldScene = scene;
1031
- if (scene?.isScene && scene !== rootScene) {
1032
- set({ rootScene: scene });
1033
- }
1034
- }
1035
- });
1036
- let oldSize = state.size;
1037
- let oldDpr = state.viewport.dpr;
1038
- let oldCamera = state.camera;
1039
- rootStore.subscribe(() => {
1040
- const { camera, size, viewport, set, internal } = rootStore.getState();
1041
- const actualRenderer = internal.actualRenderer;
1042
- const canvasTarget = internal.canvasTarget;
1043
- if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
1044
- oldSize = size;
1045
- oldDpr = viewport.dpr;
1046
- updateCamera(camera, size);
1047
- if (internal.isSecondary && canvasTarget) {
1048
- if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1049
- canvasTarget.setSize(size.width, size.height, false);
1050
- } else {
1051
- if (viewport.dpr > 0) actualRenderer.setPixelRatio(viewport.dpr);
1052
- actualRenderer.setSize(size.width, size.height, false);
1053
- if (canvasTarget) {
1054
- if (viewport.dpr > 0) canvasTarget.setPixelRatio(viewport.dpr);
1055
- canvasTarget.setSize(size.width, size.height, false);
1056
- }
1057
- }
1058
- }
1059
- if (camera !== oldCamera) {
1060
- oldCamera = camera;
1061
- const { rootScene } = rootStore.getState();
1062
- if (camera && rootScene && !camera.parent) {
1063
- rootScene.add(camera);
1064
- }
1065
- set((state2) => ({ viewport: { ...state2.viewport, ...state2.viewport.getCurrentViewport(camera) } }));
1066
- const currentState = rootStore.getState();
1067
- if (currentState.autoUpdateFrustum && camera) {
1068
- updateFrustum(camera, currentState.frustum);
1069
- }
1070
- }
1071
- });
1072
- rootStore.subscribe((state2) => invalidate(state2));
1073
- return rootStore;
1074
- };
1027
+ }, [id, isPaused]);
1028
+ return controls;
1029
+ }
1075
1030
 
1076
- const memoizedLoaders = /* @__PURE__ */ new WeakMap();
1077
- const isConstructor$1 = (value) => typeof value === "function" && value?.prototype?.constructor === value;
1078
- function getLoader$1(Proto) {
1079
- if (isConstructor$1(Proto)) {
1080
- let loader = memoizedLoaders.get(Proto);
1081
- if (!loader) {
1082
- loader = new Proto();
1083
- memoizedLoaders.set(Proto, loader);
1031
+ const IsObject = (url) => url === Object(url) && !Array.isArray(url) && typeof url !== "function";
1032
+ function getUrls(input) {
1033
+ if (typeof input === "string") return [input];
1034
+ if (Array.isArray(input)) return input;
1035
+ return Object.values(input);
1036
+ }
1037
+ function inputSignature(input) {
1038
+ if (typeof input === "string") return input;
1039
+ if (Array.isArray(input)) return input.join("\0");
1040
+ return Object.keys(input).sort().map((key) => `${key}\0${input[key]}`).join("");
1041
+ }
1042
+ function allUrlsCached(urls, textureCache) {
1043
+ return urls.every((url) => textureCache.has(url));
1044
+ }
1045
+ function buildFromCache(input, textureCache) {
1046
+ if (typeof input === "string") {
1047
+ return textureCache.get(input);
1048
+ } else if (Array.isArray(input)) {
1049
+ return input.map((url) => textureCache.get(url));
1050
+ } else {
1051
+ const result = {};
1052
+ const objInput = input;
1053
+ for (const key in objInput) {
1054
+ result[key] = textureCache.get(objInput[key]);
1084
1055
  }
1085
- return loader;
1056
+ return result;
1086
1057
  }
1087
- return Proto;
1088
1058
  }
1089
- function loadingFn(extensions, onProgress) {
1090
- return function(Proto, input) {
1091
- const loader = getLoader$1(Proto);
1092
- if (extensions) extensions(loader);
1093
- if ("loadAsync" in loader && typeof loader.loadAsync === "function") {
1094
- return loader.loadAsync(input, onProgress).then((data) => {
1095
- if (isObject3D(data?.scene)) Object.assign(data, buildGraph(data.scene));
1096
- return data;
1059
+ function useTexture(input, optionsOrOnLoad) {
1060
+ const renderer = useThree((state) => state.internal.actualRenderer);
1061
+ const store = useStore();
1062
+ const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
1063
+ const { onLoad, cache = true } = options;
1064
+ const onLoadRef = useRef(onLoad);
1065
+ onLoadRef.current = onLoad;
1066
+ const onLoadCalledForRef = useRef(null);
1067
+ const inputKey = useMemo(() => inputSignature(input), [input]);
1068
+ const stableInput = useMemo(() => input, [inputKey]);
1069
+ const urls = useMemo(() => getUrls(stableInput), [stableInput]);
1070
+ const cachedResult = useMemo(() => {
1071
+ if (!cache) return null;
1072
+ const textures = store.getState().textures;
1073
+ if (!allUrlsCached(urls, textures)) return null;
1074
+ return buildFromCache(stableInput, textures);
1075
+ }, [cache, urls, stableInput, store]);
1076
+ const loadedTextures = useLoader(
1077
+ TextureLoader,
1078
+ IsObject(stableInput) ? Object.values(stableInput) : stableInput
1079
+ );
1080
+ const mappedTextures = useMemo(() => {
1081
+ if (cachedResult) return cachedResult;
1082
+ if (IsObject(stableInput)) {
1083
+ const keyed = {};
1084
+ const textureArray = loadedTextures;
1085
+ let i = 0;
1086
+ for (const key in stableInput) keyed[key] = textureArray[i++];
1087
+ return keyed;
1088
+ } else {
1089
+ return loadedTextures;
1090
+ }
1091
+ }, [stableInput, loadedTextures, cachedResult]);
1092
+ useLayoutEffect(() => {
1093
+ if (cachedResult) return;
1094
+ if (onLoadCalledForRef.current === inputKey) return;
1095
+ onLoadCalledForRef.current = inputKey;
1096
+ onLoadRef.current?.(mappedTextures);
1097
+ }, [cachedResult, mappedTextures, inputKey]);
1098
+ useEffect(() => {
1099
+ if (cachedResult) return;
1100
+ if ("initTexture" in renderer) {
1101
+ let textureArray = [];
1102
+ if (Array.isArray(loadedTextures)) {
1103
+ textureArray = loadedTextures;
1104
+ } else if (loadedTextures instanceof Texture$1) {
1105
+ textureArray = [loadedTextures];
1106
+ } else if (IsObject(loadedTextures)) {
1107
+ textureArray = Object.values(loadedTextures);
1108
+ }
1109
+ textureArray.forEach((texture) => {
1110
+ if (texture instanceof Texture$1) {
1111
+ renderer.initTexture(texture);
1112
+ }
1097
1113
  });
1098
1114
  }
1099
- return new Promise(
1100
- (res, reject) => loader.load(
1101
- input,
1102
- (data) => {
1103
- if (isObject3D(data?.scene)) Object.assign(data, buildGraph(data.scene));
1104
- res(data);
1105
- },
1106
- onProgress,
1107
- (error) => reject(new Error(`Could not load ${input}: ${error?.message}`))
1108
- )
1109
- );
1110
- };
1111
- }
1112
- function useLoader(loader, input, extensions, onProgress, cacheKey) {
1113
- const inputs = Array.isArray(input) ? input : [input];
1114
- const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
1115
- const fn = loadingFn(extensions, onProgress);
1116
- const results = cacheKeys.map(
1117
- (key, index) => suspend(() => fn(loader, inputs[index]), [loader, key], { equal: is.equ })
1118
- );
1119
- return Array.isArray(input) ? results : results[0];
1115
+ }, [renderer, loadedTextures, cachedResult]);
1116
+ useEffect(() => {
1117
+ if (!cache) return;
1118
+ const urlTextureMap = [];
1119
+ if (typeof stableInput === "string") {
1120
+ urlTextureMap.push([stableInput, mappedTextures]);
1121
+ } else if (Array.isArray(stableInput)) {
1122
+ const textureArray = mappedTextures;
1123
+ stableInput.forEach((url, i) => urlTextureMap.push([url, textureArray[i]]));
1124
+ } else if (IsObject(stableInput)) {
1125
+ const textureRecord = mappedTextures;
1126
+ for (const key in stableInput) {
1127
+ const url = stableInput[key];
1128
+ urlTextureMap.push([url, textureRecord[key]]);
1129
+ }
1130
+ }
1131
+ store.setState((state) => {
1132
+ const refs = new Map(state._textureRefs);
1133
+ let textures = state.textures;
1134
+ let added = false;
1135
+ for (const [url, texture] of urlTextureMap) {
1136
+ if (!textures.has(url)) {
1137
+ if (!added) {
1138
+ textures = new Map(textures);
1139
+ added = true;
1140
+ }
1141
+ textures.set(url, texture);
1142
+ }
1143
+ refs.set(url, (refs.get(url) ?? 0) + 1);
1144
+ }
1145
+ return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
1146
+ });
1147
+ return () => store.setState((state) => {
1148
+ const refs = new Map(state._textureRefs);
1149
+ for (const [url] of urlTextureMap) {
1150
+ const next = (refs.get(url) ?? 0) - 1;
1151
+ if (next <= 0) refs.delete(url);
1152
+ else refs.set(url, next);
1153
+ }
1154
+ return { _textureRefs: refs };
1155
+ });
1156
+ }, [cache, stableInput, mappedTextures, store]);
1157
+ return mappedTextures;
1120
1158
  }
1121
- useLoader.preload = function(loader, input, extensions, onProgress, cacheKey) {
1122
- const inputs = Array.isArray(input) ? input : [input];
1123
- const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
1124
- cacheKeys.forEach(
1125
- (key, index) => preload(() => loadingFn(extensions, onProgress)(loader, inputs[index]), [loader, key])
1126
- );
1127
- };
1128
- useLoader.clear = function(loader, input, cacheKey) {
1129
- const cacheKeys = Array.isArray(cacheKey ?? input) ? cacheKey ?? input : [cacheKey ?? input];
1130
- cacheKeys.forEach((key) => clear([loader, key]));
1131
- };
1132
- useLoader.loader = getLoader$1;
1133
-
1134
- function useFrame(callback, priorityOrOptions) {
1135
- const store = React.useContext(context);
1136
- const isInsideCanvas = store !== null;
1137
- const scheduler = getScheduler();
1138
- const optionsKey = typeof priorityOrOptions === "number" ? `p:${priorityOrOptions}` : priorityOrOptions ? JSON.stringify({
1139
- id: priorityOrOptions.id,
1140
- phase: priorityOrOptions.phase,
1141
- priority: priorityOrOptions.priority,
1142
- fps: priorityOrOptions.fps,
1143
- drop: priorityOrOptions.drop,
1144
- enabled: priorityOrOptions.enabled,
1145
- before: priorityOrOptions.before,
1146
- after: priorityOrOptions.after
1147
- }) : "";
1148
- const options = React.useMemo(() => {
1149
- return typeof priorityOrOptions === "number" ? { priority: priorityOrOptions } : priorityOrOptions ?? {};
1150
- }, [optionsKey]);
1151
- const reactId = React.useId();
1152
- const id = options.id ?? reactId;
1153
- const callbackRef = useMutableCallback(callback);
1154
- const isLegacyPriority = typeof priorityOrOptions === "number" && priorityOrOptions > 0;
1155
- useIsomorphicLayoutEffect(() => {
1156
- if (!callback) return;
1157
- if (isInsideCanvas) {
1158
- const state = store.getState();
1159
- const rootId = state.internal.rootId;
1160
- if (isLegacyPriority) {
1161
- state.internal.priority++;
1162
- let parentRoot = state.previousRoot;
1163
- while (parentRoot) {
1164
- const parentState = parentRoot.getState();
1165
- if (parentState?.internal) parentState.internal.priority++;
1166
- parentRoot = parentState?.previousRoot;
1167
- }
1168
- notifyDepreciated({
1169
- heading: "useFrame with numeric priority is deprecated",
1170
- body: 'Using useFrame(callback, number) to control render order is deprecated.\n\nFor custom rendering, use: useFrame(callback, { phase: "render" })\nFor execution order within update phase, use: useFrame(callback, { priority: number })',
1171
- link: "https://docs.pmnd.rs/react-three-fiber/api/hooks#useframe"
1172
- });
1173
- }
1174
- const wrappedCallback = (frameState, delta) => {
1175
- const localState = store.getState();
1176
- const mergedState = {
1177
- ...localState,
1178
- time: frameState.time,
1179
- delta: frameState.delta,
1180
- elapsed: frameState.elapsed,
1181
- frame: frameState.frame
1182
- };
1183
- callbackRef.current?.(mergedState, delta);
1184
- };
1185
- const unregister = scheduler.register(wrappedCallback, {
1186
- id,
1187
- rootId,
1188
- ...options
1189
- });
1190
- return () => {
1191
- unregister();
1192
- if (isLegacyPriority) {
1193
- const currentState = store.getState();
1194
- if (currentState.internal) {
1195
- currentState.internal.priority--;
1196
- let parentRoot = currentState.previousRoot;
1197
- while (parentRoot) {
1198
- const parentState = parentRoot.getState();
1199
- if (parentState?.internal) parentState.internal.priority--;
1200
- parentRoot = parentState?.previousRoot;
1201
- }
1202
- }
1203
- }
1204
- };
1205
- } else {
1206
- return scheduler.register(
1207
- (state, delta) => {
1208
- const frameState = state;
1209
- if (!frameState.renderer) return;
1210
- callbackRef.current?.(frameState, delta);
1211
- },
1212
- { id, ...options }
1213
- );
1214
- }
1215
- }, [store, scheduler, id, optionsKey, isLegacyPriority, isInsideCanvas]);
1216
- const isPaused = React.useSyncExternalStore(
1217
- // Subscribe function
1218
- React.useCallback(
1219
- (onStoreChange) => {
1220
- return getScheduler().subscribeJobState(id, onStoreChange);
1221
- },
1222
- [id]
1223
- ),
1224
- // getSnapshot function
1225
- React.useCallback(() => getScheduler().isJobPaused(id), [id]),
1226
- // getServerSnapshot function (SSR)
1227
- React.useCallback(() => false, [])
1228
- );
1229
- const controls = React.useMemo(() => {
1230
- const scheduler2 = getScheduler();
1231
- return {
1232
- /** The job's unique ID */
1233
- id,
1234
- /**
1235
- * Access to the global scheduler for frame loop control.
1236
- * Use for controlling the entire frame loop, adding phases, etc.
1237
- */
1238
- scheduler: scheduler2,
1239
- /**
1240
- * Manually step this job only.
1241
- * Bypasses FPS limiting - always runs.
1242
- * @param timestamp Optional timestamp (defaults to performance.now())
1243
- */
1244
- step: (timestamp) => {
1245
- getScheduler().stepJob(id, timestamp);
1246
- },
1247
- /**
1248
- * Manually step ALL jobs in the scheduler.
1249
- * Useful for frameloop='never' mode.
1250
- * @param timestamp Optional timestamp (defaults to performance.now())
1251
- */
1252
- stepAll: (timestamp) => {
1253
- getScheduler().step(timestamp);
1254
- },
1255
- /**
1256
- * Pause this job (set enabled=false).
1257
- * Job remains registered but won't run.
1258
- */
1259
- pause: () => {
1260
- getScheduler().pauseJob(id);
1261
- },
1262
- /**
1263
- * Resume this job (set enabled=true).
1264
- */
1265
- resume: () => {
1266
- getScheduler().resumeJob(id);
1267
- },
1268
- /**
1269
- * Reactive paused state - automatically updates when pause/resume is called.
1270
- * No need for forceUpdate() in your components.
1271
- */
1272
- isPaused
1273
- };
1274
- }, [id, isPaused]);
1275
- return controls;
1276
- }
1277
-
1278
- const IsObject = (url) => url === Object(url) && !Array.isArray(url) && typeof url !== "function";
1279
- function getUrls(input) {
1280
- if (typeof input === "string") return [input];
1281
- if (Array.isArray(input)) return input;
1282
- return Object.values(input);
1283
- }
1284
- function allUrlsCached(urls, textureCache) {
1285
- return urls.every((url) => textureCache.has(url));
1286
- }
1287
- function buildFromCache(input, textureCache) {
1288
- if (typeof input === "string") {
1289
- return textureCache.get(input);
1290
- } else if (Array.isArray(input)) {
1291
- return input.map((url) => textureCache.get(url));
1292
- } else {
1293
- const result = {};
1294
- const objInput = input;
1295
- for (const key in objInput) {
1296
- result[key] = textureCache.get(objInput[key]);
1297
- }
1298
- return result;
1299
- }
1300
- }
1301
- function useTexture(input, optionsOrOnLoad) {
1302
- const renderer = useThree((state) => state.internal.actualRenderer);
1303
- const store = useStore();
1304
- const options = typeof optionsOrOnLoad === "function" ? { onLoad: optionsOrOnLoad } : optionsOrOnLoad ?? {};
1305
- const { onLoad, cache = true } = options;
1306
- const onLoadRef = useRef(onLoad);
1307
- onLoadRef.current = onLoad;
1308
- const onLoadCalledForRef = useRef(null);
1309
- const urls = useMemo(() => getUrls(input), [input]);
1310
- const cachedResult = useMemo(() => {
1311
- if (!cache) return null;
1312
- const textures = store.getState().textures;
1313
- if (!allUrlsCached(urls, textures)) return null;
1314
- return buildFromCache(input, textures);
1315
- }, [cache, urls, input, store]);
1316
- const loadedTextures = useLoader(
1317
- TextureLoader,
1318
- IsObject(input) ? Object.values(input) : input
1319
- );
1320
- const inputKey = urls.join("\0");
1321
- useLayoutEffect(() => {
1322
- if (cachedResult) return;
1323
- if (onLoadCalledForRef.current === inputKey) return;
1324
- onLoadCalledForRef.current = inputKey;
1325
- onLoadRef.current?.(loadedTextures);
1326
- }, [cachedResult, loadedTextures, inputKey]);
1327
- useEffect(() => {
1328
- if (cachedResult) return;
1329
- if ("initTexture" in renderer) {
1330
- let textureArray = [];
1331
- if (Array.isArray(loadedTextures)) {
1332
- textureArray = loadedTextures;
1333
- } else if (loadedTextures instanceof Texture$1) {
1334
- textureArray = [loadedTextures];
1335
- } else if (IsObject(loadedTextures)) {
1336
- textureArray = Object.values(loadedTextures);
1337
- }
1338
- textureArray.forEach((texture) => {
1339
- if (texture instanceof Texture$1) {
1340
- renderer.initTexture(texture);
1341
- }
1342
- });
1343
- }
1344
- }, [renderer, loadedTextures, cachedResult]);
1345
- const mappedTextures = useMemo(() => {
1346
- if (cachedResult) return cachedResult;
1347
- if (IsObject(input)) {
1348
- const keyed = {};
1349
- const textureArray = loadedTextures;
1350
- let i = 0;
1351
- for (const key in input) keyed[key] = textureArray[i++];
1352
- return keyed;
1353
- } else {
1354
- return loadedTextures;
1355
- }
1356
- }, [input, loadedTextures, cachedResult]);
1357
- useEffect(() => {
1358
- if (!cache) return;
1359
- const urlTextureMap = [];
1360
- if (typeof input === "string") {
1361
- urlTextureMap.push([input, mappedTextures]);
1362
- } else if (Array.isArray(input)) {
1363
- const textureArray = mappedTextures;
1364
- input.forEach((url, i) => urlTextureMap.push([url, textureArray[i]]));
1365
- } else if (IsObject(input)) {
1366
- const textureRecord = mappedTextures;
1367
- for (const key in input) {
1368
- const url = input[key];
1369
- urlTextureMap.push([url, textureRecord[key]]);
1370
- }
1371
- }
1372
- store.setState((state) => {
1373
- const refs = new Map(state._textureRefs);
1374
- let textures = state.textures;
1375
- let added = false;
1376
- for (const [url, texture] of urlTextureMap) {
1377
- if (!textures.has(url)) {
1378
- if (!added) {
1379
- textures = new Map(textures);
1380
- added = true;
1381
- }
1382
- textures.set(url, texture);
1383
- }
1384
- refs.set(url, (refs.get(url) ?? 0) + 1);
1385
- }
1386
- return added ? { textures, _textureRefs: refs } : { _textureRefs: refs };
1387
- });
1388
- return () => store.setState((state) => {
1389
- const refs = new Map(state._textureRefs);
1390
- for (const [url] of urlTextureMap) {
1391
- const next = (refs.get(url) ?? 0) - 1;
1392
- if (next <= 0) refs.delete(url);
1393
- else refs.set(url, next);
1394
- }
1395
- return { _textureRefs: refs };
1396
- });
1397
- }, [cache, input, mappedTextures, store]);
1398
- return mappedTextures;
1399
- }
1400
- useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
1401
- useTexture.clear = (input) => useLoader.clear(TextureLoader, input);
1402
- const Texture = ({
1403
- children,
1404
- input,
1405
- onLoad,
1406
- cache
1407
- }) => {
1408
- const options = typeof onLoad === "function" ? { onLoad, cache } : { ...onLoad, cache };
1409
- const ret = useTexture(input, options);
1410
- return /* @__PURE__ */ jsx(Fragment, { children: children?.(ret) });
1159
+ useTexture.preload = (url) => useLoader.preload(TextureLoader, url);
1160
+ useTexture.clear = (input) => useLoader.clear(TextureLoader, input);
1161
+ const Texture = ({
1162
+ children,
1163
+ input,
1164
+ onLoad,
1165
+ cache
1166
+ }) => {
1167
+ const options = typeof onLoad === "function" ? { onLoad, cache } : { ...onLoad, cache };
1168
+ const ret = useTexture(input, options);
1169
+ return /* @__PURE__ */ jsx(Fragment, { children: children?.(ret) });
1411
1170
  };
1412
1171
 
1413
1172
  function useTextures(selector) {
@@ -1557,7 +1316,7 @@ function useEnvironment({
1557
1316
  texture = texture.renderTarget?.texture;
1558
1317
  }
1559
1318
  texture.mapping = isCubemap ? CubeReflectionMapping : EquirectangularReflectionMapping;
1560
- texture.colorSpace = colorSpace ?? (isCubemap ? "srgb" : "srgb-linear");
1319
+ texture.colorSpace = colorSpace ?? (isCubemap && extension !== "hdr-cube" ? "srgb" : "srgb-linear");
1561
1320
  return texture;
1562
1321
  }
1563
1322
  const preloadDefaultOptions = {
@@ -1610,22 +1369,15 @@ function getExtension(files) {
1610
1369
  const isCubemap = isArray(files) && files.length === 6;
1611
1370
  const isGainmap = isArray(files) && files.length === 3 && files.some((file) => file.endsWith("json"));
1612
1371
  const firstEntry = isArray(files) ? files[0] : files;
1613
- const extension = isCubemap ? "cube" : isGainmap ? "webp" : firstEntry.startsWith("data:application/exr") ? "exr" : firstEntry.startsWith("data:application/hdr") ? "hdr" : firstEntry.startsWith("data:image/jpeg") ? "jpg" : firstEntry.split(".").pop()?.split("?")?.shift()?.toLowerCase();
1372
+ const firstExtension = firstEntry.split(".").pop()?.split("?")?.shift()?.toLowerCase();
1373
+ const extension = isCubemap ? firstExtension === "hdr" ? "hdr-cube" : "cube" : isGainmap ? "webp" : firstEntry.startsWith("data:application/exr") ? "exr" : firstEntry.startsWith("data:application/hdr") ? "hdr" : firstEntry.startsWith("data:image/jpeg") ? "jpg" : firstExtension;
1614
1374
  return { extension, isCubemap, isGainmap };
1615
1375
  }
1616
1376
  function getLoader(extension) {
1617
- const loader = extension === "cube" ? CubeTextureLoader : extension === "hdr" ? HDRLoader : extension === "exr" ? EXRLoader : extension === "jpg" || extension === "jpeg" ? UltraHDRLoader : extension === "webp" ? GainMapLoader : null;
1377
+ const loader = extension === "cube" ? CubeTextureLoader : extension === "hdr-cube" ? HDRCubeTextureLoader : extension === "hdr" ? HDRLoader : extension === "exr" ? EXRLoader : extension === "jpg" || extension === "jpeg" ? UltraHDRLoader : extension === "webp" ? GainMapLoader : null;
1618
1378
  return loader;
1619
1379
  }
1620
1380
 
1621
- function useStore() {
1622
- const store = useContext(context);
1623
- if (!store) throw new Error("R3F: Hooks can only be used within the Canvas component!");
1624
- return store;
1625
- }
1626
- function useThree(selector = (state) => state, equalityFn) {
1627
- return useStore()(selector, equalityFn);
1628
- }
1629
1381
  function useInstanceHandle(ref) {
1630
1382
  const instance = useRef(null);
1631
1383
  useImperativeHandle(instance, () => ref.current.__r3f, [ref]);
@@ -1665,13 +1417,19 @@ function addTail(callback) {
1665
1417
  return getScheduler().onIdle(callback);
1666
1418
  }
1667
1419
  function invalidate(state, frames = 1, stackFrames = false) {
1668
- getScheduler().invalidate(frames, stackFrames);
1420
+ const rootId = state?.internal?.rootId;
1421
+ const scheduler = getScheduler();
1422
+ if (rootId) scheduler.invalidateRoot(rootId, frames, stackFrames);
1423
+ else scheduler.invalidate(frames, stackFrames);
1669
1424
  }
1670
- function advance(timestamp) {
1671
- getScheduler().step(timestamp);
1425
+ function advance(timestamp, runGlobalEffects, state) {
1426
+ const rootId = state?.internal?.rootId;
1427
+ const scheduler = getScheduler();
1428
+ if (rootId) scheduler.stepRoot(rootId, timestamp);
1429
+ else scheduler.step(timestamp);
1672
1430
  }
1673
1431
 
1674
- const version = "10.0.0-alpha.3";
1432
+ const version = "10.0.0-alpha.5";
1675
1433
  const packageData = {
1676
1434
  version: version};
1677
1435
 
@@ -13530,7 +13288,10 @@ function extend(objects) {
13530
13288
  catalogue[Component] = objects;
13531
13289
  return Component;
13532
13290
  } else {
13533
- Object.assign(catalogue, objects);
13291
+ for (const name in objects) {
13292
+ const object = objects[name];
13293
+ if (isConstructor(object)) catalogue[name] = object;
13294
+ }
13534
13295
  }
13535
13296
  }
13536
13297
  function validateInstance(type, props) {
@@ -13878,7 +13639,7 @@ const reconciler = /* @__PURE__ */ createReconciler({
13878
13639
  getRootHostContext: () => NO_CONTEXT,
13879
13640
  getChildHostContext: () => NO_CONTEXT,
13880
13641
  commitUpdate(instance, type, oldProps, newProps, fiber) {
13881
- validateInstance(type, newProps);
13642
+ validateInstance(instance.type, newProps);
13882
13643
  let reconstruct = false;
13883
13644
  if (instance.type === "primitive" && oldProps.object !== newProps.object) reconstruct = true;
13884
13645
  else if (newProps.args?.length !== oldProps.args?.length) reconstruct = true;
@@ -13893,162 +13654,491 @@ const reconciler = /* @__PURE__ */ createReconciler({
13893
13654
  else delete instance.props.attach;
13894
13655
  if (Object.keys(changedProps).length) applyProps(instance.object, changedProps);
13895
13656
  }
13896
- },
13897
- finalizeInitialChildren: (instance) => {
13898
- for (const prop in instance.props) {
13899
- if (isFromRef(instance.props[prop])) return true;
13657
+ },
13658
+ finalizeInitialChildren: (instance) => {
13659
+ for (const prop in instance.props) {
13660
+ if (isFromRef(instance.props[prop])) return true;
13661
+ }
13662
+ return false;
13663
+ },
13664
+ commitMount(instance) {
13665
+ const resolved = {};
13666
+ for (const prop in instance.props) {
13667
+ const value = instance.props[prop];
13668
+ if (isFromRef(value)) {
13669
+ const { [FROM_REF]: ref, transform } = value;
13670
+ if (ref.current != null) resolved[prop] = transform ? transform(ref.current) : ref.current;
13671
+ }
13672
+ }
13673
+ if (Object.keys(resolved).length) applyProps(instance.object, resolved);
13674
+ },
13675
+ getPublicInstance: (instance) => instance?.object,
13676
+ prepareForCommit: () => null,
13677
+ preparePortalMount: (container) => {
13678
+ const target = container.getState().internal.container ?? container.getState().scene;
13679
+ return prepare(target, container, "", {});
13680
+ },
13681
+ // Reconstructed instances are swapped once all mutations are committed,
13682
+ // before layout effects run so refs point to the new objects
13683
+ resetAfterCommit: flushReconstructedInstances,
13684
+ shouldSetTextContent: () => false,
13685
+ clearContainer: () => false,
13686
+ hideInstance,
13687
+ unhideInstance,
13688
+ createTextInstance: handleTextInstance,
13689
+ hideTextInstance: handleTextInstance,
13690
+ unhideTextInstance: handleTextInstance,
13691
+ // Mirrors react-dom, which flushes discrete work in microtasks
13692
+ supportsMicrotasks: true,
13693
+ scheduleMicrotask,
13694
+ scheduleTimeout: typeof setTimeout === "function" ? setTimeout : void 0,
13695
+ cancelTimeout: typeof clearTimeout === "function" ? clearTimeout : void 0,
13696
+ noTimeout: -1,
13697
+ getInstanceFromNode: () => null,
13698
+ beforeActiveInstanceBlur() {
13699
+ },
13700
+ afterActiveInstanceBlur() {
13701
+ },
13702
+ detachDeletedInstance() {
13703
+ },
13704
+ prepareScopeUpdate() {
13705
+ },
13706
+ getInstanceFromScope: () => null,
13707
+ shouldAttemptEagerTransition: () => false,
13708
+ trackSchedulerEvent: () => {
13709
+ },
13710
+ resolveEventType: () => null,
13711
+ resolveEventTimeStamp: () => -1.1,
13712
+ requestPostPaintCallback() {
13713
+ },
13714
+ maySuspendCommit: () => false,
13715
+ preloadInstance: () => true,
13716
+ // true indicates already loaded
13717
+ suspendInstance() {
13718
+ },
13719
+ waitForCommitToBeReady: () => null,
13720
+ NotPendingTransition: null,
13721
+ // The reconciler types use the internal ReactContext with all the hidden properties
13722
+ // so we have to cast from the public React.Context type
13723
+ HostTransitionContext: /* @__PURE__ */ React.createContext(
13724
+ null
13725
+ ),
13726
+ setCurrentUpdatePriority(newPriority) {
13727
+ currentUpdatePriority = newPriority;
13728
+ },
13729
+ getCurrentUpdatePriority() {
13730
+ return currentUpdatePriority;
13731
+ },
13732
+ resolveUpdatePriority() {
13733
+ if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority;
13734
+ const eventType = typeof window !== "undefined" ? window.event?.type : void 0;
13735
+ if (eventType === void 0) return r;
13736
+ return getEventPriority(eventType);
13737
+ },
13738
+ resetFormInstance() {
13739
+ },
13740
+ // @ts-ignore DefinitelyTyped is not up to date
13741
+ rendererPackageName: "@react-three/fiber",
13742
+ rendererVersion: packageData.version,
13743
+ // 19.2 Reconciler
13744
+ // https://github.com/facebook/react/pull/31975
13745
+ // https://github.com/facebook/react/pull/31999
13746
+ applyViewTransitionName(_instance, _name, _className) {
13747
+ },
13748
+ restoreViewTransitionName(_instance, _props) {
13749
+ },
13750
+ cancelViewTransitionName(_instance, _name, _props) {
13751
+ },
13752
+ cancelRootViewTransitionName(_rootContainer) {
13753
+ },
13754
+ restoreRootViewTransitionName(_rootContainer) {
13755
+ },
13756
+ InstanceMeasurement: null,
13757
+ measureInstance: (_instance) => null,
13758
+ wasInstanceInViewport: (_measurement) => true,
13759
+ hasInstanceChanged: (_oldMeasurement, _newMeasurement) => false,
13760
+ hasInstanceAffectedParent: (_oldMeasurement, _newMeasurement) => false,
13761
+ // https://github.com/facebook/react/pull/32002
13762
+ // https://github.com/facebook/react/pull/34486
13763
+ suspendOnActiveViewTransition(_state, _container) {
13764
+ },
13765
+ // https://github.com/facebook/react/pull/32451
13766
+ // https://github.com/facebook/react/pull/32760
13767
+ startGestureTransition: () => null,
13768
+ startViewTransition: () => null,
13769
+ stopViewTransition(_transition) {
13770
+ },
13771
+ // https://github.com/facebook/react/pull/32038
13772
+ createViewTransitionInstance: (_name) => null,
13773
+ // https://github.com/facebook/react/pull/32379
13774
+ // https://github.com/facebook/react/pull/32786
13775
+ getCurrentGestureOffset(_provider) {
13776
+ throw new Error("startGestureTransition is not yet supported in react-three-fiber.");
13777
+ },
13778
+ // https://github.com/facebook/react/pull/32500
13779
+ cloneMutableInstance(instance, _keepChildren) {
13780
+ return instance;
13781
+ },
13782
+ cloneMutableTextInstance(textInstance) {
13783
+ return textInstance;
13784
+ },
13785
+ // React 19.2 added these for root-level view transitions. They only run for hosts that opt into
13786
+ // view transitions by returning a container from cloneRootViewTransitionContainer; R3F renders
13787
+ // into a WebGL/WebGPU canvas, where cloning the root has no meaning, so we never opt in and
13788
+ // React never calls them. They stay as no-ops rather than throws: the host config is shared by
13789
+ // every root, and a throw here would turn a React-internal probe into a hard crash for users.
13790
+ // Guarded by a test asserting a full render + unmount never reaches them.
13791
+ cloneRootViewTransitionContainer(_rootContainer) {
13792
+ },
13793
+ removeRootViewTransitionClone(_rootContainer, _clone) {
13794
+ },
13795
+ // https://github.com/facebook/react/pull/32465
13796
+ createFragmentInstance: (_fiber) => null,
13797
+ updateFragmentInstanceFiber(_fiber, _instance) {
13798
+ },
13799
+ commitNewChildToFragmentInstance(_child, _fragmentInstance) {
13800
+ },
13801
+ deleteChildFromFragmentInstance(_child, _fragmentInstance) {
13802
+ },
13803
+ // https://github.com/facebook/react/pull/32653
13804
+ measureClonedInstance: (_instance) => null,
13805
+ // https://github.com/facebook/react/pull/32819
13806
+ maySuspendCommitOnUpdate: (_type, _oldProps, _newProps) => false,
13807
+ maySuspendCommitInSyncRender: (_type, _props) => false,
13808
+ // https://github.com/facebook/react/pull/34486
13809
+ startSuspendingCommit: () => null,
13810
+ // https://github.com/facebook/react/pull/34522
13811
+ getSuspendedCommitReason: (_state, _rootContainer) => null
13812
+ });
13813
+
13814
+ const INTERNAL_LIB_MARKERS = ["zustand", "setState", "Object.assign"];
13815
+ function isInternalRendererAccess(stack, callerDepth = 2) {
13816
+ if (!stack) return false;
13817
+ const caller = stack.split("\n")[callerDepth] ?? "";
13818
+ return INTERNAL_LIB_MARKERS.some((m) => caller.includes(m));
13819
+ }
13820
+
13821
+ const createStore = (invalidate, advance) => {
13822
+ const rootStore = createWithEqualityFn((set, get) => {
13823
+ const position = new Vector3();
13824
+ const defaultTarget = new Vector3();
13825
+ const tempTarget = new Vector3();
13826
+ function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
13827
+ const { width, height, top, left } = size;
13828
+ const aspect = width / height;
13829
+ if (target.isVector3) tempTarget.copy(target);
13830
+ else tempTarget.set(...target);
13831
+ const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
13832
+ if (isOrthographicCamera(camera)) {
13833
+ return { width: width / camera.zoom, height: height / camera.zoom, top, left, factor: 1, distance, aspect };
13834
+ } else {
13835
+ const fov = camera.fov * Math.PI / 180;
13836
+ const h = 2 * Math.tan(fov / 2) * distance;
13837
+ const w = h * (width / height);
13838
+ return { width: w, height: h, top, left, factor: width / w, distance, aspect };
13839
+ }
13840
+ }
13841
+ let performanceTimeout = void 0;
13842
+ const setPerformanceCurrent = (current) => set((state2) => ({ performance: { ...state2.performance, current } }));
13843
+ const pointer = new Vector2();
13844
+ const rootState = {
13845
+ set,
13846
+ get,
13847
+ // Mock objects that have to be configured
13848
+ // primaryStore is set after store creation (self-reference for primary, primary's store for secondary)
13849
+ primaryStore: null,
13850
+ gl: null,
13851
+ renderer: null,
13852
+ camera: null,
13853
+ frustum: new Frustum(),
13854
+ autoUpdateFrustum: true,
13855
+ raycaster: null,
13856
+ events: {
13857
+ priority: 1,
13858
+ enabled: true,
13859
+ connected: false,
13860
+ frameTimedRaycasts: true,
13861
+ alwaysFireOnScroll: true,
13862
+ updateOnFrame: false
13863
+ },
13864
+ scene: null,
13865
+ rootScene: null,
13866
+ xr: null,
13867
+ inspector: null,
13868
+ invalidate: (frames = 1, stackFrames = false) => invalidate(get(), frames, stackFrames),
13869
+ advance: (timestamp, runGlobalEffects) => advance(timestamp, runGlobalEffects, get()),
13870
+ textureColorSpace: SRGBColorSpace,
13871
+ isLegacy: false,
13872
+ webGPUSupported: false,
13873
+ isNative: false,
13874
+ controls: null,
13875
+ pointer,
13876
+ mouse: pointer,
13877
+ frameloop: "always",
13878
+ onPointerMissed: void 0,
13879
+ onDragOverMissed: void 0,
13880
+ onDropMissed: void 0,
13881
+ performance: {
13882
+ current: 1,
13883
+ min: 0.5,
13884
+ max: 1,
13885
+ debounce: 200,
13886
+ regress: () => {
13887
+ const state2 = get();
13888
+ if (performanceTimeout) clearTimeout(performanceTimeout);
13889
+ if (state2.performance.current !== state2.performance.min) setPerformanceCurrent(state2.performance.min);
13890
+ performanceTimeout = setTimeout(
13891
+ () => setPerformanceCurrent(get().performance.max),
13892
+ state2.performance.debounce
13893
+ );
13894
+ }
13895
+ },
13896
+ size: { width: 0, height: 0, top: 0, left: 0 },
13897
+ viewport: {
13898
+ initialDpr: 0,
13899
+ dpr: 0,
13900
+ width: 0,
13901
+ height: 0,
13902
+ top: 0,
13903
+ left: 0,
13904
+ aspect: 0,
13905
+ distance: 0,
13906
+ factor: 0,
13907
+ getCurrentViewport
13908
+ },
13909
+ setEvents: (events) => set((state2) => ({ ...state2, events: { ...state2.events, ...events } })),
13910
+ setSize: (width, height, top, left) => {
13911
+ const state2 = get();
13912
+ if (width === void 0) {
13913
+ set({ _sizeImperative: false });
13914
+ if (state2._sizeProps) {
13915
+ const { width: propW, height: propH } = state2._sizeProps;
13916
+ if (propW !== void 0 || propH !== void 0) {
13917
+ const currentSize = state2.size;
13918
+ const newSize = {
13919
+ width: propW ?? currentSize.width,
13920
+ height: propH ?? currentSize.height,
13921
+ top: currentSize.top,
13922
+ left: currentSize.left
13923
+ };
13924
+ set((s) => ({
13925
+ size: newSize,
13926
+ viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, newSize) }
13927
+ }));
13928
+ get().invalidate();
13929
+ }
13930
+ }
13931
+ return;
13932
+ }
13933
+ const w = width;
13934
+ const h = height ?? width;
13935
+ const t = top ?? state2.size.top;
13936
+ const l = left ?? state2.size.left;
13937
+ const size = { width: w, height: h, top: t, left: l };
13938
+ set((s) => ({
13939
+ size,
13940
+ viewport: { ...s.viewport, ...getCurrentViewport(state2.camera, defaultTarget, size) },
13941
+ _sizeImperative: true
13942
+ }));
13943
+ get().invalidate();
13944
+ },
13945
+ setDpr: (dpr) => set((state2) => {
13946
+ const resolved = calculateDpr(dpr);
13947
+ return { viewport: { ...state2.viewport, dpr: resolved, initialDpr: state2.viewport.initialDpr || resolved } };
13948
+ }),
13949
+ setFrameloop: (frameloop = "always") => {
13950
+ const rootId = get().internal.rootId;
13951
+ if (rootId) getScheduler().setRootFrameloop(rootId, frameloop);
13952
+ set(() => ({ frameloop }));
13953
+ },
13954
+ setError: (error) => set(() => ({ error })),
13955
+ error: null,
13956
+ //* TSL State (managed via hooks: useUniforms, useNodes, useBuffers, useGPUStorage, useTextures, useRenderPipeline) ==============================
13957
+ uniforms: {},
13958
+ nodes: {},
13959
+ buffers: {},
13960
+ gpuStorage: {},
13961
+ textures: /* @__PURE__ */ new Map(),
13962
+ _textureRefs: /* @__PURE__ */ new Map(),
13963
+ renderPipeline: null,
13964
+ passes: {},
13965
+ _hmrVersion: 0,
13966
+ _sizeImperative: false,
13967
+ _sizeProps: null,
13968
+ previousRoot: void 0,
13969
+ internal: {
13970
+ // Events
13971
+ interaction: [],
13972
+ subscribers: [],
13973
+ // Per-pointer state (new unified structure)
13974
+ pointerMap: /* @__PURE__ */ new Map(),
13975
+ pointerDirty: /* @__PURE__ */ new Map(),
13976
+ lastEvent: React.createRef(),
13977
+ // Deprecated but kept for backwards compatibility
13978
+ hovered: /* @__PURE__ */ new Map(),
13979
+ initialClick: [0, 0],
13980
+ initialHits: [],
13981
+ capturedMap: /* @__PURE__ */ new Map(),
13982
+ // Visibility tracking (onFramed, onOccluded, onVisible)
13983
+ visibilityRegistry: /* @__PURE__ */ new Map(),
13984
+ // Occlusion system (WebGPU only)
13985
+ occlusionEnabled: false,
13986
+ occlusionObserver: null,
13987
+ occlusionCache: /* @__PURE__ */ new Map(),
13988
+ helperGroup: null,
13989
+ // Updates
13990
+ active: false,
13991
+ frames: 0,
13992
+ priority: 0,
13993
+ subscribe: (ref, priority, store) => {
13994
+ const internal = get().internal;
13995
+ internal.priority = internal.priority + (priority > 0 ? 1 : 0);
13996
+ internal.subscribers.push({ ref, priority, store });
13997
+ internal.subscribers = internal.subscribers.sort((a, b) => a.priority - b.priority);
13998
+ return () => {
13999
+ const internal2 = get().internal;
14000
+ if (internal2?.subscribers) {
14001
+ internal2.priority = internal2.priority - (priority > 0 ? 1 : 0);
14002
+ internal2.subscribers = internal2.subscribers.filter((s) => s.ref !== ref);
14003
+ }
14004
+ };
14005
+ },
14006
+ // Renderer Storage (single source of truth)
14007
+ actualRenderer: null,
14008
+ // Scheduler for useFrameNext (initialized in renderer.tsx)
14009
+ scheduler: null,
14010
+ // Replaces the default render call when set (see setRenderOverride)
14011
+ renderOverride: null
14012
+ }
14013
+ };
14014
+ return rootState;
14015
+ });
14016
+ const state = rootStore.getState();
14017
+ Object.defineProperty(state, "gl", {
14018
+ get() {
14019
+ const currentState = rootStore.getState();
14020
+ if (!currentState.isLegacy && currentState.internal.actualRenderer) {
14021
+ const stack = new Error().stack || "";
14022
+ if (!isInternalRendererAccess(stack)) {
14023
+ const cleanedStack = stack.split("\n").slice(2).join("\n") || "Stack trace unavailable";
14024
+ notifyDepreciated({
14025
+ heading: "Accessing state.gl in WebGPU mode",
14026
+ body: "Please use state.renderer instead. state.gl is deprecated and will be removed in future versions.\n\nFor backwards compatibility, state.gl currently maps to state.renderer, but this may cause issues with libraries expecting WebGLRenderer.\n\nAccessed from:\n" + cleanedStack
14027
+ });
14028
+ }
14029
+ }
14030
+ return currentState.internal.actualRenderer;
14031
+ },
14032
+ set(value) {
14033
+ rootStore.getState().internal.actualRenderer = value;
14034
+ },
14035
+ enumerable: true,
14036
+ configurable: true
14037
+ });
14038
+ Object.defineProperty(state, "renderer", {
14039
+ get() {
14040
+ return rootStore.getState().internal.actualRenderer;
14041
+ },
14042
+ set(value) {
14043
+ rootStore.getState().internal.actualRenderer = value;
14044
+ },
14045
+ enumerable: true,
14046
+ configurable: true
14047
+ });
14048
+ let oldScene = state.scene;
14049
+ rootStore.subscribe(() => {
14050
+ const currentState = rootStore.getState();
14051
+ const { scene, rootScene, set } = currentState;
14052
+ if (scene !== oldScene) {
14053
+ oldScene = scene;
14054
+ if (scene?.isScene && scene !== rootScene) {
14055
+ set({ rootScene: scene });
14056
+ }
14057
+ }
14058
+ });
14059
+ let oldSize = state.size;
14060
+ let oldDpr = state.viewport.dpr;
14061
+ let oldCamera = state.camera;
14062
+ rootStore.subscribe(() => {
14063
+ const { camera, size, viewport, set, internal } = rootStore.getState();
14064
+ const actualRenderer = internal.actualRenderer;
14065
+ const canvasTarget = internal.canvasTarget;
14066
+ if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
14067
+ oldSize = size;
14068
+ oldDpr = viewport.dpr;
14069
+ updateCamera(camera, size);
14070
+ const resizeTarget = canvasTarget ?? actualRenderer;
14071
+ if (viewport.dpr > 0) resizeTarget.setPixelRatio(viewport.dpr);
14072
+ resizeTarget.setSize(size.width, size.height, false);
14073
+ const activeTarget = actualRenderer.getCanvasTarget?.();
14074
+ if (!canvasTarget || activeTarget !== canvasTarget) internal.canvasTargetSizeDirty = true;
13900
14075
  }
13901
- return false;
13902
- },
13903
- commitMount(instance) {
13904
- const resolved = {};
13905
- for (const prop in instance.props) {
13906
- const value = instance.props[prop];
13907
- if (isFromRef(value)) {
13908
- const ref = value[FROM_REF];
13909
- if (ref.current != null) resolved[prop] = ref.current;
14076
+ if (camera !== oldCamera) {
14077
+ oldCamera = camera;
14078
+ const { rootScene } = rootStore.getState();
14079
+ if (camera && rootScene && !camera.parent) {
14080
+ rootScene.add(camera);
14081
+ }
14082
+ set((state2) => ({ viewport: { ...state2.viewport, ...state2.viewport.getCurrentViewport(camera) } }));
14083
+ const currentState = rootStore.getState();
14084
+ if (currentState.autoUpdateFrustum && camera) {
14085
+ updateFrustum(camera, currentState.frustum);
13910
14086
  }
13911
14087
  }
13912
- if (Object.keys(resolved).length) applyProps(instance.object, resolved);
13913
- },
13914
- getPublicInstance: (instance) => instance?.object,
13915
- prepareForCommit: () => null,
13916
- preparePortalMount: (container) => {
13917
- const target = container.getState().internal.container ?? container.getState().scene;
13918
- return prepare(target, container, "", {});
13919
- },
13920
- // Reconstructed instances are swapped once all mutations are committed,
13921
- // before layout effects run so refs point to the new objects
13922
- resetAfterCommit: flushReconstructedInstances,
13923
- shouldSetTextContent: () => false,
13924
- clearContainer: () => false,
13925
- hideInstance,
13926
- unhideInstance,
13927
- createTextInstance: handleTextInstance,
13928
- hideTextInstance: handleTextInstance,
13929
- unhideTextInstance: handleTextInstance,
13930
- // Mirrors react-dom, which flushes discrete work in microtasks
13931
- supportsMicrotasks: true,
13932
- scheduleMicrotask,
13933
- scheduleTimeout: typeof setTimeout === "function" ? setTimeout : void 0,
13934
- cancelTimeout: typeof clearTimeout === "function" ? clearTimeout : void 0,
13935
- noTimeout: -1,
13936
- getInstanceFromNode: () => null,
13937
- beforeActiveInstanceBlur() {
13938
- },
13939
- afterActiveInstanceBlur() {
13940
- },
13941
- detachDeletedInstance() {
13942
- },
13943
- prepareScopeUpdate() {
13944
- },
13945
- getInstanceFromScope: () => null,
13946
- shouldAttemptEagerTransition: () => false,
13947
- trackSchedulerEvent: () => {
13948
- },
13949
- resolveEventType: () => null,
13950
- resolveEventTimeStamp: () => -1.1,
13951
- requestPostPaintCallback() {
13952
- },
13953
- maySuspendCommit: () => false,
13954
- preloadInstance: () => true,
13955
- // true indicates already loaded
13956
- suspendInstance() {
13957
- },
13958
- waitForCommitToBeReady: () => null,
13959
- NotPendingTransition: null,
13960
- // The reconciler types use the internal ReactContext with all the hidden properties
13961
- // so we have to cast from the public React.Context type
13962
- HostTransitionContext: /* @__PURE__ */ React.createContext(
13963
- null
13964
- ),
13965
- setCurrentUpdatePriority(newPriority) {
13966
- currentUpdatePriority = newPriority;
13967
- },
13968
- getCurrentUpdatePriority() {
13969
- return currentUpdatePriority;
13970
- },
13971
- resolveUpdatePriority() {
13972
- if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority;
13973
- const eventType = typeof window !== "undefined" ? window.event?.type : void 0;
13974
- if (eventType === void 0) return r;
13975
- return getEventPriority(eventType);
13976
- },
13977
- resetFormInstance() {
13978
- },
13979
- // @ts-ignore DefinitelyTyped is not up to date
13980
- rendererPackageName: "@react-three/fiber",
13981
- rendererVersion: packageData.version,
13982
- // 19.2 Reconciler
13983
- // https://github.com/facebook/react/pull/31975
13984
- // https://github.com/facebook/react/pull/31999
13985
- applyViewTransitionName(_instance, _name, _className) {
13986
- },
13987
- restoreViewTransitionName(_instance, _props) {
13988
- },
13989
- cancelViewTransitionName(_instance, _name, _props) {
13990
- },
13991
- cancelRootViewTransitionName(_rootContainer) {
13992
- },
13993
- restoreRootViewTransitionName(_rootContainer) {
13994
- },
13995
- InstanceMeasurement: null,
13996
- measureInstance: (_instance) => null,
13997
- wasInstanceInViewport: (_measurement) => true,
13998
- hasInstanceChanged: (_oldMeasurement, _newMeasurement) => false,
13999
- hasInstanceAffectedParent: (_oldMeasurement, _newMeasurement) => false,
14000
- // https://github.com/facebook/react/pull/32002
14001
- // https://github.com/facebook/react/pull/34486
14002
- suspendOnActiveViewTransition(_state, _container) {
14003
- },
14004
- // https://github.com/facebook/react/pull/32451
14005
- // https://github.com/facebook/react/pull/32760
14006
- startGestureTransition: () => null,
14007
- startViewTransition: () => null,
14008
- stopViewTransition(_transition) {
14009
- },
14010
- // https://github.com/facebook/react/pull/32038
14011
- createViewTransitionInstance: (_name) => null,
14012
- // https://github.com/facebook/react/pull/32379
14013
- // https://github.com/facebook/react/pull/32786
14014
- getCurrentGestureOffset(_provider) {
14015
- throw new Error("startGestureTransition is not yet supported in react-three-fiber.");
14016
- },
14017
- // https://github.com/facebook/react/pull/32500
14018
- cloneMutableInstance(instance, _keepChildren) {
14019
- return instance;
14020
- },
14021
- cloneMutableTextInstance(textInstance) {
14022
- return textInstance;
14023
- },
14024
- // React 19.2 added these for root-level view transitions. They only run for hosts that opt into
14025
- // view transitions by returning a container from cloneRootViewTransitionContainer; R3F renders
14026
- // into a WebGL/WebGPU canvas, where cloning the root has no meaning, so we never opt in and
14027
- // React never calls them. They stay as no-ops rather than throws: the host config is shared by
14028
- // every root, and a throw here would turn a React-internal probe into a hard crash for users.
14029
- // Guarded by a test asserting a full render + unmount never reaches them.
14030
- cloneRootViewTransitionContainer(_rootContainer) {
14031
- },
14032
- removeRootViewTransitionClone(_rootContainer, _clone) {
14033
- },
14034
- // https://github.com/facebook/react/pull/32465
14035
- createFragmentInstance: (_fiber) => null,
14036
- updateFragmentInstanceFiber(_fiber, _instance) {
14037
- },
14038
- commitNewChildToFragmentInstance(_child, _fragmentInstance) {
14039
- },
14040
- deleteChildFromFragmentInstance(_child, _fragmentInstance) {
14041
- },
14042
- // https://github.com/facebook/react/pull/32653
14043
- measureClonedInstance: (_instance) => null,
14044
- // https://github.com/facebook/react/pull/32819
14045
- maySuspendCommitOnUpdate: (_type, _oldProps, _newProps) => false,
14046
- maySuspendCommitInSyncRender: (_type, _props) => false,
14047
- // https://github.com/facebook/react/pull/34486
14048
- startSuspendingCommit: () => null,
14049
- // https://github.com/facebook/react/pull/34522
14050
- getSuspendedCommitReason: (_state, _rootContainer) => null
14051
- });
14088
+ });
14089
+ rootStore.subscribe((state2) => invalidate(state2));
14090
+ return rootStore;
14091
+ };
14092
+
14093
+ const R3F_EXTENSIONS = Symbol.for("@react-three/fiber.extensions");
14094
+ function getRegistry() {
14095
+ return globalThis[R3F_EXTENSIONS] ?? (globalThis[R3F_EXTENSIONS] = {
14096
+ extensions: /* @__PURE__ */ new Map(),
14097
+ roots: /* @__PURE__ */ new Set(),
14098
+ applied: /* @__PURE__ */ new WeakMap()
14099
+ });
14100
+ }
14101
+ function setupRoot(registry, store, extension) {
14102
+ let applied = registry.applied.get(store);
14103
+ if (!applied) registry.applied.set(store, applied = /* @__PURE__ */ new Map());
14104
+ if (applied.has(extension.name)) return;
14105
+ applied.set(extension.name, extension);
14106
+ const patch = extension.setup?.(store);
14107
+ if (patch) store.setState(patch);
14108
+ }
14109
+ function registerRootExtension(extension) {
14110
+ const registry = getRegistry();
14111
+ registry.extensions.set(extension.name, extension);
14112
+ for (const store of registry.roots) setupRoot(registry, store, extension);
14113
+ return () => {
14114
+ if (registry.extensions.get(extension.name) === extension) registry.extensions.delete(extension.name);
14115
+ };
14116
+ }
14117
+ function setRenderOverride(store, render) {
14118
+ store.getState().internal.renderOverride = render;
14119
+ }
14120
+ function attachRootExtensions(store) {
14121
+ const registry = getRegistry();
14122
+ registry.roots.add(store);
14123
+ for (const extension of registry.extensions.values()) setupRoot(registry, store, extension);
14124
+ }
14125
+ function current(registry, setUpBy) {
14126
+ return registry.extensions.get(setUpBy.name) ?? setUpBy;
14127
+ }
14128
+ function detachRootExtensions(store) {
14129
+ const registry = getRegistry();
14130
+ if (!registry.roots.delete(store)) return;
14131
+ const applied = registry.applied.get(store);
14132
+ registry.applied.delete(store);
14133
+ if (!applied) return;
14134
+ for (const setUpBy of applied.values()) current(registry, setUpBy).dispose?.(store);
14135
+ }
14136
+ function notifyRootExtensionsHmr(store) {
14137
+ const registry = getRegistry();
14138
+ const applied = registry.applied.get(store);
14139
+ if (!applied) return;
14140
+ for (const setUpBy of applied.values()) current(registry, setUpBy).hmr?.(store);
14141
+ }
14052
14142
 
14053
14143
  const isRenderer = (def) => !!def?.render;
14054
14144
  const _roots = /* @__PURE__ */ new Map();
@@ -14205,9 +14295,8 @@ function createRoot(canvas) {
14205
14295
  if (!renderer.hasInitialized?.()) {
14206
14296
  const size2 = computeInitialSize(canvas, propsSize);
14207
14297
  if (size2.width > 0 && size2.height > 0) {
14208
- const pixelRatio = calculateDpr(dpr);
14209
- canvas.width = size2.width * pixelRatio;
14210
- canvas.height = size2.height * pixelRatio;
14298
+ renderer.setPixelRatio(calculateDpr(dpr));
14299
+ renderer.setSize(size2.width, size2.height, false);
14211
14300
  }
14212
14301
  await renderer.init();
14213
14302
  }
@@ -14216,7 +14305,7 @@ function createRoot(canvas) {
14216
14305
  state.internal.actualRenderer = renderer;
14217
14306
  state.set({ webGPUSupported: isWebGPUBackend, renderer, primaryStore: store });
14218
14307
  if (canvasId && !state.internal.isSecondary) {
14219
- const canvasTarget = new CanvasTarget(canvas);
14308
+ const canvasTarget = renderer.getCanvasTarget?.();
14220
14309
  const unregisterPrimary = registerPrimary(canvasId, renderer, store);
14221
14310
  state.set((prev) => ({
14222
14311
  internal: {
@@ -14330,7 +14419,7 @@ function createRoot(canvas) {
14330
14419
  const handleXRFrame = (timestamp, _frame) => {
14331
14420
  const state2 = store.getState();
14332
14421
  if (state2.frameloop === "never") return;
14333
- advance(timestamp);
14422
+ advance(timestamp, true, state2);
14334
14423
  };
14335
14424
  const actualRenderer = state.internal.actualRenderer;
14336
14425
  const handleSessionChange = () => {
@@ -14338,7 +14427,7 @@ function createRoot(canvas) {
14338
14427
  const renderer2 = state2.internal.actualRenderer;
14339
14428
  actualRenderer.xr.enabled = actualRenderer.xr.isPresenting;
14340
14429
  renderer2.xr.setAnimationLoop(renderer2.xr.isPresenting ? handleXRFrame : null);
14341
- if (!renderer2.xr.isPresenting) invalidate();
14430
+ if (!renderer2.xr.isPresenting) invalidate(state2);
14342
14431
  };
14343
14432
  const xr = {
14344
14433
  connect() {
@@ -14420,14 +14509,23 @@ function createRoot(canvas) {
14420
14509
  const newRootId = canvasId || scheduler.generateRootId();
14421
14510
  const unregisterRoot = scheduler.registerRoot(newRootId, {
14422
14511
  getState: () => store.getState(),
14423
- onError: (err) => store.getState().setError(err)
14512
+ onError: (err) => store.getState().setError(err),
14513
+ frameloop: store.getState().frameloop,
14514
+ before: schedulerConfig?.before,
14515
+ after: schedulerConfig?.after,
14516
+ order: schedulerConfig?.order
14424
14517
  });
14425
14518
  const unregisterCanvasTarget = scheduler.register(
14426
14519
  () => {
14427
14520
  const state2 = store.getState();
14428
- if (state2.internal.isMultiCanvas && state2.internal.canvasTarget) {
14521
+ const canvasTarget = state2.internal.canvasTarget;
14522
+ if (canvasTarget) {
14429
14523
  const renderer2 = state2.internal.actualRenderer;
14430
- renderer2.setCanvasTarget(state2.internal.canvasTarget);
14524
+ if (renderer2.getCanvasTarget() !== canvasTarget) renderer2.setCanvasTarget(canvasTarget);
14525
+ if (state2.internal.canvasTargetSizeDirty) {
14526
+ state2.internal.canvasTargetSizeDirty = false;
14527
+ renderer2.backend?.updateSize?.();
14528
+ }
14431
14529
  }
14432
14530
  },
14433
14531
  {
@@ -14483,7 +14581,8 @@ function createRoot(canvas) {
14483
14581
  const userHandlesRender = scheduler.hasUserJobsInPhase("render", newRootId);
14484
14582
  if (userHandlesRender || state2.internal.priority) return;
14485
14583
  try {
14486
- if (state2.renderPipeline?.render) state2.renderPipeline.render();
14584
+ const renderOverride = state2.internal.renderOverride;
14585
+ if (renderOverride) renderOverride();
14487
14586
  else if (renderer2?.render) renderer2.render(state2.scene, state2.camera);
14488
14587
  } catch (error) {
14489
14588
  state2.setError(error instanceof Error ? error : new Error(String(error)));
@@ -14496,8 +14595,7 @@ function createRoot(canvas) {
14496
14595
  phase: "render",
14497
14596
  system: true,
14498
14597
  // Internal flag: this is a system job, not user-controlled
14499
- // Apply scheduler config for render ordering and rate limiting
14500
- ...schedulerConfig?.after && { after: schedulerConfig.after },
14598
+ // FPS throttles only the default render job; Canvas ordering belongs to the root.
14501
14599
  ...schedulerConfig?.fps && { fps: schedulerConfig.fps }
14502
14600
  }
14503
14601
  );
@@ -14516,8 +14614,22 @@ function createRoot(canvas) {
14516
14614
  scheduler
14517
14615
  }
14518
14616
  }));
14617
+ } else {
14618
+ const constraintsChanged = !is.equ(schedulerConfig?.before, lastConfiguredProps.schedulerBefore, shallowLoose) || !is.equ(schedulerConfig?.after, lastConfiguredProps.schedulerAfter, shallowLoose);
14619
+ if (constraintsChanged) {
14620
+ scheduler.setRootConstraints(rootId, {
14621
+ before: schedulerConfig?.before,
14622
+ after: schedulerConfig?.after
14623
+ });
14624
+ }
14625
+ if (schedulerConfig?.order !== lastConfiguredProps.schedulerOrder) {
14626
+ scheduler.setRootOrder(rootId, schedulerConfig?.order ?? 0);
14627
+ }
14519
14628
  }
14520
- scheduler.frameloop = frameloop;
14629
+ lastConfiguredProps.schedulerBefore = schedulerConfig?.before;
14630
+ lastConfiguredProps.schedulerAfter = schedulerConfig?.after;
14631
+ lastConfiguredProps.schedulerOrder = schedulerConfig?.order;
14632
+ attachRootExtensions(store);
14521
14633
  onCreated = onCreatedCallback;
14522
14634
  configured = true;
14523
14635
  resolve();
@@ -14559,18 +14671,24 @@ function unmountComponentAtNode(canvas, callback) {
14559
14671
  const fiber = root?.fiber;
14560
14672
  if (fiber) {
14561
14673
  const state = root?.store.getState();
14562
- if (state) state.internal.active = false;
14674
+ if (state) {
14675
+ state.internal.active = false;
14676
+ const unregisterRoot = state.internal.unregisterRoot;
14677
+ if (unregisterRoot) {
14678
+ unregisterRoot();
14679
+ state.internal.unregisterRoot = void 0;
14680
+ }
14681
+ }
14563
14682
  reconciler.updateContainer(null, fiber, null, () => {
14683
+ if (root?.store) detachRootExtensions(root.store);
14564
14684
  if (state) {
14565
14685
  setTimeout(() => {
14566
14686
  try {
14567
14687
  const renderer = state.internal.actualRenderer;
14568
- const unregisterRoot = state.internal.unregisterRoot;
14569
- if (unregisterRoot) unregisterRoot();
14570
14688
  const unregisterPrimary = state.internal.unregisterPrimary;
14571
14689
  if (unregisterPrimary) unregisterPrimary();
14572
14690
  const canvasTarget = state.internal.canvasTarget;
14573
- if (canvasTarget?.dispose) canvasTarget.dispose();
14691
+ if (state.internal.isSecondary && canvasTarget?.dispose) canvasTarget.dispose();
14574
14692
  state.events.disconnect?.();
14575
14693
  cleanupHelperGroup(root.store);
14576
14694
  if (state.isLegacy && renderer) {
@@ -14640,11 +14758,24 @@ function PortalInner({ state = {}, children, container }) {
14640
14758
  }
14641
14759
  };
14642
14760
  }, [portalScene, container, injectScene]);
14761
+ const lastRootState = useRef(null);
14643
14762
  const inject = useMutableCallback((rootState, injectState) => {
14763
+ const followed = {};
14764
+ const lastRoot = lastRootState.current;
14765
+ if (lastRoot) {
14766
+ for (const key in rootState) {
14767
+ const field = key;
14768
+ const changed = rootState[field] !== lastRoot[field];
14769
+ const inherited = !(key in rest) && injectState[field] === lastRoot[field];
14770
+ if (changed && inherited) followed[field] = rootState[field];
14771
+ }
14772
+ }
14773
+ lastRootState.current = rootState;
14644
14774
  const resolvedSize = { ...rootState.size, ...injectState.size, ...size };
14645
14775
  let viewport = void 0;
14646
- if (injectState.camera && (size || injectState.size)) {
14647
- const camera = injectState.camera;
14776
+ const portalCamera = followed.camera ?? injectState.camera;
14777
+ if (portalCamera && (size || injectState.size)) {
14778
+ const camera = portalCamera;
14648
14779
  viewport = rootState.viewport.getCurrentViewport(camera, new Vector3(), resolvedSize);
14649
14780
  if (camera !== rootState.camera) updateCamera(camera, resolvedSize);
14650
14781
  }
@@ -14653,6 +14784,7 @@ function PortalInner({ state = {}, children, container }) {
14653
14784
  // The intersect consists of the previous root state
14654
14785
  ...rootState,
14655
14786
  ...injectState,
14787
+ ...followed,
14656
14788
  // Portals have their own scene - always a real THREE.Scene (injected if needed)
14657
14789
  scene: portalScene,
14658
14790
  // rootScene always points to the actual THREE.Scene, even inside portals
@@ -14711,16 +14843,11 @@ function setEnvProps(background, scene, defaultScene, texture, sceneProps = {})
14711
14843
  const oldbg = target.background;
14712
14844
  const oldenv = target.environment;
14713
14845
  const oldSceneProps = {
14714
- // @ts-ignore
14715
14846
  backgroundBlurriness: target.backgroundBlurriness,
14716
- // @ts-ignore
14717
14847
  backgroundIntensity: target.backgroundIntensity,
14718
- // @ts-ignore
14719
- backgroundRotation: target.backgroundRotation?.clone?.() ?? [0, 0, 0],
14720
- // @ts-ignore
14848
+ backgroundRotation: target.backgroundRotation.clone(),
14721
14849
  environmentIntensity: target.environmentIntensity,
14722
- // @ts-ignore
14723
- environmentRotation: target.environmentRotation?.clone?.() ?? [0, 0, 0]
14850
+ environmentRotation: target.environmentRotation.clone()
14724
14851
  };
14725
14852
  if (background !== "only") target.environment = texture;
14726
14853
  if (background) target.background = texture;
@@ -14945,38 +15072,6 @@ function parseRendererConfig(rendererProp) {
14945
15072
  return { primaryCanvas: void 0, scheduler: void 0, renderer: rendererProp };
14946
15073
  }
14947
15074
 
14948
- const RESOURCE_KINDS = ["nodes", "uniforms", "buffers", "gpuStorage"];
14949
- const registries = /* @__PURE__ */ new WeakMap();
14950
- function getKindRegistry(store, kind) {
14951
- let registry = registries.get(store);
14952
- if (!registry) {
14953
- registry = {
14954
- nodes: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14955
- uniforms: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14956
- buffers: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() },
14957
- gpuStorage: { valid: /* @__PURE__ */ new Set(), staged: /* @__PURE__ */ new Map() }
14958
- };
14959
- registries.set(store, registry);
14960
- }
14961
- return registry[kind];
14962
- }
14963
- function invalidateResource(store, kind, scopeKey) {
14964
- const registry = getKindRegistry(store, kind);
14965
- {
14966
- registry.valid.clear();
14967
- registry.staged.clear();
14968
- }
14969
- }
14970
- function invalidateAllResources(store) {
14971
- for (const kind of RESOURCE_KINDS) invalidateResource(store, kind);
14972
- }
14973
-
14974
- function clearHmrCaches(store) {
14975
- const primary = store.getState().primaryStore ?? store;
14976
- invalidateAllResources(primary);
14977
- primary.setState((state) => ({ _hmrVersion: state._hmrVersion + 1 }));
14978
- }
14979
-
14980
15075
  function CanvasImpl({
14981
15076
  ref,
14982
15077
  children,
@@ -15014,9 +15109,9 @@ function CanvasImpl({
15014
15109
  React.useMemo(() => extend(THREE), []);
15015
15110
  const Bridge = useBridge();
15016
15111
  const backgroundProps = React.useMemo(() => parseBackground(background), [background]);
15017
- const hasInitialSizeRef = React.useRef(false);
15112
+ const [hasInitialSize, setHasInitialSize] = React.useState(false);
15018
15113
  const measureConfig = React.useMemo(() => {
15019
- if (!hasInitialSizeRef.current) {
15114
+ if (!hasInitialSize) {
15020
15115
  return {
15021
15116
  ...resize,
15022
15117
  scroll: resize?.scroll ?? true,
@@ -15028,7 +15123,7 @@ function CanvasImpl({
15028
15123
  debounce: { scroll: 50, resize: 0 },
15029
15124
  ...resize
15030
15125
  };
15031
- }, [resize, hasInitialSizeRef.current]);
15126
+ }, [hasInitialSize, resize]);
15032
15127
  const [containerRef, containerRect] = useMeasure(measureConfig);
15033
15128
  const effectiveSize = React.useMemo(() => {
15034
15129
  let w = width ?? containerRect.width;
@@ -15044,9 +15139,7 @@ function CanvasImpl({
15044
15139
  left: containerRect.left
15045
15140
  };
15046
15141
  }, [width, height, containerRect, forceEven]);
15047
- if (!hasInitialSizeRef.current && effectiveSize.width > 0 && effectiveSize.height > 0) {
15048
- hasInitialSizeRef.current = true;
15049
- }
15142
+ if (!hasInitialSize && effectiveSize.width > 0 && effectiveSize.height > 0) setHasInitialSize(true);
15050
15143
  const canvasRef = React.useRef(null);
15051
15144
  const divRef = React.useRef(null);
15052
15145
  React.useImperativeHandle(ref, () => canvasRef.current);
@@ -15160,6 +15253,7 @@ function CanvasImpl({
15160
15253
  const canvas = canvasRef.current;
15161
15254
  if (canvas) {
15162
15255
  return () => {
15256
+ if (canvas.isConnected) return;
15163
15257
  unmountComponentAtNode(canvas);
15164
15258
  root.current = null;
15165
15259
  };
@@ -15172,7 +15266,7 @@ function CanvasImpl({
15172
15266
  const handleHMR = () => {
15173
15267
  queueMicrotask(() => {
15174
15268
  const rootEntry = _roots.get(canvas);
15175
- if (rootEntry?.store) clearHmrCaches(rootEntry.store);
15269
+ if (rootEntry?.store) notifyRootExtensionsHmr(rootEntry.store);
15176
15270
  });
15177
15271
  };
15178
15272
  if (typeof import.meta !== "undefined" && import.meta.hot) {
@@ -15224,4 +15318,4 @@ function Canvas(props) {
15224
15318
 
15225
15319
  extend(THREE);
15226
15320
 
15227
- export { Block, Canvas, Environment, EnvironmentCube, EnvironmentMap, EnvironmentPortal, ErrorBoundary, FROM_REF, IsObject, ONCE, Portal, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Texture, _roots, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, context, createEvents, createPointerEvents, createPortal, createRoot, createStore, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, fromRef, getInstanceProps, getPrimary, getPrimaryIds, getRootState, getUuidPrefix, hasConstructor, hasPrimary, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isFromRef, isObject3D, isOnce, isOrthographicCamera, isRef$1 as isRef, isRenderer, isTexture, isVectorLike, once, prepare, presetsObj, reconciler, registerPrimary, removeInteractivity, resolve, swapInteractivity, unmountComponentAtNode, unregisterPrimary, updateCamera, updateFrustum, useBridge, useEnvironment, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useMutableCallback, useRenderTarget, useStore, useTexture, useTextures, useThree, waitForPrimary };
15321
+ export { Block, Canvas, Environment, EnvironmentCube, EnvironmentMap, EnvironmentPortal, ErrorBoundary, FROM_REF, IsObject, ONCE, Portal, R3F_BUILD_LEGACY, R3F_BUILD_WEBGPU, REACT_INTERNAL_PROPS, RESERVED_PROPS, Texture, _roots, addAfterEffect, addEffect, addTail, advance, applyProps, attach, buildGraph, calculateDpr, context, createEvents, createPointerEvents, createPortal, createRoot, createStore, detach, diffProps, dispose, createPointerEvents as events, extend, findInitialRoot, flushSync, fromRef, getInstanceProps, getPrimary, getPrimaryIds, getRootState, getUuidPrefix, hasConstructor, hasPrimary, invalidate, invalidateInstance, is, isColorRepresentation, isCopyable, isFromRef, isObject3D, isOnce, isOrthographicCamera, isRef$1 as isRef, isRenderer, isTexture, isVectorLike, once, prepare, presetsObj, reconciler, registerPrimary, registerRootExtension, removeInteractivity, resolve, setRenderOverride, swapInteractivity, unmountComponentAtNode, unregisterPrimary, updateCamera, updateFrustum, useBridge, useEnvironment, useFrame, useGraph, useInstanceHandle, useIsomorphicLayoutEffect, useLoader, useMutableCallback, useRenderTarget, useStore, useTexture, useTextures, useThree, waitForPrimary };