# Multiple Views With Uniform Controls

Shows how to form self-contained components with their own state and user interaction.

Demo: https://pmndrs.github.io/examples/examples/multiple-views-with-uniform-controls
Source: https://github.com/pmndrs/examples/tree/main/examples/multiple-views-with-uniform-controls
Scaffold: npx degit pmndrs/examples/examples/multiple-views-with-uniform-controls
Published: 2023-01-19
Authors: Paul Henschel
Tags: views, controls, state, soft-shadows
Ported from: https://codesandbox.io/s/r9w2ob
Dependencies: @mantine/core@^5.10.5, @mantine/hooks@^5.10.1, @react-three/drei@10.7.8, @react-three/fiber@9.6.1, @tabler/icons-react@^3.36.0, react@19.2.8, react-dom@19.2.8, react-use-refs@^1.0.1, three@0.165.0, zustand@^5.0.14
Binary files, in the repository but not below: src/assets/bricks.gltf

## src/App.tsx

```tsx
import * as THREE from "three";
import { forwardRef, type ReactNode } from "react";
import { Canvas, type ThreeElements } from "@react-three/fiber";
import {
  useGLTF,
  View,
  Center,
  Environment,
  MapControls,
  OrbitControls,
  PivotControls,
  RandomizedLight,
} from "@react-three/drei";
import {
  PerspectiveCamera,
  OrthographicCamera,
  AccumulativeShadows,
} from "@react-three/drei";
import { Menu, Button } from "@mantine/core";
import * as ICONS from "@tabler/icons-react";
import useRefs from "react-use-refs";
import { create } from "zustand";
import { type GLTF } from "three-stdlib";

// From the poimandres market (https://market.pmnd.rs/), vendored locally since the original CDN is offline.
import bricksModel from "./assets/bricks.gltf?url";

type PanelKey = "top" | "middle" | "bottom";

interface StoreState {
  projection: string;
  top: string;
  middle: string;
  bottom: string;
  setPanelView: (which: PanelKey, view: string) => void;
  setProjection: (projection: string) => void;
}

const matrix = new THREE.Matrix4();
const positions: Record<string, [number, number, number]> = {
  Top: [0, 10, 0],
  Bottom: [0, -10, 0],
  Left: [-10, 0, 0],
  Right: [10, 0, 0],
  Back: [0, 0, -10],
  Front: [0, 0, 10],
};
const useStore = create<StoreState>((set) => ({
  projection: "Perspective",
  top: "Back",
  middle: "Top",
  bottom: "Right",
  setPanelView: (which, view) => set({ [which]: view }),
  setProjection: (projection) => set({ projection }),
}));

export function App() {
  const [view1, view2, view3, view4] = useRefs<HTMLDivElement>(null);
  return (
    <div className="container">
      {/** A single canvas, it will only render when things move or change, and otherwise stay idle ... */}
      <Canvas
        shadows
        frameloop="demand"
        eventSource={document.getElementById("root")!}
        className="canvas"
      >
        {/** Each view tracks one of the divs above and creates a sandboxed environment that behaves
             as if it were a normal everyday canvas, <View> will figure out the gl.scissor stuff alone. */}
        <View.Port />
      </Canvas>
      {/** Tracking div's, regular HTML and made responsive with CSS media-queries ... */}
      <MainPanel ref={view1}>
        <CameraSwitcher />
        <PivotControls scale={0.4} depthTest={false} matrix={matrix} />
        <Scene background="aquamarine" matrix={matrix}>
          <AccumulativeShadows
            temporal
            frames={100}
            position={[0, -0.4, 0]}
            scale={14}
            alphaTest={0.85}
            color="orange"
            colorBlend={0.5}
          >
            <RandomizedLight
              amount={8}
              radius={8}
              ambient={0.5}
              position={[5, 5, -10]}
              bias={0.001}
            />
          </AccumulativeShadows>
        </Scene>
        <OrbitControls makeDefault />
      </MainPanel>
      <SidePanel ref={view2} which="top">
        <PanelCamera which="top" />
        <PivotControls
          activeAxes={[true, true, false]}
          depthTest={false}
          matrix={matrix}
        />
        <Scene background="lightpink" matrix={matrix} />
        <MapControls makeDefault screenSpacePanning enableRotate={false} />
      </SidePanel>
      <SidePanel ref={view3} which="middle">
        <PanelCamera which="middle" />
        <PivotControls
          activeAxes={[true, false, true]}
          depthTest={false}
          matrix={matrix}
        />
        <Scene background="peachpuff" matrix={matrix} />
        <MapControls makeDefault screenSpacePanning enableRotate={false} />
      </SidePanel>
      <SidePanel ref={view4} which="bottom">
        <PanelCamera which="bottom" />
        <PivotControls
          activeAxes={[false, true, true]}
          depthTest={false}
          matrix={matrix}
        />
        <Scene background="skyblue" matrix={matrix} />
        <MapControls makeDefault screenSpacePanning enableRotate={false} />
      </SidePanel>
    </div>
  );
}

type GLTFResult = GLTF & {
  nodes: { bricks: THREE.Mesh };
  materials: { "Stone.014": THREE.MeshStandardMaterial };
};

type SceneProps = ThreeElements["group"] & { background?: string };

function Scene({ background = "white", children, ...props }: SceneProps) {
  const { nodes, materials } = useGLTF(bricksModel) as unknown as GLTFResult;
  return (
    <>
      <color attach="background" args={[background]} />
      <ambientLight intensity={Math.PI} />
      <directionalLight
        position={[10, 10, -15]}
        intensity={Math.PI}
        castShadow
        shadow-bias={-0.0001}
        shadow-mapSize={1024}
      />
      <Environment preset="city" />
      <group
        matrixAutoUpdate={false}
        // Why onUpdate and not just matrix={matrix} ?
        // This is an implementation detail, overwriting (most) transform objects isn't possible in Threejs
        // because they are defined read-only. Therefore Fiber will always call .copy() if you pass
        // an object, for instance matrix={new THREE.Matrix4()} or position={new THREE.Vector3()}
        // In this rare case we do not want it to copy the matrix, but refer to it.
        onUpdate={(self) => (self.matrix = matrix)}
        {...props}
      >
        <Center>
          <mesh
            castShadow
            geometry={nodes.bricks.geometry}
            material={materials["Stone.014"]}
            rotation={[Math.PI / 2, 0, 0]}
          >
            <meshStandardMaterial
              color="goldenrod"
              roughness={0}
              metalness={1}
            />
          </mesh>
        </Center>
        {children}
      </group>
    </>
  );
}

function CameraSwitcher() {
  const projection = useStore((state) => state.projection);
  // Would need to remember the old coordinates to be more useful ...
  return projection === "Perspective" ? (
    <PerspectiveCamera makeDefault position={[4, 4, 4]} fov={25} />
  ) : (
    <OrthographicCamera makeDefault position={[4, 4, 4]} zoom={280} />
  );
}

function PanelCamera({ which }: { which: PanelKey }) {
  const view = useStore((state) => state[which]);
  return (
    <OrthographicCamera makeDefault position={positions[view]} zoom={100} />
  );
}

interface MainPanelProps {
  children?: ReactNode;
}

const MainPanel = forwardRef<HTMLDivElement, MainPanelProps>(
  ({ children, ...props }, fref) => {
    const projection = useStore((state) => state.projection);
    const setProjection = useStore((state) => state.setProjection);
    return (
      <div ref={fref} className="panel" style={{ gridArea: "main" }}>
        <View
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            width: "100%",
            height: "100%",
          }}
        >
          {children}
        </View>
        <Menu shadow="md" width={200}>
          <Menu.Target>
            <Button>{projection}</Button>
          </Menu.Target>
          <Menu.Dropdown
            onClick={(e) => setProjection((e.target as HTMLElement).innerText)}
          >
            <Menu.Item icon={<ICONS.IconPerspective size={14} />}>
              Perspective
            </Menu.Item>
            <Menu.Item icon={<ICONS.IconPerspectiveOff size={14} />}>
              Orthographic
            </Menu.Item>
          </Menu.Dropdown>
        </Menu>
      </div>
    );
  },
);

interface SidePanelProps {
  which: PanelKey;
  children?: ReactNode;
}

const SidePanel = forwardRef<HTMLDivElement, SidePanelProps>(
  ({ which, children }, fref) => {
    const value = useStore((state) => state[which]);
    const setPanelView = useStore((state) => state.setPanelView);
    return (
      <div ref={fref} className="panel" style={{ gridArea: which }}>
        <View
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            width: "100%",
            height: "100%",
          }}
        >
          {children}
        </View>
        <Menu shadow="md" width={200}>
          <Menu.Target>
            <Button>{value}</Button>
          </Menu.Target>
          <Menu.Dropdown
            onClick={(e) =>
              setPanelView(which, (e.target as HTMLElement).innerText)
            }
          >
            <Menu.Item icon={<ICONS.IconArrowBigUp size={14} />}>Top</Menu.Item>
            <Menu.Item icon={<ICONS.IconArrowBigDown size={14} />}>
              Bottom
            </Menu.Item>
            <Menu.Item icon={<ICONS.IconArrowBigLeft size={14} />}>
              Left
            </Menu.Item>
            <Menu.Item icon={<ICONS.IconArrowBigRight size={14} />}>
              Right
            </Menu.Item>
            <Menu.Item icon={<ICONS.IconHomeUp size={14} />}>Front</Menu.Item>
            <Menu.Item icon={<ICONS.IconHomeDown size={14} />}>Back</Menu.Item>
          </Menu.Dropdown>
        </Menu>
      </div>
    );
  },
);
```

## src/index.tsx

```tsx
import { createRoot } from "react-dom/client";
import "./styles.css";
import { App } from "./App";

createRoot(document.getElementById("root")!).render(<App />);
```

## src/styles.css

```css
@import url("https://rsms.me/inter/inter.css");

* {
  box-sizing: border-box;
}

html,
body,
#root {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  background: #f0f0f0;
  font-family: inter;
  padding: 10px;
}

.container {
  position: relative;
  width: 100%;
  height: 100%;
  display: grid;
  gap: 10px;
  grid-template-columns: 1fr 300px;
  grid-template-rows: 1fr 1fr 1fr;
  grid-template-areas:
    "main top"
    "main middle"
    "main bottom";
}

@media only screen and (max-width: 600px) {
  .container {
    grid-template-columns: 1fr 1fr 1fr;
    grid-template-rows: 1fr 200px;
    grid-template-areas:
      "main main main"
      "top middle bottom";
  }
}

.container > .panel {
  position: relative;
  padding: 20px;
}

.canvas {
  pointer-events: none;
  position: absolute !important;
  top: 0px;
  left: 0px;
  width: 100vw;
  height: 100vh;
}
```

## src/vite-env.d.ts

```ts
/// <reference types="vite/client" />
```
