@xrift/world-components 0.1.0 → 0.2.1

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/README.md ADDED
@@ -0,0 +1,100 @@
1
+ # @xrift/world-components
2
+
3
+ Xrift ワールド開発用の共有コンポーネントとユーティリティライブラリです。
4
+
5
+ ## インストール
6
+
7
+ ```bash
8
+ npm install @xrift/world-components
9
+ ```
10
+
11
+ ## 必要な依存関係
12
+
13
+ 以下のパッケージがpeerDependenciesとして必要です:
14
+
15
+ ```json
16
+ {
17
+ "react": "^18.0.0 || ^19.0.0",
18
+ "@react-three/fiber": "^8.0.0",
19
+ "@react-three/drei": "^9.0.0",
20
+ "three": "^0.150.0"
21
+ }
22
+ ```
23
+
24
+ ## 機能
25
+
26
+ ### XRiftContext
27
+
28
+ ワールドの基本情報(ベースURLなど)を提供するContextです。
29
+
30
+ ```tsx
31
+ import { XRiftProvider, useXRift } from '@xrift/world-components'
32
+
33
+ // Providerでワールドをラップ
34
+ function App() {
35
+ return (
36
+ <XRiftProvider baseUrl="https://assets.xrift.net/users/xxx/worlds/yyy/hash123/">
37
+ <MyWorld />
38
+ </XRiftProvider>
39
+ )
40
+ }
41
+
42
+ // ワールド内でbaseUrlを取得
43
+ function MyWorld() {
44
+ const { baseUrl } = useXRift()
45
+ const gltf = useGLTF(baseUrl + 'assets/model.glb')
46
+
47
+ return <primitive object={gltf.scene} />
48
+ }
49
+ ```
50
+
51
+ ### Interactable コンポーネント
52
+
53
+ 3Dオブジェクトをインタラクション可能にするラッパーコンポーネントです。
54
+
55
+ ```tsx
56
+ import { Interactable } from '@xrift/world-components'
57
+
58
+ function MyWorld() {
59
+ const handleButtonClick = (id: string) => {
60
+ console.log(`${id} がクリックされました!`)
61
+ }
62
+
63
+ return (
64
+ <Interactable
65
+ id="my-button"
66
+ onInteract={handleButtonClick}
67
+ interactionText="ボタンを押す"
68
+ >
69
+ <mesh position={[0, 1, -3]}>
70
+ <boxGeometry args={[1, 1, 1]} />
71
+ <meshStandardMaterial color="blue" />
72
+ </mesh>
73
+ </Interactable>
74
+ )
75
+ }
76
+ ```
77
+
78
+ #### Props
79
+
80
+ | プロパティ | 型 | 必須 | デフォルト | 説明 |
81
+ |-----------|-----|------|-----------|------|
82
+ | `id` | `string` | ✓ | - | オブジェクトの一意なID |
83
+ | `onInteract` | `(id: string) => void` | ✓ | - | インタラクション時のコールバック |
84
+ | `interactionText` | `string` | - | `"クリックする"` | インタラクション時に表示するテキスト |
85
+ | `enabled` | `boolean` | - | `true` | インタラクション可能かどうか |
86
+ | `children` | `ReactNode` | ✓ | - | 3Dオブジェクト |
87
+
88
+ ## 開発
89
+
90
+ ```bash
91
+ # 開発モード(ウォッチモード)
92
+ npm run dev
93
+
94
+ # ビルド
95
+ npm run build
96
+ ```
97
+
98
+ ## ライセンス
99
+
100
+ MIT
@@ -0,0 +1,5 @@
1
+ import { type FC } from 'react';
2
+ import type { Props } from './types';
3
+ export declare const Interactable: FC<Props>;
4
+ export type { Props as InteractableProps } from './types';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/Interactable/index.tsx"],"names":[],"mappings":"AACA,OAAO,EAA6D,KAAK,EAAE,EAAE,MAAM,OAAO,CAAA;AAG1F,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,KAAK,CAkFlC,CAAA;AAED,YAAY,EAAE,KAAK,IAAI,iBAAiB,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Outlines } from '@react-three/drei';
3
+ import { Children, cloneElement, isValidElement, useEffect, useRef } from 'react';
4
+ import { useXRift } from '../../contexts/XRiftContext';
5
+ const INTERACTABLE_LAYER = 10;
6
+ export const Interactable = ({ id, type = 'button', onInteract, interactionText, enabled = true, children, }) => {
7
+ const { currentTarget } = useXRift();
8
+ const groupRef = useRef(null);
9
+ // userDataにインタラクション情報を設定 & レイヤー設定
10
+ useEffect(() => {
11
+ const object = groupRef.current;
12
+ if (!object)
13
+ return;
14
+ // userDataにインタラクション情報を設定
15
+ const interactableData = {
16
+ id,
17
+ type,
18
+ onInteract,
19
+ interactionText,
20
+ enabled,
21
+ };
22
+ Object.assign(object.userData, interactableData);
23
+ // レイヤーを設定(レイキャスト最適化のため)
24
+ object.traverse((child) => {
25
+ child.layers.enable(INTERACTABLE_LAYER);
26
+ });
27
+ // クリーンアップ: userDataからインタラクション情報を削除
28
+ return () => {
29
+ if (object.userData) {
30
+ delete object.userData.id;
31
+ delete object.userData.type;
32
+ delete object.userData.onInteract;
33
+ delete object.userData.interactionText;
34
+ delete object.userData.enabled;
35
+ }
36
+ // レイヤーを無効化
37
+ object.traverse((child) => {
38
+ child.layers.disable(INTERACTABLE_LAYER);
39
+ });
40
+ };
41
+ }, [id, type, onInteract, interactionText, enabled]);
42
+ // 現在のターゲットかどうかで視覚的フィードバックを提供
43
+ const isTargeted = currentTarget !== null && currentTarget.uuid === groupRef.current?.uuid;
44
+ // 子要素(mesh)に<Outlines>を追加
45
+ const childrenWithOutlines = Children.map(children, (child) => {
46
+ if (!isValidElement(child))
47
+ return child;
48
+ // meshの子要素として<Outlines>を追加
49
+ return cloneElement(child, {
50
+ children: (_jsxs(_Fragment, { children: [child.props.children, isTargeted && enabled && (_jsx(Outlines, { thickness: 5, color: "#4dabf7", screenspace: false, opacity: 1, transparent: false, angle: Math.PI }))] })),
51
+ });
52
+ });
53
+ return (_jsx("group", { ref: groupRef, children: childrenWithOutlines }));
54
+ };
55
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/components/Interactable/index.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,EAAW,MAAM,OAAO,CAAA;AAE1F,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAA;AAGtD,MAAM,kBAAkB,GAAG,EAAE,CAAA;AAE7B,MAAM,CAAC,MAAM,YAAY,GAAc,CAAC,EACtC,EAAE,EACF,IAAI,GAAG,QAAQ,EACf,UAAU,EACV,eAAe,EACf,OAAO,GAAG,IAAI,EACd,QAAQ,GACT,EAAE,EAAE;IACH,MAAM,EAAE,aAAa,EAAE,GAAG,QAAQ,EAAE,CAAA;IACpC,MAAM,QAAQ,GAAG,MAAM,CAAQ,IAAI,CAAC,CAAA;IAEpC,kCAAkC;IAClC,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAA;QAC/B,IAAI,CAAC,MAAM;YAAE,OAAM;QAEnB,yBAAyB;QACzB,MAAM,gBAAgB,GAAG;YACvB,EAAE;YACF,IAAI;YACJ,UAAU;YACV,eAAe;YACf,OAAO;SACR,CAAA;QAED,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAA;QAEhD,wBAAwB;QACxB,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE;YACxB,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAA;QACzC,CAAC,CAAC,CAAA;QAEF,mCAAmC;QACnC,OAAO,GAAG,EAAE;YACV,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;gBACpB,OAAO,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAA;gBACzB,OAAO,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAA;gBAC3B,OAAO,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAA;gBACjC,OAAO,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAA;gBACtC,OAAO,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAA;YAChC,CAAC;YAED,WAAW;YACX,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE;gBACxB,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAA;YAC1C,CAAC,CAAC,CAAA;QACJ,CAAC,CAAA;IACH,CAAC,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,eAAe,EAAE,OAAO,CAAC,CAAC,CAAA;IAEpD,6BAA6B;IAC7B,MAAM,UAAU,GAAG,aAAa,KAAK,IAAI,IAAI,aAAa,CAAC,IAAI,KAAK,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAA;IAE1F,0BAA0B;IAC1B,MAAM,oBAAoB,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;QAC5D,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAA;QAExC,2BAA2B;QAC3B,OAAO,YAAY,CAAC,KAAK,EAAE;YACzB,QAAQ,EAAE,CACR,8BAEI,KAAK,CAAC,KAAa,CAAC,QAAQ,EAC7B,UAAU,IAAI,OAAO,IAAI,CACxB,KAAC,QAAQ,IACP,SAAS,EAAE,CAAC,EACZ,KAAK,EAAC,SAAS,EACf,WAAW,EAAE,KAAK,EAClB,OAAO,EAAE,CAAC,EACV,WAAW,EAAE,KAAK,EAClB,KAAK,EAAE,IAAI,CAAC,EAAE,GACd,CACH,IACA,CACJ;SACO,CAAC,CAAA;IACb,CAAC,CAAC,CAAA;IAEF,OAAO,CACL,gBAAO,GAAG,EAAE,QAAQ,YACjB,oBAAoB,GACf,CACT,CAAA;AACH,CAAC,CAAA"}
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from 'react';
2
+ export interface Props {
3
+ /** オブジェクトの一意なID */
4
+ id: string;
5
+ /** オブジェクトの種類 */
6
+ type?: 'button';
7
+ /** インタラクション時のコールバック(オブジェクトのIDが渡される) */
8
+ onInteract: (id: string) => void;
9
+ /** インタラクション時に表示するテキスト */
10
+ interactionText?: string;
11
+ /** インタラクション可能かどうか */
12
+ enabled?: boolean;
13
+ /** 子要素(3Dオブジェクト) */
14
+ children: ReactNode;
15
+ }
16
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/components/Interactable/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,WAAW,KAAK;IACpB,mBAAmB;IACnB,EAAE,EAAE,MAAM,CAAA;IACV,gBAAgB;IAChB,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf,uCAAuC;IACvC,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAA;IAChC,yBAAyB;IACzB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,oBAAoB;IACpB,QAAQ,EAAE,SAAS,CAAA;CACpB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/components/Interactable/types.ts"],"names":[],"mappings":""}
@@ -1,10 +1,16 @@
1
1
  import { type ReactNode } from 'react';
2
+ import type { Object3D } from 'three';
2
3
  export interface XRiftContextValue {
3
4
  /**
4
5
  * ワールドのベースURL(CDNのディレクトリパス)
5
6
  * 例: 'https://assets.xrift.net/users/xxx/worlds/yyy/hash123/'
6
7
  */
7
8
  baseUrl: string;
9
+ /**
10
+ * 現在レイキャストでターゲットされているオブジェクト
11
+ * xrift-frontend側のRaycastDetectorが設定する
12
+ */
13
+ currentTarget: Object3D | null;
8
14
  }
9
15
  /**
10
16
  * XRift ワールドの情報を提供するContext
@@ -13,6 +19,7 @@ export interface XRiftContextValue {
13
19
  export declare const XRiftContext: import("react").Context<XRiftContextValue | null>;
14
20
  interface Props {
15
21
  baseUrl: string;
22
+ currentTarget?: Object3D | null;
16
23
  children: ReactNode;
17
24
  }
18
25
  /**
@@ -20,7 +27,7 @@ interface Props {
20
27
  * Module Federationで動的にロードされたワールドコンポーネントに
21
28
  * 必要な情報を注入するために使用
22
29
  */
23
- export declare const XRiftProvider: ({ baseUrl, children }: Props) => import("react/jsx-runtime").JSX.Element;
30
+ export declare const XRiftProvider: ({ baseUrl, currentTarget, children }: Props) => import("react/jsx-runtime").JSX.Element;
24
31
  /**
25
32
  * XRift ワールドの情報を取得するhook
26
33
  * ワールドプロジェクト側でアセットの相対パスを絶対パスに変換する際に使用
@@ -1 +1 @@
1
- {"version":3,"file":"XRiftContext.d.ts","sourceRoot":"","sources":["../../src/contexts/XRiftContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAc,MAAM,OAAO,CAAA;AAEjE,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;CAKhB;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,mDAAgD,CAAA;AAEzE,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,uBAAuB,KAAK,4CAMzD,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,QAAO,iBAQ3B,CAAA"}
1
+ {"version":3,"file":"XRiftContext.d.ts","sourceRoot":"","sources":["../../src/contexts/XRiftContext.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAc,MAAM,OAAO,CAAA;AACjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAErC,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,aAAa,EAAE,QAAQ,GAAG,IAAI,CAAA;CAK/B;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,mDAAgD,CAAA;AAEzE,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAA;IACf,aAAa,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAA;IAC/B,QAAQ,EAAE,SAAS,CAAA;CACpB;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAI,sCAA6C,KAAK,4CAW/E,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,QAAO,iBAQ3B,CAAA"}
@@ -10,8 +10,11 @@ export const XRiftContext = createContext(null);
10
10
  * Module Federationで動的にロードされたワールドコンポーネントに
11
11
  * 必要な情報を注入するために使用
12
12
  */
13
- export const XRiftProvider = ({ baseUrl, children }) => {
14
- return (_jsx(XRiftContext.Provider, { value: { baseUrl }, children: children }));
13
+ export const XRiftProvider = ({ baseUrl, currentTarget = null, children }) => {
14
+ return (_jsx(XRiftContext.Provider, { value: {
15
+ baseUrl,
16
+ currentTarget,
17
+ }, children: children }));
15
18
  };
16
19
  /**
17
20
  * XRift ワールドの情報を取得するhook
@@ -1 +1 @@
1
- {"version":3,"file":"XRiftContext.js","sourceRoot":"","sources":["../../src/contexts/XRiftContext.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,MAAM,OAAO,CAAA;AAcjE;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,CAA2B,IAAI,CAAC,CAAA;AAOzE;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAS,EAAE,EAAE;IAC5D,OAAO,CACL,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,EAAE,OAAO,EAAE,YACtC,QAAQ,GACa,CACzB,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAsB,EAAE;IAC9C,MAAM,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC,CAAA;IAExC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;IAC/D,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
1
+ {"version":3,"file":"XRiftContext.js","sourceRoot":"","sources":["../../src/contexts/XRiftContext.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAkB,UAAU,EAAE,MAAM,OAAO,CAAA;AAoBjE;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,aAAa,CAA2B,IAAI,CAAC,CAAA;AAQzE;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,EAAE,OAAO,EAAE,aAAa,GAAG,IAAI,EAAE,QAAQ,EAAS,EAAE,EAAE;IAClF,OAAO,CACL,KAAC,YAAY,CAAC,QAAQ,IACpB,KAAK,EAAE;YACL,OAAO;YACP,aAAa;SACd,YAEA,QAAQ,GACa,CACzB,CAAA;AACH,CAAC,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAsB,EAAE;IAC9C,MAAM,OAAO,GAAG,UAAU,CAAC,YAAY,CAAC,CAAA;IAExC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAA;IAC/D,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { XRiftContext, XRiftProvider, useXRift, type XRiftContextValue, } from './contexts/XRiftContext';
2
+ export { Interactable, type InteractableProps, } from './components/Interactable';
2
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,KAAK,iBAAiB,GACvB,MAAM,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,QAAQ,EACR,KAAK,iBAAiB,GACvB,MAAM,yBAAyB,CAAA;AAGhC,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,GACvB,MAAM,2BAA2B,CAAA"}
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  // Contexts
2
2
  export { XRiftContext, XRiftProvider, useXRift, } from './contexts/XRiftContext';
3
+ // Components
4
+ export { Interactable, } from './components/Interactable';
3
5
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,WAAW;AACX,OAAO,EACL,YAAY,EACZ,aAAa,EACb,QAAQ,GAET,MAAM,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,WAAW;AACX,OAAO,EACL,YAAY,EACZ,aAAa,EACb,QAAQ,GAET,MAAM,yBAAyB,CAAA;AAEhC,aAAa;AACb,OAAO,EACL,YAAY,GAEb,MAAM,2BAA2B,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xrift/world-components",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Shared components and utilities for Xrift worlds",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -29,10 +29,17 @@
29
29
  },
30
30
  "homepage": "https://github.com/WebXR-JP/xrift-world-components#readme",
31
31
  "peerDependencies": {
32
- "react": "^18.0.0 || ^19.0.0"
32
+ "@react-three/drei": "^10.0.0",
33
+ "@react-three/fiber": "^9.0.0",
34
+ "react": "^18.0.0 || ^19.0.0",
35
+ "three": "^0.170.0"
33
36
  },
34
37
  "devDependencies": {
38
+ "@react-three/drei": "^10.7.6",
39
+ "@react-three/fiber": "^9.4.0",
35
40
  "@types/react": "^18.3.12",
41
+ "@types/three": "^0.181.0",
42
+ "three": "^0.181.0",
36
43
  "typescript": "^5.6.3"
37
44
  }
38
45
  }