@react-three/fiber 9.0.0-alpha.4 → 9.0.0-alpha.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @react-three/fiber
2
2
 
3
+ ## 8.16.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 9c83502c: fix(Canvas): don't override camera frustum props
8
+
3
9
  ## 8.16.2
4
10
 
5
11
  ### Patch Changes
@@ -5,11 +5,15 @@ import { ObjectMap } from "./utils.js";
5
5
  import type { Instance } from "./reconciler.js";
6
6
  /**
7
7
  * Exposes an object's {@link Instance}.
8
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useInstanceHandle
8
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useinstancehandle
9
9
  *
10
10
  * **Note**: this is an escape hatch to react-internal fields. Expect this to change significantly between versions.
11
11
  */
12
- export declare function useInstanceHandle<O>(ref: React.RefObject<O>): React.RefObject<Instance>;
12
+ export declare function useInstanceHandle<T>(ref: React.RefObject<T>): React.RefObject<Instance<T>>;
13
+ /**
14
+ * Returns the R3F Canvas' Zustand store. Useful for [transient updates](https://github.com/pmndrs/zustand#transient-updates-for-often-occurring-state-changes).
15
+ * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usestore
16
+ */
13
17
  export declare function useStore(): RootStore;
14
18
  /**
15
19
  * Accesses R3F's internal state, containing renderer, canvas, scene, etc.
@@ -24,7 +28,7 @@ export declare function useThree<T = RootState>(selector?: (state: RootState) =>
24
28
  export declare function useFrame(callback: RenderCallback, renderPriority?: number): null;
25
29
  /**
26
30
  * Executes a callback in a given update stage.
27
- * Uses the stage instance to indetify which stage to target in the lifecycle.
31
+ * Uses the stage instance to identify which stage to target in the lifecycle.
28
32
  */
29
33
  export declare function useUpdate(callback: UpdateCallback, stage?: StageTypes): void;
30
34
  /**
@@ -9,7 +9,7 @@ export type { ReconcilerRoot, GLProps, CameraProps, RenderProps, InjectState } f
9
9
  export { _roots, render, createRoot, unmountComponentAtNode, createPortal } from "./renderer.js";
10
10
  export type { UpdateSubscription } from "./stages.js";
11
11
  export { Stage, FixedStage, Stages } from "./stages.js";
12
- export type { Subscription, Dpr, Size, RenderCallback, UpdateCallback, LegacyAlways, FrameloopMode, FrameloopRender, FrameloopLegacy, Frameloop, Performance, Renderer, StageTypes, XRManager, RootState, RootStore, } from "./store.js";
12
+ export type { Subscription, Dpr, Size, Viewport, RenderCallback, UpdateCallback, LegacyAlways, FrameloopMode, FrameloopRender, FrameloopLegacy, Frameloop, Performance, Renderer, StageTypes, XRManager, RootState, RootStore, } from "./store.js";
13
13
  export { context } from "./store.js";
14
14
  export type { ObjectMap, Camera, Disposable, Act } from "./utils.js";
15
15
  export { applyProps, getRootState, dispose, act, buildGraph } from "./utils.js";
@@ -35,6 +35,7 @@ export interface Instance<O = any> {
35
35
  handlers: Partial<EventHandlers>;
36
36
  attach?: AttachType<O>;
37
37
  previousAttach?: any;
38
+ isHidden: boolean;
38
39
  autoRemovedBeforeAppend?: boolean;
39
40
  }
40
41
  export declare const catalogue: Catalogue;
@@ -21,6 +21,18 @@ export interface Size {
21
21
  top: number;
22
22
  left: number;
23
23
  }
24
+ export interface Viewport extends Size {
25
+ /** The initial pixel ratio */
26
+ initialDpr: number;
27
+ /** Current pixel ratio */
28
+ dpr: number;
29
+ /** size.width / viewport.width */
30
+ factor: number;
31
+ /** Camera distance */
32
+ distance: number;
33
+ /** Camera aspect ratio: width / height */
34
+ aspect: number;
35
+ }
24
36
  export type RenderCallback = (state: RootState, delta: number, frame?: XRFrame) => void;
25
37
  export type UpdateCallback = RenderCallback;
26
38
  export type LegacyAlways = 'always';
@@ -106,10 +118,12 @@ export interface RootState {
106
118
  frameloop: FrameloopLegacy;
107
119
  /** Adaptive performance interface */
108
120
  performance: Performance;
109
- /** The current pixel ratio */
110
- dpr: number;
111
121
  /** Reactive pixel-size of the canvas */
112
122
  size: Size;
123
+ /** Reactive size of the viewport in threejs units */
124
+ viewport: Viewport & {
125
+ getCurrentViewport: (camera?: Camera, target?: THREE.Vector3 | Parameters<THREE.Vector3['set']>, size?: Size) => Omit<Viewport, 'dpr' | 'initialDpr'>;
126
+ };
113
127
  /** Flags the canvas for render, but doesn't render in itself */
114
128
  invalidate: (frames?: number) => void;
115
129
  /** Advance (render) one step */
@@ -60,7 +60,8 @@ const extend = objects => {
60
60
  return void Object.assign(catalogue, objects);
61
61
  }
62
62
  };
63
- function createInstance(type, props, root) {
63
+ function createInstance(type, props, root, flushPrimitive = true) {
64
+ var _props$object;
64
65
  // Get target from catalogue
65
66
  const name = `${type[0].toUpperCase()}${type.slice(1)}`;
66
67
  const target = catalogue[name];
@@ -74,10 +75,37 @@ function createInstance(type, props, root) {
74
75
  // Throw if an object or literal was passed for args
75
76
  if (props.args !== undefined && !Array.isArray(props.args)) throw new Error('R3F: The args prop must be an array!');
76
77
 
78
+ // Regenerate the R3F instance for primitives to simulate a new object
79
+ if (flushPrimitive && type === 'primitive' && (_props$object = props.object) != null && _props$object.__r3f) delete props.object.__r3f;
80
+
77
81
  // Create instance
78
82
  const instance = prepare(props.object, root, type, props);
79
83
  return instance;
80
84
  }
85
+ function hideInstance(instance) {
86
+ if (!instance.isHidden) {
87
+ var _instance$parent;
88
+ if (instance.props.attach && (_instance$parent = instance.parent) != null && _instance$parent.object) {
89
+ detach(instance.parent, instance);
90
+ } else if (isObject3D(instance.object)) {
91
+ instance.object.visible = false;
92
+ }
93
+ instance.isHidden = true;
94
+ invalidateInstance(instance);
95
+ }
96
+ }
97
+ function unhideInstance(instance) {
98
+ if (instance.isHidden) {
99
+ var _instance$parent2;
100
+ if (instance.props.attach && (_instance$parent2 = instance.parent) != null && _instance$parent2.object) {
101
+ attach(instance.parent, instance);
102
+ } else if (isObject3D(instance.object) && instance.props.visible !== false) {
103
+ instance.object.visible = true;
104
+ }
105
+ instance.isHidden = false;
106
+ invalidateInstance(instance);
107
+ }
108
+ }
81
109
 
82
110
  // https://github.com/facebook/react/issues/20271
83
111
  // This will make sure events and attach are only handled once when trees are complete
@@ -213,8 +241,19 @@ function setFiberInstance(fiber, instance) {
213
241
  }
214
242
  }
215
243
  function switchInstance(oldInstance, type, props, fiber) {
244
+ // If the old instance is hidden, we need to unhide it.
245
+ // React assumes it can discard instances since they're pure for DOM.
246
+ // This isn't true for us since our lifetimes are impure and longliving.
247
+ // So, we manually check if an instance was hidden and unhide it.
248
+ if (oldInstance.isHidden) unhideInstance(oldInstance);
249
+
216
250
  // Create a new instance
217
- const newInstance = createInstance(type, props, oldInstance.root);
251
+ const newInstance = createInstance(type, props, oldInstance.root, false);
252
+
253
+ // Update attach props for primitives since we don't flush them
254
+ if (type === 'primitive') {
255
+ newInstance.props.attach = props.attach;
256
+ }
218
257
 
219
258
  // Move children to new instance
220
259
  for (const child of oldInstance.children) {
@@ -262,7 +301,9 @@ const reconciler = Reconciler__default["default"]({
262
301
  supportsMutation: true,
263
302
  supportsPersistence: false,
264
303
  supportsHydration: false,
265
- createInstance,
304
+ createInstance(type, props, root) {
305
+ return createInstance(type, props, root);
306
+ },
266
307
  removeChild,
267
308
  appendChild,
268
309
  appendInitialChild: appendChild,
@@ -319,8 +360,8 @@ const reconciler = Reconciler__default["default"]({
319
360
  resetAfterCommit: () => {},
320
361
  shouldSetTextContent: () => false,
321
362
  clearContainer: () => false,
322
- hideInstance() {},
323
- unhideInstance() {},
363
+ hideInstance,
364
+ unhideInstance,
324
365
  createTextInstance: handleTextInstance,
325
366
  hideTextInstance: handleTextInstance,
326
367
  unhideTextInstance: handleTextInstance,
@@ -575,7 +616,8 @@ function prepare(target, root, type, props) {
575
616
  props: getInstanceProps(props),
576
617
  object,
577
618
  eventCount: 0,
578
- handlers: {}
619
+ handlers: {},
620
+ isHidden: false
579
621
  };
580
622
  if (object) {
581
623
  object.__r3f = instance;
@@ -1247,6 +1289,44 @@ const isRenderer = def => !!(def != null && def.render);
1247
1289
  const context = /*#__PURE__*/React__namespace.createContext(null);
1248
1290
  const createStore = (invalidate, advance) => {
1249
1291
  const rootStore = traditional.createWithEqualityFn((set, get) => {
1292
+ const position = new THREE__namespace.Vector3();
1293
+ const defaultTarget = new THREE__namespace.Vector3();
1294
+ const tempTarget = new THREE__namespace.Vector3();
1295
+ function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
1296
+ const {
1297
+ width,
1298
+ height,
1299
+ top,
1300
+ left
1301
+ } = size;
1302
+ const aspect = width / height;
1303
+ if (target instanceof THREE__namespace.Vector3) tempTarget.copy(target);else tempTarget.set(...target);
1304
+ const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
1305
+ if (isOrthographicCamera(camera)) {
1306
+ return {
1307
+ width: width / camera.zoom,
1308
+ height: height / camera.zoom,
1309
+ top,
1310
+ left,
1311
+ factor: 1,
1312
+ distance,
1313
+ aspect
1314
+ };
1315
+ } else {
1316
+ const fov = camera.fov * Math.PI / 180; // convert vertical fov to radians
1317
+ const h = 2 * Math.tan(fov / 2) * distance; // visible height
1318
+ const w = h * (width / height);
1319
+ return {
1320
+ width: w,
1321
+ height: h,
1322
+ top,
1323
+ left,
1324
+ factor: width / w,
1325
+ distance,
1326
+ aspect
1327
+ };
1328
+ }
1329
+ }
1250
1330
  let performanceTimeout = undefined;
1251
1331
  const setPerformanceCurrent = current => set(state => ({
1252
1332
  performance: {
@@ -1295,13 +1375,24 @@ const createStore = (invalidate, advance) => {
1295
1375
  performanceTimeout = setTimeout(() => setPerformanceCurrent(get().performance.max), state.performance.debounce);
1296
1376
  }
1297
1377
  },
1298
- dpr: 1,
1299
1378
  size: {
1300
1379
  width: 0,
1301
1380
  height: 0,
1302
1381
  top: 0,
1303
1382
  left: 0
1304
1383
  },
1384
+ viewport: {
1385
+ initialDpr: 0,
1386
+ dpr: 0,
1387
+ width: 0,
1388
+ height: 0,
1389
+ top: 0,
1390
+ left: 0,
1391
+ aspect: 0,
1392
+ distance: 0,
1393
+ factor: 0,
1394
+ getCurrentViewport
1395
+ },
1305
1396
  setEvents: events => set(state => ({
1306
1397
  ...state,
1307
1398
  events: {
@@ -1310,17 +1401,30 @@ const createStore = (invalidate, advance) => {
1310
1401
  }
1311
1402
  })),
1312
1403
  setSize: (width, height, top = 0, left = 0) => {
1313
- set({
1314
- size: {
1315
- width,
1316
- height,
1317
- top,
1318
- left
1404
+ const camera = get().camera;
1405
+ const size = {
1406
+ width,
1407
+ height,
1408
+ top,
1409
+ left
1410
+ };
1411
+ set(state => ({
1412
+ size,
1413
+ viewport: {
1414
+ ...state.viewport,
1415
+ ...getCurrentViewport(camera, defaultTarget, size)
1319
1416
  }
1320
- });
1417
+ }));
1321
1418
  },
1322
- setDpr: dpr => set({
1323
- dpr: calculateDpr(dpr)
1419
+ setDpr: dpr => set(state => {
1420
+ const resolved = calculateDpr(dpr);
1421
+ return {
1422
+ viewport: {
1423
+ ...state.viewport,
1424
+ dpr: resolved,
1425
+ initialDpr: state.viewport.initialDpr || resolved
1426
+ }
1427
+ };
1324
1428
  }),
1325
1429
  setFrameloop: frameloop => {
1326
1430
  var _frameloop$mode, _frameloop$render, _frameloop$maxDelta;
@@ -1409,23 +1513,39 @@ const createStore = (invalidate, advance) => {
1409
1513
  });
1410
1514
  const state = rootStore.getState();
1411
1515
  let oldSize = state.size;
1412
- let oldDpr = state.dpr;
1413
- rootStore.subscribe(({
1414
- camera,
1415
- size,
1416
- dpr,
1417
- gl
1418
- }) => {
1516
+ let oldDpr = state.viewport.dpr;
1517
+ let oldCamera = state.camera;
1518
+ rootStore.subscribe(() => {
1519
+ const {
1520
+ camera,
1521
+ size,
1522
+ viewport,
1523
+ gl,
1524
+ set
1525
+ } = rootStore.getState();
1526
+
1419
1527
  // Resize camera and renderer on changes to size and pixelratio
1420
- if (size !== oldSize || dpr !== oldDpr) {
1528
+ if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
1421
1529
  oldSize = size;
1422
- oldDpr = dpr;
1530
+ oldDpr = viewport.dpr;
1423
1531
  // Update camera & renderer
1424
1532
  updateCamera(camera, size);
1425
- gl.setPixelRatio(dpr);
1533
+ gl.setPixelRatio(viewport.dpr);
1426
1534
  const updateStyle = typeof HTMLCanvasElement !== 'undefined' && gl.domElement instanceof HTMLCanvasElement;
1427
1535
  gl.setSize(size.width, size.height, updateStyle);
1428
1536
  }
1537
+
1538
+ // Update viewport once the camera changes
1539
+ if (camera !== oldCamera) {
1540
+ oldCamera = camera;
1541
+ // Update viewport
1542
+ set(state => ({
1543
+ viewport: {
1544
+ ...state.viewport,
1545
+ ...state.viewport.getCurrentViewport(camera)
1546
+ }
1547
+ }));
1548
+ }
1429
1549
  });
1430
1550
 
1431
1551
  // Invalidate on any change
@@ -1570,15 +1690,20 @@ const Lifecycle = [Early, Fixed, Update, Late, Render, After];
1570
1690
 
1571
1691
  /**
1572
1692
  * Exposes an object's {@link Instance}.
1573
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useInstanceHandle
1693
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useinstancehandle
1574
1694
  *
1575
1695
  * **Note**: this is an escape hatch to react-internal fields. Expect this to change significantly between versions.
1576
1696
  */
1577
1697
  function useInstanceHandle(ref) {
1578
1698
  const instance = React__namespace.useRef(null);
1579
- useIsomorphicLayoutEffect(() => void (instance.current = ref.current.__r3f), [ref]);
1699
+ React__namespace.useImperativeHandle(instance, () => ref.current.__r3f, [ref]);
1580
1700
  return instance;
1581
1701
  }
1702
+
1703
+ /**
1704
+ * Returns the R3F Canvas' Zustand store. Useful for [transient updates](https://github.com/pmndrs/zustand#transient-updates-for-often-occurring-state-changes).
1705
+ * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usestore
1706
+ */
1582
1707
  function useStore() {
1583
1708
  const store = React__namespace.useContext(context);
1584
1709
  if (!store) throw new Error('R3F: Hooks can only be used within the Canvas component!');
@@ -1610,7 +1735,7 @@ function useFrame(callback, renderPriority = 0) {
1610
1735
 
1611
1736
  /**
1612
1737
  * Executes a callback in a given update stage.
1613
- * Uses the stage instance to indetify which stage to target in the lifecycle.
1738
+ * Uses the stage instance to identify which stage to target in the lifecycle.
1614
1739
  */
1615
1740
  function useUpdate(callback, stage = Stages.Update) {
1616
1741
  const store = useStore();
@@ -1657,6 +1782,7 @@ function loadingFn(extensions, onProgress) {
1657
1782
  return Promise.all(input.map(input => new Promise((res, reject) => loader.load(input, data => res(isObject3D(data == null ? void 0 : data.scene) ? Object.assign(data, buildGraph(data.scene)) : data), onProgress, error => reject(new Error(`Could not load ${input}: ${error == null ? void 0 : error.message}`))))));
1658
1783
  };
1659
1784
  }
1785
+
1660
1786
  /**
1661
1787
  * Synchronously loads and caches assets with a three loader.
1662
1788
  *
@@ -1880,7 +2006,15 @@ function createRoot(canvas) {
1880
2006
  const camera = isCamera ? cameraOptions : orthographic ? new THREE__namespace.OrthographicCamera(0, 0, 0, 0, 0.1, 1000) : new THREE__namespace.PerspectiveCamera(75, 0, 0.1, 1000);
1881
2007
  if (!isCamera) {
1882
2008
  camera.position.z = 5;
1883
- if (cameraOptions) applyProps(camera, cameraOptions);
2009
+ if (cameraOptions) {
2010
+ applyProps(camera, cameraOptions);
2011
+ // Preserve user-defined frustum if possible
2012
+ // https://github.com/pmndrs/react-three-fiber/issues/3160
2013
+ if ('aspect' in cameraOptions || 'left' in cameraOptions || 'right' in cameraOptions || 'bottom' in cameraOptions || 'top' in cameraOptions) {
2014
+ camera.manual = true;
2015
+ camera.updateProjectionMatrix();
2016
+ }
2017
+ }
1884
2018
  // Always look at center by default
1885
2019
  if (!state.camera && !(cameraOptions != null && cameraOptions.rotation)) camera.lookAt(0, 0, 0);
1886
2020
  }
@@ -2009,7 +2143,7 @@ function createRoot(canvas) {
2009
2143
  state.setSize(size.width, size.height, size.top, size.left);
2010
2144
  }
2011
2145
  // Check pixelratio
2012
- if (dpr && state.dpr !== calculateDpr(dpr)) state.setDpr(dpr);
2146
+ if (dpr && state.viewport.dpr !== calculateDpr(dpr)) state.setDpr(dpr);
2013
2147
  // Check frameloop
2014
2148
  if (state.frameloop !== frameloop) state.setFrameloop(frameloop);
2015
2149
  // Check pointer missed
@@ -2132,8 +2266,11 @@ function Portal({
2132
2266
  const [raycaster] = React__namespace.useState(() => new THREE__namespace.Raycaster());
2133
2267
  const [pointer] = React__namespace.useState(() => new THREE__namespace.Vector2());
2134
2268
  const inject = useMutableCallback((rootState, injectState) => {
2269
+ let viewport;
2135
2270
  if (injectState.camera && size) {
2136
2271
  const camera = injectState.camera;
2272
+ // Calculate the override viewport, if present
2273
+ viewport = rootState.viewport.getCurrentViewport(camera, new THREE__namespace.Vector3(), size);
2137
2274
  // Update the portal camera, if it differs from the previous layer
2138
2275
  if (camera !== rootState.camera) updateCamera(camera, size);
2139
2276
  }
@@ -2149,7 +2286,7 @@ function Portal({
2149
2286
  mouse: pointer,
2150
2287
  // Their previous root is the layer before it
2151
2288
  previousRoot,
2152
- // Events and size can be overridden by the inject layer
2289
+ // Events, size and viewport can be overridden by the inject layer
2153
2290
  events: {
2154
2291
  ...rootState.events,
2155
2292
  ...injectState.events,
@@ -2159,6 +2296,10 @@ function Portal({
2159
2296
  ...rootState.size,
2160
2297
  ...size
2161
2298
  },
2299
+ viewport: {
2300
+ ...rootState.viewport,
2301
+ ...viewport
2302
+ },
2162
2303
  // Layers are allowed to override events
2163
2304
  setEvents: events => injectState.set(state => ({
2164
2305
  ...state,
@@ -60,7 +60,8 @@ const extend = objects => {
60
60
  return void Object.assign(catalogue, objects);
61
61
  }
62
62
  };
63
- function createInstance(type, props, root) {
63
+ function createInstance(type, props, root, flushPrimitive = true) {
64
+ var _props$object;
64
65
  // Get target from catalogue
65
66
  const name = `${type[0].toUpperCase()}${type.slice(1)}`;
66
67
  const target = catalogue[name];
@@ -74,10 +75,37 @@ function createInstance(type, props, root) {
74
75
  // Throw if an object or literal was passed for args
75
76
  if (props.args !== undefined && !Array.isArray(props.args)) throw new Error('R3F: The args prop must be an array!');
76
77
 
78
+ // Regenerate the R3F instance for primitives to simulate a new object
79
+ if (flushPrimitive && type === 'primitive' && (_props$object = props.object) != null && _props$object.__r3f) delete props.object.__r3f;
80
+
77
81
  // Create instance
78
82
  const instance = prepare(props.object, root, type, props);
79
83
  return instance;
80
84
  }
85
+ function hideInstance(instance) {
86
+ if (!instance.isHidden) {
87
+ var _instance$parent;
88
+ if (instance.props.attach && (_instance$parent = instance.parent) != null && _instance$parent.object) {
89
+ detach(instance.parent, instance);
90
+ } else if (isObject3D(instance.object)) {
91
+ instance.object.visible = false;
92
+ }
93
+ instance.isHidden = true;
94
+ invalidateInstance(instance);
95
+ }
96
+ }
97
+ function unhideInstance(instance) {
98
+ if (instance.isHidden) {
99
+ var _instance$parent2;
100
+ if (instance.props.attach && (_instance$parent2 = instance.parent) != null && _instance$parent2.object) {
101
+ attach(instance.parent, instance);
102
+ } else if (isObject3D(instance.object) && instance.props.visible !== false) {
103
+ instance.object.visible = true;
104
+ }
105
+ instance.isHidden = false;
106
+ invalidateInstance(instance);
107
+ }
108
+ }
81
109
 
82
110
  // https://github.com/facebook/react/issues/20271
83
111
  // This will make sure events and attach are only handled once when trees are complete
@@ -213,8 +241,19 @@ function setFiberInstance(fiber, instance) {
213
241
  }
214
242
  }
215
243
  function switchInstance(oldInstance, type, props, fiber) {
244
+ // If the old instance is hidden, we need to unhide it.
245
+ // React assumes it can discard instances since they're pure for DOM.
246
+ // This isn't true for us since our lifetimes are impure and longliving.
247
+ // So, we manually check if an instance was hidden and unhide it.
248
+ if (oldInstance.isHidden) unhideInstance(oldInstance);
249
+
216
250
  // Create a new instance
217
- const newInstance = createInstance(type, props, oldInstance.root);
251
+ const newInstance = createInstance(type, props, oldInstance.root, false);
252
+
253
+ // Update attach props for primitives since we don't flush them
254
+ if (type === 'primitive') {
255
+ newInstance.props.attach = props.attach;
256
+ }
218
257
 
219
258
  // Move children to new instance
220
259
  for (const child of oldInstance.children) {
@@ -262,7 +301,9 @@ const reconciler = Reconciler__default["default"]({
262
301
  supportsMutation: true,
263
302
  supportsPersistence: false,
264
303
  supportsHydration: false,
265
- createInstance,
304
+ createInstance(type, props, root) {
305
+ return createInstance(type, props, root);
306
+ },
266
307
  removeChild,
267
308
  appendChild,
268
309
  appendInitialChild: appendChild,
@@ -319,8 +360,8 @@ const reconciler = Reconciler__default["default"]({
319
360
  resetAfterCommit: () => {},
320
361
  shouldSetTextContent: () => false,
321
362
  clearContainer: () => false,
322
- hideInstance() {},
323
- unhideInstance() {},
363
+ hideInstance,
364
+ unhideInstance,
324
365
  createTextInstance: handleTextInstance,
325
366
  hideTextInstance: handleTextInstance,
326
367
  unhideTextInstance: handleTextInstance,
@@ -575,7 +616,8 @@ function prepare(target, root, type, props) {
575
616
  props: getInstanceProps(props),
576
617
  object,
577
618
  eventCount: 0,
578
- handlers: {}
619
+ handlers: {},
620
+ isHidden: false
579
621
  };
580
622
  if (object) {
581
623
  object.__r3f = instance;
@@ -1247,6 +1289,44 @@ const isRenderer = def => !!(def != null && def.render);
1247
1289
  const context = /*#__PURE__*/React__namespace.createContext(null);
1248
1290
  const createStore = (invalidate, advance) => {
1249
1291
  const rootStore = traditional.createWithEqualityFn((set, get) => {
1292
+ const position = new THREE__namespace.Vector3();
1293
+ const defaultTarget = new THREE__namespace.Vector3();
1294
+ const tempTarget = new THREE__namespace.Vector3();
1295
+ function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
1296
+ const {
1297
+ width,
1298
+ height,
1299
+ top,
1300
+ left
1301
+ } = size;
1302
+ const aspect = width / height;
1303
+ if (target instanceof THREE__namespace.Vector3) tempTarget.copy(target);else tempTarget.set(...target);
1304
+ const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
1305
+ if (isOrthographicCamera(camera)) {
1306
+ return {
1307
+ width: width / camera.zoom,
1308
+ height: height / camera.zoom,
1309
+ top,
1310
+ left,
1311
+ factor: 1,
1312
+ distance,
1313
+ aspect
1314
+ };
1315
+ } else {
1316
+ const fov = camera.fov * Math.PI / 180; // convert vertical fov to radians
1317
+ const h = 2 * Math.tan(fov / 2) * distance; // visible height
1318
+ const w = h * (width / height);
1319
+ return {
1320
+ width: w,
1321
+ height: h,
1322
+ top,
1323
+ left,
1324
+ factor: width / w,
1325
+ distance,
1326
+ aspect
1327
+ };
1328
+ }
1329
+ }
1250
1330
  let performanceTimeout = undefined;
1251
1331
  const setPerformanceCurrent = current => set(state => ({
1252
1332
  performance: {
@@ -1295,13 +1375,24 @@ const createStore = (invalidate, advance) => {
1295
1375
  performanceTimeout = setTimeout(() => setPerformanceCurrent(get().performance.max), state.performance.debounce);
1296
1376
  }
1297
1377
  },
1298
- dpr: 1,
1299
1378
  size: {
1300
1379
  width: 0,
1301
1380
  height: 0,
1302
1381
  top: 0,
1303
1382
  left: 0
1304
1383
  },
1384
+ viewport: {
1385
+ initialDpr: 0,
1386
+ dpr: 0,
1387
+ width: 0,
1388
+ height: 0,
1389
+ top: 0,
1390
+ left: 0,
1391
+ aspect: 0,
1392
+ distance: 0,
1393
+ factor: 0,
1394
+ getCurrentViewport
1395
+ },
1305
1396
  setEvents: events => set(state => ({
1306
1397
  ...state,
1307
1398
  events: {
@@ -1310,17 +1401,30 @@ const createStore = (invalidate, advance) => {
1310
1401
  }
1311
1402
  })),
1312
1403
  setSize: (width, height, top = 0, left = 0) => {
1313
- set({
1314
- size: {
1315
- width,
1316
- height,
1317
- top,
1318
- left
1404
+ const camera = get().camera;
1405
+ const size = {
1406
+ width,
1407
+ height,
1408
+ top,
1409
+ left
1410
+ };
1411
+ set(state => ({
1412
+ size,
1413
+ viewport: {
1414
+ ...state.viewport,
1415
+ ...getCurrentViewport(camera, defaultTarget, size)
1319
1416
  }
1320
- });
1417
+ }));
1321
1418
  },
1322
- setDpr: dpr => set({
1323
- dpr: calculateDpr(dpr)
1419
+ setDpr: dpr => set(state => {
1420
+ const resolved = calculateDpr(dpr);
1421
+ return {
1422
+ viewport: {
1423
+ ...state.viewport,
1424
+ dpr: resolved,
1425
+ initialDpr: state.viewport.initialDpr || resolved
1426
+ }
1427
+ };
1324
1428
  }),
1325
1429
  setFrameloop: frameloop => {
1326
1430
  var _frameloop$mode, _frameloop$render, _frameloop$maxDelta;
@@ -1409,23 +1513,39 @@ const createStore = (invalidate, advance) => {
1409
1513
  });
1410
1514
  const state = rootStore.getState();
1411
1515
  let oldSize = state.size;
1412
- let oldDpr = state.dpr;
1413
- rootStore.subscribe(({
1414
- camera,
1415
- size,
1416
- dpr,
1417
- gl
1418
- }) => {
1516
+ let oldDpr = state.viewport.dpr;
1517
+ let oldCamera = state.camera;
1518
+ rootStore.subscribe(() => {
1519
+ const {
1520
+ camera,
1521
+ size,
1522
+ viewport,
1523
+ gl,
1524
+ set
1525
+ } = rootStore.getState();
1526
+
1419
1527
  // Resize camera and renderer on changes to size and pixelratio
1420
- if (size !== oldSize || dpr !== oldDpr) {
1528
+ if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
1421
1529
  oldSize = size;
1422
- oldDpr = dpr;
1530
+ oldDpr = viewport.dpr;
1423
1531
  // Update camera & renderer
1424
1532
  updateCamera(camera, size);
1425
- gl.setPixelRatio(dpr);
1533
+ gl.setPixelRatio(viewport.dpr);
1426
1534
  const updateStyle = typeof HTMLCanvasElement !== 'undefined' && gl.domElement instanceof HTMLCanvasElement;
1427
1535
  gl.setSize(size.width, size.height, updateStyle);
1428
1536
  }
1537
+
1538
+ // Update viewport once the camera changes
1539
+ if (camera !== oldCamera) {
1540
+ oldCamera = camera;
1541
+ // Update viewport
1542
+ set(state => ({
1543
+ viewport: {
1544
+ ...state.viewport,
1545
+ ...state.viewport.getCurrentViewport(camera)
1546
+ }
1547
+ }));
1548
+ }
1429
1549
  });
1430
1550
 
1431
1551
  // Invalidate on any change
@@ -1570,15 +1690,20 @@ const Lifecycle = [Early, Fixed, Update, Late, Render, After];
1570
1690
 
1571
1691
  /**
1572
1692
  * Exposes an object's {@link Instance}.
1573
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useInstanceHandle
1693
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useinstancehandle
1574
1694
  *
1575
1695
  * **Note**: this is an escape hatch to react-internal fields. Expect this to change significantly between versions.
1576
1696
  */
1577
1697
  function useInstanceHandle(ref) {
1578
1698
  const instance = React__namespace.useRef(null);
1579
- useIsomorphicLayoutEffect(() => void (instance.current = ref.current.__r3f), [ref]);
1699
+ React__namespace.useImperativeHandle(instance, () => ref.current.__r3f, [ref]);
1580
1700
  return instance;
1581
1701
  }
1702
+
1703
+ /**
1704
+ * Returns the R3F Canvas' Zustand store. Useful for [transient updates](https://github.com/pmndrs/zustand#transient-updates-for-often-occurring-state-changes).
1705
+ * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usestore
1706
+ */
1582
1707
  function useStore() {
1583
1708
  const store = React__namespace.useContext(context);
1584
1709
  if (!store) throw new Error('R3F: Hooks can only be used within the Canvas component!');
@@ -1610,7 +1735,7 @@ function useFrame(callback, renderPriority = 0) {
1610
1735
 
1611
1736
  /**
1612
1737
  * Executes a callback in a given update stage.
1613
- * Uses the stage instance to indetify which stage to target in the lifecycle.
1738
+ * Uses the stage instance to identify which stage to target in the lifecycle.
1614
1739
  */
1615
1740
  function useUpdate(callback, stage = Stages.Update) {
1616
1741
  const store = useStore();
@@ -1657,6 +1782,7 @@ function loadingFn(extensions, onProgress) {
1657
1782
  return Promise.all(input.map(input => new Promise((res, reject) => loader.load(input, data => res(isObject3D(data == null ? void 0 : data.scene) ? Object.assign(data, buildGraph(data.scene)) : data), onProgress, error => reject(new Error(`Could not load ${input}: ${error == null ? void 0 : error.message}`))))));
1658
1783
  };
1659
1784
  }
1785
+
1660
1786
  /**
1661
1787
  * Synchronously loads and caches assets with a three loader.
1662
1788
  *
@@ -1880,7 +2006,15 @@ function createRoot(canvas) {
1880
2006
  const camera = isCamera ? cameraOptions : orthographic ? new THREE__namespace.OrthographicCamera(0, 0, 0, 0, 0.1, 1000) : new THREE__namespace.PerspectiveCamera(75, 0, 0.1, 1000);
1881
2007
  if (!isCamera) {
1882
2008
  camera.position.z = 5;
1883
- if (cameraOptions) applyProps(camera, cameraOptions);
2009
+ if (cameraOptions) {
2010
+ applyProps(camera, cameraOptions);
2011
+ // Preserve user-defined frustum if possible
2012
+ // https://github.com/pmndrs/react-three-fiber/issues/3160
2013
+ if ('aspect' in cameraOptions || 'left' in cameraOptions || 'right' in cameraOptions || 'bottom' in cameraOptions || 'top' in cameraOptions) {
2014
+ camera.manual = true;
2015
+ camera.updateProjectionMatrix();
2016
+ }
2017
+ }
1884
2018
  // Always look at center by default
1885
2019
  if (!state.camera && !(cameraOptions != null && cameraOptions.rotation)) camera.lookAt(0, 0, 0);
1886
2020
  }
@@ -2009,7 +2143,7 @@ function createRoot(canvas) {
2009
2143
  state.setSize(size.width, size.height, size.top, size.left);
2010
2144
  }
2011
2145
  // Check pixelratio
2012
- if (dpr && state.dpr !== calculateDpr(dpr)) state.setDpr(dpr);
2146
+ if (dpr && state.viewport.dpr !== calculateDpr(dpr)) state.setDpr(dpr);
2013
2147
  // Check frameloop
2014
2148
  if (state.frameloop !== frameloop) state.setFrameloop(frameloop);
2015
2149
  // Check pointer missed
@@ -2132,8 +2266,11 @@ function Portal({
2132
2266
  const [raycaster] = React__namespace.useState(() => new THREE__namespace.Raycaster());
2133
2267
  const [pointer] = React__namespace.useState(() => new THREE__namespace.Vector2());
2134
2268
  const inject = useMutableCallback((rootState, injectState) => {
2269
+ let viewport;
2135
2270
  if (injectState.camera && size) {
2136
2271
  const camera = injectState.camera;
2272
+ // Calculate the override viewport, if present
2273
+ viewport = rootState.viewport.getCurrentViewport(camera, new THREE__namespace.Vector3(), size);
2137
2274
  // Update the portal camera, if it differs from the previous layer
2138
2275
  if (camera !== rootState.camera) updateCamera(camera, size);
2139
2276
  }
@@ -2149,7 +2286,7 @@ function Portal({
2149
2286
  mouse: pointer,
2150
2287
  // Their previous root is the layer before it
2151
2288
  previousRoot,
2152
- // Events and size can be overridden by the inject layer
2289
+ // Events, size and viewport can be overridden by the inject layer
2153
2290
  events: {
2154
2291
  ...rootState.events,
2155
2292
  ...injectState.events,
@@ -2159,6 +2296,10 @@ function Portal({
2159
2296
  ...rootState.size,
2160
2297
  ...size
2161
2298
  },
2299
+ viewport: {
2300
+ ...rootState.viewport,
2301
+ ...viewport
2302
+ },
2162
2303
  // Layers are allowed to override events
2163
2304
  setEvents: events => injectState.set(state => ({
2164
2305
  ...state,
@@ -34,7 +34,8 @@ const extend = objects => {
34
34
  return void Object.assign(catalogue, objects);
35
35
  }
36
36
  };
37
- function createInstance(type, props, root) {
37
+ function createInstance(type, props, root, flushPrimitive = true) {
38
+ var _props$object;
38
39
  // Get target from catalogue
39
40
  const name = `${type[0].toUpperCase()}${type.slice(1)}`;
40
41
  const target = catalogue[name];
@@ -48,10 +49,37 @@ function createInstance(type, props, root) {
48
49
  // Throw if an object or literal was passed for args
49
50
  if (props.args !== undefined && !Array.isArray(props.args)) throw new Error('R3F: The args prop must be an array!');
50
51
 
52
+ // Regenerate the R3F instance for primitives to simulate a new object
53
+ if (flushPrimitive && type === 'primitive' && (_props$object = props.object) != null && _props$object.__r3f) delete props.object.__r3f;
54
+
51
55
  // Create instance
52
56
  const instance = prepare(props.object, root, type, props);
53
57
  return instance;
54
58
  }
59
+ function hideInstance(instance) {
60
+ if (!instance.isHidden) {
61
+ var _instance$parent;
62
+ if (instance.props.attach && (_instance$parent = instance.parent) != null && _instance$parent.object) {
63
+ detach(instance.parent, instance);
64
+ } else if (isObject3D(instance.object)) {
65
+ instance.object.visible = false;
66
+ }
67
+ instance.isHidden = true;
68
+ invalidateInstance(instance);
69
+ }
70
+ }
71
+ function unhideInstance(instance) {
72
+ if (instance.isHidden) {
73
+ var _instance$parent2;
74
+ if (instance.props.attach && (_instance$parent2 = instance.parent) != null && _instance$parent2.object) {
75
+ attach(instance.parent, instance);
76
+ } else if (isObject3D(instance.object) && instance.props.visible !== false) {
77
+ instance.object.visible = true;
78
+ }
79
+ instance.isHidden = false;
80
+ invalidateInstance(instance);
81
+ }
82
+ }
55
83
 
56
84
  // https://github.com/facebook/react/issues/20271
57
85
  // This will make sure events and attach are only handled once when trees are complete
@@ -187,8 +215,19 @@ function setFiberInstance(fiber, instance) {
187
215
  }
188
216
  }
189
217
  function switchInstance(oldInstance, type, props, fiber) {
218
+ // If the old instance is hidden, we need to unhide it.
219
+ // React assumes it can discard instances since they're pure for DOM.
220
+ // This isn't true for us since our lifetimes are impure and longliving.
221
+ // So, we manually check if an instance was hidden and unhide it.
222
+ if (oldInstance.isHidden) unhideInstance(oldInstance);
223
+
190
224
  // Create a new instance
191
- const newInstance = createInstance(type, props, oldInstance.root);
225
+ const newInstance = createInstance(type, props, oldInstance.root, false);
226
+
227
+ // Update attach props for primitives since we don't flush them
228
+ if (type === 'primitive') {
229
+ newInstance.props.attach = props.attach;
230
+ }
192
231
 
193
232
  // Move children to new instance
194
233
  for (const child of oldInstance.children) {
@@ -236,7 +275,9 @@ const reconciler = Reconciler({
236
275
  supportsMutation: true,
237
276
  supportsPersistence: false,
238
277
  supportsHydration: false,
239
- createInstance,
278
+ createInstance(type, props, root) {
279
+ return createInstance(type, props, root);
280
+ },
240
281
  removeChild,
241
282
  appendChild,
242
283
  appendInitialChild: appendChild,
@@ -293,8 +334,8 @@ const reconciler = Reconciler({
293
334
  resetAfterCommit: () => {},
294
335
  shouldSetTextContent: () => false,
295
336
  clearContainer: () => false,
296
- hideInstance() {},
297
- unhideInstance() {},
337
+ hideInstance,
338
+ unhideInstance,
298
339
  createTextInstance: handleTextInstance,
299
340
  hideTextInstance: handleTextInstance,
300
341
  unhideTextInstance: handleTextInstance,
@@ -549,7 +590,8 @@ function prepare(target, root, type, props) {
549
590
  props: getInstanceProps(props),
550
591
  object,
551
592
  eventCount: 0,
552
- handlers: {}
593
+ handlers: {},
594
+ isHidden: false
553
595
  };
554
596
  if (object) {
555
597
  object.__r3f = instance;
@@ -1221,6 +1263,44 @@ const isRenderer = def => !!(def != null && def.render);
1221
1263
  const context = /*#__PURE__*/React.createContext(null);
1222
1264
  const createStore = (invalidate, advance) => {
1223
1265
  const rootStore = createWithEqualityFn((set, get) => {
1266
+ const position = new THREE.Vector3();
1267
+ const defaultTarget = new THREE.Vector3();
1268
+ const tempTarget = new THREE.Vector3();
1269
+ function getCurrentViewport(camera = get().camera, target = defaultTarget, size = get().size) {
1270
+ const {
1271
+ width,
1272
+ height,
1273
+ top,
1274
+ left
1275
+ } = size;
1276
+ const aspect = width / height;
1277
+ if (target instanceof THREE.Vector3) tempTarget.copy(target);else tempTarget.set(...target);
1278
+ const distance = camera.getWorldPosition(position).distanceTo(tempTarget);
1279
+ if (isOrthographicCamera(camera)) {
1280
+ return {
1281
+ width: width / camera.zoom,
1282
+ height: height / camera.zoom,
1283
+ top,
1284
+ left,
1285
+ factor: 1,
1286
+ distance,
1287
+ aspect
1288
+ };
1289
+ } else {
1290
+ const fov = camera.fov * Math.PI / 180; // convert vertical fov to radians
1291
+ const h = 2 * Math.tan(fov / 2) * distance; // visible height
1292
+ const w = h * (width / height);
1293
+ return {
1294
+ width: w,
1295
+ height: h,
1296
+ top,
1297
+ left,
1298
+ factor: width / w,
1299
+ distance,
1300
+ aspect
1301
+ };
1302
+ }
1303
+ }
1224
1304
  let performanceTimeout = undefined;
1225
1305
  const setPerformanceCurrent = current => set(state => ({
1226
1306
  performance: {
@@ -1269,13 +1349,24 @@ const createStore = (invalidate, advance) => {
1269
1349
  performanceTimeout = setTimeout(() => setPerformanceCurrent(get().performance.max), state.performance.debounce);
1270
1350
  }
1271
1351
  },
1272
- dpr: 1,
1273
1352
  size: {
1274
1353
  width: 0,
1275
1354
  height: 0,
1276
1355
  top: 0,
1277
1356
  left: 0
1278
1357
  },
1358
+ viewport: {
1359
+ initialDpr: 0,
1360
+ dpr: 0,
1361
+ width: 0,
1362
+ height: 0,
1363
+ top: 0,
1364
+ left: 0,
1365
+ aspect: 0,
1366
+ distance: 0,
1367
+ factor: 0,
1368
+ getCurrentViewport
1369
+ },
1279
1370
  setEvents: events => set(state => ({
1280
1371
  ...state,
1281
1372
  events: {
@@ -1284,17 +1375,30 @@ const createStore = (invalidate, advance) => {
1284
1375
  }
1285
1376
  })),
1286
1377
  setSize: (width, height, top = 0, left = 0) => {
1287
- set({
1288
- size: {
1289
- width,
1290
- height,
1291
- top,
1292
- left
1378
+ const camera = get().camera;
1379
+ const size = {
1380
+ width,
1381
+ height,
1382
+ top,
1383
+ left
1384
+ };
1385
+ set(state => ({
1386
+ size,
1387
+ viewport: {
1388
+ ...state.viewport,
1389
+ ...getCurrentViewport(camera, defaultTarget, size)
1293
1390
  }
1294
- });
1391
+ }));
1295
1392
  },
1296
- setDpr: dpr => set({
1297
- dpr: calculateDpr(dpr)
1393
+ setDpr: dpr => set(state => {
1394
+ const resolved = calculateDpr(dpr);
1395
+ return {
1396
+ viewport: {
1397
+ ...state.viewport,
1398
+ dpr: resolved,
1399
+ initialDpr: state.viewport.initialDpr || resolved
1400
+ }
1401
+ };
1298
1402
  }),
1299
1403
  setFrameloop: frameloop => {
1300
1404
  var _frameloop$mode, _frameloop$render, _frameloop$maxDelta;
@@ -1383,23 +1487,39 @@ const createStore = (invalidate, advance) => {
1383
1487
  });
1384
1488
  const state = rootStore.getState();
1385
1489
  let oldSize = state.size;
1386
- let oldDpr = state.dpr;
1387
- rootStore.subscribe(({
1388
- camera,
1389
- size,
1390
- dpr,
1391
- gl
1392
- }) => {
1490
+ let oldDpr = state.viewport.dpr;
1491
+ let oldCamera = state.camera;
1492
+ rootStore.subscribe(() => {
1493
+ const {
1494
+ camera,
1495
+ size,
1496
+ viewport,
1497
+ gl,
1498
+ set
1499
+ } = rootStore.getState();
1500
+
1393
1501
  // Resize camera and renderer on changes to size and pixelratio
1394
- if (size !== oldSize || dpr !== oldDpr) {
1502
+ if (size.width !== oldSize.width || size.height !== oldSize.height || viewport.dpr !== oldDpr) {
1395
1503
  oldSize = size;
1396
- oldDpr = dpr;
1504
+ oldDpr = viewport.dpr;
1397
1505
  // Update camera & renderer
1398
1506
  updateCamera(camera, size);
1399
- gl.setPixelRatio(dpr);
1507
+ gl.setPixelRatio(viewport.dpr);
1400
1508
  const updateStyle = typeof HTMLCanvasElement !== 'undefined' && gl.domElement instanceof HTMLCanvasElement;
1401
1509
  gl.setSize(size.width, size.height, updateStyle);
1402
1510
  }
1511
+
1512
+ // Update viewport once the camera changes
1513
+ if (camera !== oldCamera) {
1514
+ oldCamera = camera;
1515
+ // Update viewport
1516
+ set(state => ({
1517
+ viewport: {
1518
+ ...state.viewport,
1519
+ ...state.viewport.getCurrentViewport(camera)
1520
+ }
1521
+ }));
1522
+ }
1403
1523
  });
1404
1524
 
1405
1525
  // Invalidate on any change
@@ -1544,15 +1664,20 @@ const Lifecycle = [Early, Fixed, Update, Late, Render, After];
1544
1664
 
1545
1665
  /**
1546
1666
  * Exposes an object's {@link Instance}.
1547
- * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useInstanceHandle
1667
+ * @see https://docs.pmnd.rs/react-three-fiber/api/additional-exports#useinstancehandle
1548
1668
  *
1549
1669
  * **Note**: this is an escape hatch to react-internal fields. Expect this to change significantly between versions.
1550
1670
  */
1551
1671
  function useInstanceHandle(ref) {
1552
1672
  const instance = React.useRef(null);
1553
- useIsomorphicLayoutEffect(() => void (instance.current = ref.current.__r3f), [ref]);
1673
+ React.useImperativeHandle(instance, () => ref.current.__r3f, [ref]);
1554
1674
  return instance;
1555
1675
  }
1676
+
1677
+ /**
1678
+ * Returns the R3F Canvas' Zustand store. Useful for [transient updates](https://github.com/pmndrs/zustand#transient-updates-for-often-occurring-state-changes).
1679
+ * @see https://docs.pmnd.rs/react-three-fiber/api/hooks#usestore
1680
+ */
1556
1681
  function useStore() {
1557
1682
  const store = React.useContext(context);
1558
1683
  if (!store) throw new Error('R3F: Hooks can only be used within the Canvas component!');
@@ -1584,7 +1709,7 @@ function useFrame(callback, renderPriority = 0) {
1584
1709
 
1585
1710
  /**
1586
1711
  * Executes a callback in a given update stage.
1587
- * Uses the stage instance to indetify which stage to target in the lifecycle.
1712
+ * Uses the stage instance to identify which stage to target in the lifecycle.
1588
1713
  */
1589
1714
  function useUpdate(callback, stage = Stages.Update) {
1590
1715
  const store = useStore();
@@ -1631,6 +1756,7 @@ function loadingFn(extensions, onProgress) {
1631
1756
  return Promise.all(input.map(input => new Promise((res, reject) => loader.load(input, data => res(isObject3D(data == null ? void 0 : data.scene) ? Object.assign(data, buildGraph(data.scene)) : data), onProgress, error => reject(new Error(`Could not load ${input}: ${error == null ? void 0 : error.message}`))))));
1632
1757
  };
1633
1758
  }
1759
+
1634
1760
  /**
1635
1761
  * Synchronously loads and caches assets with a three loader.
1636
1762
  *
@@ -1854,7 +1980,15 @@ function createRoot(canvas) {
1854
1980
  const camera = isCamera ? cameraOptions : orthographic ? new THREE.OrthographicCamera(0, 0, 0, 0, 0.1, 1000) : new THREE.PerspectiveCamera(75, 0, 0.1, 1000);
1855
1981
  if (!isCamera) {
1856
1982
  camera.position.z = 5;
1857
- if (cameraOptions) applyProps(camera, cameraOptions);
1983
+ if (cameraOptions) {
1984
+ applyProps(camera, cameraOptions);
1985
+ // Preserve user-defined frustum if possible
1986
+ // https://github.com/pmndrs/react-three-fiber/issues/3160
1987
+ if ('aspect' in cameraOptions || 'left' in cameraOptions || 'right' in cameraOptions || 'bottom' in cameraOptions || 'top' in cameraOptions) {
1988
+ camera.manual = true;
1989
+ camera.updateProjectionMatrix();
1990
+ }
1991
+ }
1858
1992
  // Always look at center by default
1859
1993
  if (!state.camera && !(cameraOptions != null && cameraOptions.rotation)) camera.lookAt(0, 0, 0);
1860
1994
  }
@@ -1983,7 +2117,7 @@ function createRoot(canvas) {
1983
2117
  state.setSize(size.width, size.height, size.top, size.left);
1984
2118
  }
1985
2119
  // Check pixelratio
1986
- if (dpr && state.dpr !== calculateDpr(dpr)) state.setDpr(dpr);
2120
+ if (dpr && state.viewport.dpr !== calculateDpr(dpr)) state.setDpr(dpr);
1987
2121
  // Check frameloop
1988
2122
  if (state.frameloop !== frameloop) state.setFrameloop(frameloop);
1989
2123
  // Check pointer missed
@@ -2106,8 +2240,11 @@ function Portal({
2106
2240
  const [raycaster] = React.useState(() => new THREE.Raycaster());
2107
2241
  const [pointer] = React.useState(() => new THREE.Vector2());
2108
2242
  const inject = useMutableCallback((rootState, injectState) => {
2243
+ let viewport;
2109
2244
  if (injectState.camera && size) {
2110
2245
  const camera = injectState.camera;
2246
+ // Calculate the override viewport, if present
2247
+ viewport = rootState.viewport.getCurrentViewport(camera, new THREE.Vector3(), size);
2111
2248
  // Update the portal camera, if it differs from the previous layer
2112
2249
  if (camera !== rootState.camera) updateCamera(camera, size);
2113
2250
  }
@@ -2123,7 +2260,7 @@ function Portal({
2123
2260
  mouse: pointer,
2124
2261
  // Their previous root is the layer before it
2125
2262
  previousRoot,
2126
- // Events and size can be overridden by the inject layer
2263
+ // Events, size and viewport can be overridden by the inject layer
2127
2264
  events: {
2128
2265
  ...rootState.events,
2129
2266
  ...injectState.events,
@@ -2133,6 +2270,10 @@ function Portal({
2133
2270
  ...rootState.size,
2134
2271
  ...size
2135
2272
  },
2273
+ viewport: {
2274
+ ...rootState.viewport,
2275
+ ...viewport
2276
+ },
2136
2277
  // Layers are allowed to override events
2137
2278
  setEvents: events => injectState.set(state => ({
2138
2279
  ...state,
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var loop = require('./loop-7f1fb121.cjs.dev.js');
5
+ var loop = require('./loop-02cefb27.cjs.dev.js');
6
6
  var _extends = require('@babel/runtime/helpers/extends');
7
7
  var React = require('react');
8
8
  var THREE = require('three');
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var loop = require('./loop-7224f71f.cjs.prod.js');
5
+ var loop = require('./loop-1fad3b6f.cjs.prod.js');
6
6
  var _extends = require('@babel/runtime/helpers/extends');
7
7
  var React = require('react');
8
8
  var THREE = require('three');
@@ -1,5 +1,5 @@
1
- import { c as createEvents, e as extend, u as useBridge, a as useMutableCallback, b as useIsomorphicLayoutEffect, d as createRoot, i as isRef, E as ErrorBoundary, B as Block, f as unmountComponentAtNode } from './loop-bdf826ba.esm.js';
2
- export { F as FixedStage, t as ReactThreeFiber, S as Stage, p as Stages, _ as _roots, x as act, j as addAfterEffect, h as addEffect, k as addTail, m as advance, s as applyProps, y as buildGraph, q as context, c as createEvents, o as createPortal, d as createRoot, w as dispose, e as extend, g as flushGlobalEffects, v as getRootState, l as invalidate, r as reconciler, n as render, f as unmountComponentAtNode, D as useFrame, H as useGraph, z as useInstanceHandle, I as useLoader, A as useStore, C as useThree, G as useUpdate } from './loop-bdf826ba.esm.js';
1
+ import { c as createEvents, e as extend, u as useBridge, a as useMutableCallback, b as useIsomorphicLayoutEffect, d as createRoot, i as isRef, E as ErrorBoundary, B as Block, f as unmountComponentAtNode } from './loop-d73c6316.esm.js';
2
+ export { F as FixedStage, t as ReactThreeFiber, S as Stage, p as Stages, _ as _roots, x as act, j as addAfterEffect, h as addEffect, k as addTail, m as advance, s as applyProps, y as buildGraph, q as context, c as createEvents, o as createPortal, d as createRoot, w as dispose, e as extend, g as flushGlobalEffects, v as getRootState, l as invalidate, r as reconciler, n as render, f as unmountComponentAtNode, D as useFrame, H as useGraph, z as useInstanceHandle, I as useLoader, A as useStore, C as useThree, G as useUpdate } from './loop-d73c6316.esm.js';
3
3
  import _extends from '@babel/runtime/helpers/esm/extends';
4
4
  import * as React from 'react';
5
5
  import * as THREE from 'three';
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var loop = require('../../dist/loop-7f1fb121.cjs.dev.js');
5
+ var loop = require('../../dist/loop-02cefb27.cjs.dev.js');
6
6
  var _extends = require('@babel/runtime/helpers/extends');
7
7
  var React = require('react');
8
8
  var THREE = require('three');
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var loop = require('../../dist/loop-7224f71f.cjs.prod.js');
5
+ var loop = require('../../dist/loop-1fad3b6f.cjs.prod.js');
6
6
  var _extends = require('@babel/runtime/helpers/extends');
7
7
  var React = require('react');
8
8
  var THREE = require('three');
@@ -1,5 +1,5 @@
1
- import { c as createEvents, e as extend, u as useBridge, a as useMutableCallback, d as createRoot, E as ErrorBoundary, B as Block, f as unmountComponentAtNode } from '../../dist/loop-bdf826ba.esm.js';
2
- export { F as FixedStage, t as ReactThreeFiber, S as Stage, p as Stages, _ as _roots, x as act, j as addAfterEffect, h as addEffect, k as addTail, m as advance, s as applyProps, y as buildGraph, q as context, c as createEvents, o as createPortal, d as createRoot, w as dispose, e as extend, g as flushGlobalEffects, v as getRootState, l as invalidate, r as reconciler, n as render, f as unmountComponentAtNode, D as useFrame, H as useGraph, z as useInstanceHandle, I as useLoader, A as useStore, C as useThree, G as useUpdate } from '../../dist/loop-bdf826ba.esm.js';
1
+ import { c as createEvents, e as extend, u as useBridge, a as useMutableCallback, d as createRoot, E as ErrorBoundary, B as Block, f as unmountComponentAtNode } from '../../dist/loop-d73c6316.esm.js';
2
+ export { F as FixedStage, t as ReactThreeFiber, S as Stage, p as Stages, _ as _roots, x as act, j as addAfterEffect, h as addEffect, k as addTail, m as advance, s as applyProps, y as buildGraph, q as context, c as createEvents, o as createPortal, d as createRoot, w as dispose, e as extend, g as flushGlobalEffects, v as getRootState, l as invalidate, r as reconciler, n as render, f as unmountComponentAtNode, D as useFrame, H as useGraph, z as useInstanceHandle, I as useLoader, A as useStore, C as useThree, G as useUpdate } from '../../dist/loop-d73c6316.esm.js';
3
3
  import _extends from '@babel/runtime/helpers/esm/extends';
4
4
  import * as React from 'react';
5
5
  import * as THREE from 'three';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-three/fiber",
3
- "version": "9.0.0-alpha.4",
3
+ "version": "9.0.0-alpha.5",
4
4
  "description": "A React renderer for Threejs",
5
5
  "keywords": [
6
6
  "react",