💨

[React Three Fiber]r3fとrapierを使ってVueのslot的なことをやりたい

2024/02/23に公開

やったこと

物理演算ライブラリであるrapierを使っていて、PhysicsコンポーネントとRigidBodyコンポーネントを別のコンポーネントとして書きたくなった。(ファイルを分けたい)

しかし、以下のようにPhysicsとは別のところにRigidBodyを記載してしまうとRigidBodyはPhysicsコンポーネント内部に書けと怒られる。

    <Physics gravity={[0, -30, 0]} colliders="cuboid" debug={debug}>
    </Physics>

    <RigidBody position={[0, -1, 0]} type="fixed" colliders="false">
      <CuboidCollider restitution={0.01} args={[1000, 1, 1000]} />
    </RigidBody>

そのためコンポーネント内部に別のコンポーネントを入れ子にしたい。
この時vueだとslotでそれができたが、同じものがReactではわからなかったのでslotのように機能するコンポーネントを作成した。

やり方

まずはslotのように機能するクラスを用意する。

// vueでいうslotのようなものをreactで実現するためのクラス
// 実装参考
// https://blog.capilano-fw.com/?p=11118

import { Children } from "react";

export class ComponentSlot {
  constructor(children) {
    this.children = Children.toArray(children);
  }

  get(key, defaultValue = "") {
    const contents = this.children.find((child) => {
      return (
        child.type === "template" && child.props.slot === key // スロット名が一致する要素を返す
      );
    });

    return contents ? contents.props.children : defaultValue; // コンテンツがない場合はデフォルト値を返す
  }
}

定義したコンポーネントを使用してPhysicsコンポーネントを使用したWorldコンポーネントを新規で定義する。

/** 物理演算の世界を作る関数
 * 実装参考:
 * 当たり判定固定(静的)回り:
 * https://codesandbox.io/p/sandbox/bruno-simons-20k-challenge-857z1i?file=%2Fsrc%2FApp.js
 * https://codesandbox.io/p/sandbox/pensive-drake-66cd7?
 *
 * vueのslot的なことをやる:
 * https://blog.capilano-fw.com/?p=11118
 */

import { Physics, RigidBody, CuboidCollider } from "@react-three/rapier";
import { useControls } from "leva";
import { ComponentSlot } from "@components/common/helper/Componentslot";

export default function World(props) {
  const { debug } = useControls({ debug: false });

  const slot = new ComponentSlot(props.children);
  const obj = slot.get("object", "-");

  return (
    <>
      {/* 以下物理演算 */}
      <Physics gravity={[0, -30, 0]} colliders="cuboid" debug={debug}>
        <RigidBody position={[0, -1, 0]} type="fixed" colliders="false">
          <CuboidCollider restitution={0.01} args={[1000, 1, 1000]} />
        </RigidBody>
        {/* ここにslot的なことやる */}
        {obj}
      </Physics>

      <mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -0, 0]} receiveShadow>
        <planeGeometry args={[100, 100]} />
        <shadowMaterial transparent opacity={0.4} />
      </mesh>
    </>
  );
}

最後にApp.tsxに記載したWorldコンポーネント内部でtemplateタグを使用して内部に別のコンポーネントを定義できるようにする。

        <World>
          <template slot="object">
            <Boxes2 position={[0, 10, 0]} colors={opsions} />
          </template>
        </World>

これで別ファイルにコンポーネントを定義できるようになった。

実際に作ったコード

https://github.com/Karakure178/astro-r3f_custom/tree/main/src/components/r3f/shader

参考資料

Discussion