@navaramap/three-react 0.0.0
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/LICENSE +8 -0
- package/README.md +128 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +105 -0
- package/dist/index.umd.cjs +1 -0
- package/package.json +44 -0
- package/src/Layer.tsx +173 -0
- package/src/ViewContext.tsx +85 -0
- package/src/index.ts +2 -0
- package/src/vite-env.d.ts +6 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# @navaramap/three-react
|
|
2
|
+
|
|
3
|
+
React bindings for Navara Three. It provides a small set of components and hooks to use `@navaramap/three` declaratively from React apps.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @navaramap/three @navaramap/three-react react react-dom three
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
Wrap your app with `ViewProvider`, then add layers with the `Layer` component.
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
// App.tsx
|
|
17
|
+
import { ViewProvider } from "@navaramap/three-react";
|
|
18
|
+
import { Layers } from "./Layers";
|
|
19
|
+
|
|
20
|
+
export default function App() {
|
|
21
|
+
return (
|
|
22
|
+
<ViewProvider>
|
|
23
|
+
<Layers />
|
|
24
|
+
</ViewProvider>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
// Layers.tsx
|
|
31
|
+
import {
|
|
32
|
+
JAPAN_GSI_ELEVATION_DECODER,
|
|
33
|
+
type LayerDescription,
|
|
34
|
+
type TilesLayer,
|
|
35
|
+
Layer as NavaraLayer,
|
|
36
|
+
} from "@navaramap/three";
|
|
37
|
+
import { useViewContext, Layer } from "@navaramap/three-react";
|
|
38
|
+
import { useEffect, useMemo } from "react";
|
|
39
|
+
|
|
40
|
+
export function Layers() {
|
|
41
|
+
const { view } = useViewContext();
|
|
42
|
+
|
|
43
|
+
// Credit the data through the built-in attribution UI.
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
view?.attribution?.add([
|
|
46
|
+
{
|
|
47
|
+
attribution: "Geospatial Information Authority of Japan (GSI)",
|
|
48
|
+
attributionUrl: "https://maps.gsi.go.jp/development/ichiran.html",
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
}, [view]);
|
|
52
|
+
|
|
53
|
+
const baseTiles = useMemo<LayerDescription>(
|
|
54
|
+
() => ({
|
|
55
|
+
type: "tiles",
|
|
56
|
+
data: { url: "…" },
|
|
57
|
+
raster_tile: { min_zoom: 2, max_zoom: 18 },
|
|
58
|
+
}),
|
|
59
|
+
[],
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const terrain = useMemo<LayerDescription>(
|
|
63
|
+
() => ({
|
|
64
|
+
type: "terrain",
|
|
65
|
+
data: { url: "…" },
|
|
66
|
+
raster_terrain: {
|
|
67
|
+
min_zoom: 6,
|
|
68
|
+
max_zoom: 15,
|
|
69
|
+
elevation_decoder: JAPAN_GSI_ELEVATION_DECODER(),
|
|
70
|
+
cast_shadow: true,
|
|
71
|
+
receive_shadow: true,
|
|
72
|
+
},
|
|
73
|
+
}),
|
|
74
|
+
[],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<>
|
|
79
|
+
<Layer config={baseTiles} />
|
|
80
|
+
<Layer config={terrain} />
|
|
81
|
+
</>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
- `ViewProvider`
|
|
89
|
+
- Props: `{ canvas?: HTMLCanvasElement | RefObject<HTMLCanvasElement> }`.
|
|
90
|
+
- Creates a `ThreeView` and provides it via context. If no `canvas` is given, a fullscreen canvas is appended to `document.body`.
|
|
91
|
+
|
|
92
|
+
- `useViewContext<CustomLayerDescriptions>()`
|
|
93
|
+
- Returns `{ view }` where `view` is the underlying `ThreeView<CustomLayerDescriptions>`.
|
|
94
|
+
- Must be used inside `ViewProvider`.
|
|
95
|
+
|
|
96
|
+
- `Layer`
|
|
97
|
+
- Props: `{ config: LayerDescription; onReady?: (handle) => void }`.
|
|
98
|
+
- Declaratively adds a layer on mount and updates it when `config` changes. The `onReady` callback receives a layer handle.
|
|
99
|
+
|
|
100
|
+
## Canvas control (optional)
|
|
101
|
+
|
|
102
|
+
If you want to host the canvas inside your own layout, pass a ref to `ViewProvider`:
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { useRef } from "react";
|
|
106
|
+
import { ViewProvider } from "@navaramap/three-react";
|
|
107
|
+
|
|
108
|
+
export function AppWithCanvas() {
|
|
109
|
+
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
|
110
|
+
return (
|
|
111
|
+
<div style={{ position: "relative", width: "100vw", height: "100vh" }}>
|
|
112
|
+
<canvas ref={canvasRef} style={{ width: "100%", height: "100%" }} />
|
|
113
|
+
<ViewProvider canvas={canvasRef}>
|
|
114
|
+
<Layers />
|
|
115
|
+
</ViewProvider>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Notes
|
|
122
|
+
|
|
123
|
+
- This package is client-side only; if using SSR, render these components on the client.
|
|
124
|
+
- Types are included.
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT OR Apache-2.0
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { BaseHandle } from '@navaramap/three';
|
|
2
|
+
import { BuiltInEffectDescription } from '@navaramap/three';
|
|
3
|
+
import { default as default_2 } from '@navaramap/three';
|
|
4
|
+
import { Descriptions } from '@navaramap/three';
|
|
5
|
+
import { EffectConfig } from '@navaramap/three';
|
|
6
|
+
import { EffectDesc as EffectDesc_2 } from '@navaramap/three';
|
|
7
|
+
import { EmptyDescriptions } from '@navaramap/three';
|
|
8
|
+
import { FC } from 'react';
|
|
9
|
+
import { Layer as Layer_2 } from '@navaramap/three';
|
|
10
|
+
import { LayerDescription } from '@navaramap/three';
|
|
11
|
+
import { LightConfig } from '@navaramap/three';
|
|
12
|
+
import { LightDesc as LightDesc_2 } from '@navaramap/three';
|
|
13
|
+
import { MeshConfig } from '@navaramap/three';
|
|
14
|
+
import { MeshDesc as MeshDesc_2 } from '@navaramap/three';
|
|
15
|
+
import { Options } from '@navaramap/three';
|
|
16
|
+
import { Plugin as Plugin_2 } from '@navaramap/three';
|
|
17
|
+
import { PropsWithChildren } from 'react';
|
|
18
|
+
import { RefObject } from 'react';
|
|
19
|
+
|
|
20
|
+
export declare function EffectDesc<L extends EffectDesc_2 = EffectDesc_2>({ config, onReady, }: PropsWithChildren<EffectDescProps<L>>): null;
|
|
21
|
+
|
|
22
|
+
declare type EffectDescProps<L extends EffectDesc_2 = EffectDesc_2> = {
|
|
23
|
+
config: EffectConfig | BuiltInEffectDescription;
|
|
24
|
+
onReady?: (handle: BaseHandle<L>) => (() => void) | void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export declare function Layer({ config, onReady }: PropsWithChildren<LayerProps>): null;
|
|
28
|
+
|
|
29
|
+
declare type LayerProps = {
|
|
30
|
+
config: LayerDescription;
|
|
31
|
+
onReady?: (handle: Layer_2) => (() => void) | void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export declare function LightDesc<L extends LightDesc_2 = LightDesc_2>({ config, onReady, }: PropsWithChildren<LightDescProps<L>>): null;
|
|
35
|
+
|
|
36
|
+
declare type LightDescProps<L extends LightDesc_2 = LightDesc_2> = {
|
|
37
|
+
config: LightConfig;
|
|
38
|
+
onReady?: (handle: BaseHandle<L>) => (() => void) | void;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export declare function MeshDesc<L extends MeshDesc_2 = MeshDesc_2>({ config, onReady, }: PropsWithChildren<MeshDescProps<L>>): null;
|
|
42
|
+
|
|
43
|
+
declare type MeshDescProps<L extends MeshDesc_2 = MeshDesc_2> = {
|
|
44
|
+
config: MeshConfig;
|
|
45
|
+
onReady?: (handle: BaseHandle<L>) => (() => void) | void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export declare const useViewContext: <D extends Descriptions = EmptyDescriptions>() => Required<ViewContextValues<D>>;
|
|
49
|
+
|
|
50
|
+
declare type ViewContextValues<D extends Descriptions = EmptyDescriptions> = {
|
|
51
|
+
view?: default_2<D>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export declare const ViewProvider: FC<PropsWithChildren<ViewProviderProps>>;
|
|
55
|
+
|
|
56
|
+
export declare type ViewProviderProps = {
|
|
57
|
+
canvas?: HTMLCanvasElement | RefObject<HTMLCanvasElement>;
|
|
58
|
+
plugins?: Plugin_2[];
|
|
59
|
+
} & Options;
|
|
60
|
+
|
|
61
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import e from "@navaramap/three";
|
|
2
|
+
import { createContext as t, useCallback as n, useContext as r, useEffect as i, useRef as a, useState as o } from "react";
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var s = (e, t) => () => (t || (e((t = { exports: {} }).exports, t), e = null), t.exports), c = /* @__PURE__ */ s(((e) => {
|
|
5
|
+
var t = Symbol.for("react.transitional.element");
|
|
6
|
+
function n(e, n, r) {
|
|
7
|
+
var i = null;
|
|
8
|
+
if (r !== void 0 && (i = "" + r), n.key !== void 0 && (i = "" + n.key), "key" in n) for (var a in r = {}, n) a !== "key" && (r[a] = n[a]);
|
|
9
|
+
else r = n;
|
|
10
|
+
return n = r.ref, {
|
|
11
|
+
$$typeof: t,
|
|
12
|
+
type: e,
|
|
13
|
+
key: i,
|
|
14
|
+
ref: n === void 0 ? null : n,
|
|
15
|
+
props: r
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
e.jsx = n;
|
|
19
|
+
})), l = (/* @__PURE__ */ s(((e, t) => {
|
|
20
|
+
t.exports = c();
|
|
21
|
+
})))(), u = t(void 0), d = () => {
|
|
22
|
+
let e = r(u);
|
|
23
|
+
if (!e) throw Error("Navara React Error: You have to invoke this hook inside of ViewProvider.");
|
|
24
|
+
return e;
|
|
25
|
+
}, f = ({ canvas: t, children: n, plugins: r, ...s }) => {
|
|
26
|
+
let [c, d] = o(), [f, p] = o(!1), m = a(void 0);
|
|
27
|
+
return i(() => {
|
|
28
|
+
if (m.current) {
|
|
29
|
+
console.warn("You need to recreate ThreeView.");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let n = new e({
|
|
33
|
+
canvas: t && "current" in t ? t.current : t,
|
|
34
|
+
...s
|
|
35
|
+
});
|
|
36
|
+
if (r) for (let e of r) n.addPlugin(e);
|
|
37
|
+
return d(n), m.current = n, (async () => {
|
|
38
|
+
try {
|
|
39
|
+
await n.init(), p(!0);
|
|
40
|
+
} catch (e) {
|
|
41
|
+
console.error("Navara init failed:", e);
|
|
42
|
+
}
|
|
43
|
+
})(), () => {};
|
|
44
|
+
}, [
|
|
45
|
+
t,
|
|
46
|
+
s,
|
|
47
|
+
r
|
|
48
|
+
]), /* @__PURE__ */ (0, l.jsx)(u.Provider, {
|
|
49
|
+
value: { view: c },
|
|
50
|
+
children: f ? n : null
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region src/Layer.tsx
|
|
55
|
+
function p({ config: e, onReady: t }) {
|
|
56
|
+
let { view: n } = d(), r = a(null), o = a(e), s = a(t);
|
|
57
|
+
return i(() => {
|
|
58
|
+
o.current = e, s.current = t;
|
|
59
|
+
}, [e, t]), i(() => {
|
|
60
|
+
let e = n.addLayer(o.current);
|
|
61
|
+
r.current = e;
|
|
62
|
+
let t = s.current?.(e);
|
|
63
|
+
return () => {
|
|
64
|
+
t?.(), e.delete(), r.current = null;
|
|
65
|
+
};
|
|
66
|
+
}, [n]), i(() => {
|
|
67
|
+
if (r.current) if ("data" in e) {
|
|
68
|
+
let { data: t, ...n } = e;
|
|
69
|
+
r.current.update(n);
|
|
70
|
+
} else r.current.update(e);
|
|
71
|
+
}, [e]), null;
|
|
72
|
+
}
|
|
73
|
+
function m(e, t, n, r) {
|
|
74
|
+
let o = a(null), s = a(e), c = a(n), l = a(r);
|
|
75
|
+
i(() => {
|
|
76
|
+
s.current = e, c.current = n, l.current = r;
|
|
77
|
+
}, [
|
|
78
|
+
e,
|
|
79
|
+
n,
|
|
80
|
+
r
|
|
81
|
+
]), i(() => {
|
|
82
|
+
let e = s.current(c.current);
|
|
83
|
+
o.current = e;
|
|
84
|
+
let t = l.current?.(e);
|
|
85
|
+
return () => {
|
|
86
|
+
t?.(), e.delete(), o.current = null;
|
|
87
|
+
};
|
|
88
|
+
}, [t]), i(() => {
|
|
89
|
+
o.current && o.current.update(n);
|
|
90
|
+
}, [n]);
|
|
91
|
+
}
|
|
92
|
+
function h({ config: e, onReady: t }) {
|
|
93
|
+
let { view: r } = d();
|
|
94
|
+
return m(n((e) => r.addMesh(e), [r]), r, e, t), null;
|
|
95
|
+
}
|
|
96
|
+
function g({ config: e, onReady: t }) {
|
|
97
|
+
let { view: r } = d();
|
|
98
|
+
return m(n((e) => r.addLight(e), [r]), r, e, t), null;
|
|
99
|
+
}
|
|
100
|
+
function _({ config: e, onReady: t }) {
|
|
101
|
+
let { view: r } = d();
|
|
102
|
+
return m(n((e) => r.addEffect(e), [r]), r, e, t), null;
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
export { _ as EffectDesc, p as Layer, g as LightDesc, h as MeshDesc, f as ViewProvider, d as useViewContext };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports,require("@navaramap/three"),require("react")):typeof define==`function`&&define.amd?define([`exports`,`@navaramap/three`,`react`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.NavaraThreeReact={},e._navaramap_three,e.react))})(this,function(e,t,n){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});var r=Object.create,i=Object.defineProperty,a=Object.getOwnPropertyDescriptor,o=Object.getOwnPropertyNames,s=Object.getPrototypeOf,c=Object.prototype.hasOwnProperty,l=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),u=(e,t,n,r)=>{if(t&&typeof t==`object`||typeof t==`function`)for(var s=o(t),l=0,u=s.length,d;l<u;l++)d=s[l],!c.call(e,d)&&d!==n&&i(e,d,{get:(e=>t[e]).bind(null,d),enumerable:!(r=a(t,d))||r.enumerable});return e};t=((e,t,n)=>(n=e==null?{}:r(s(e)),u(t||!e||!e.__esModule?i(n,`default`,{value:e,enumerable:!0}):n,e)))(t,1);var d=l((e=>{var t=Symbol.for(`react.transitional.element`);function n(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.jsx=n})),f=l(((e,t)=>{t.exports=d()}))(),p=(0,n.createContext)(void 0),m=()=>{let e=(0,n.useContext)(p);if(!e)throw Error(`Navara React Error: You have to invoke this hook inside of ViewProvider.`);return e},h=({canvas:e,children:r,plugins:i,...a})=>{let[o,s]=(0,n.useState)(),[c,l]=(0,n.useState)(!1),u=(0,n.useRef)(void 0);return(0,n.useEffect)(()=>{if(u.current){console.warn(`You need to recreate ThreeView.`);return}let n=new t.default({canvas:e&&`current`in e?e.current:e,...a});if(i)for(let e of i)n.addPlugin(e);return s(n),u.current=n,(async()=>{try{await n.init(),l(!0)}catch(e){console.error(`Navara init failed:`,e)}})(),()=>{}},[e,a,i]),(0,f.jsx)(p.Provider,{value:{view:o},children:c?r:null})};function g({config:e,onReady:t}){let{view:r}=m(),i=(0,n.useRef)(null),a=(0,n.useRef)(e),o=(0,n.useRef)(t);return(0,n.useEffect)(()=>{a.current=e,o.current=t},[e,t]),(0,n.useEffect)(()=>{let e=r.addLayer(a.current);i.current=e;let t=o.current?.(e);return()=>{t?.(),e.delete(),i.current=null}},[r]),(0,n.useEffect)(()=>{if(i.current)if(`data`in e){let{data:t,...n}=e;i.current.update(n)}else i.current.update(e)},[e]),null}function _(e,t,r,i){let a=(0,n.useRef)(null),o=(0,n.useRef)(e),s=(0,n.useRef)(r),c=(0,n.useRef)(i);(0,n.useEffect)(()=>{o.current=e,s.current=r,c.current=i},[e,r,i]),(0,n.useEffect)(()=>{let e=o.current(s.current);a.current=e;let t=c.current?.(e);return()=>{t?.(),e.delete(),a.current=null}},[t]),(0,n.useEffect)(()=>{a.current&&a.current.update(r)},[r])}function v({config:e,onReady:t}){let{view:r}=m();return _((0,n.useCallback)(e=>r.addMesh(e),[r]),r,e,t),null}function y({config:e,onReady:t}){let{view:r}=m();return _((0,n.useCallback)(e=>r.addLight(e),[r]),r,e,t),null}function b({config:e,onReady:t}){let{view:r}=m();return _((0,n.useCallback)(e=>r.addEffect(e),[r]),r,e,t),null}e.EffectDesc=b,e.Layer=g,e.LightDesc=y,e.MeshDesc=v,e.ViewProvider=h,e.useViewContext=m});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@navaramap/three-react",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"main": "dist/index.umd.cjs",
|
|
6
|
+
"module": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT OR Apache-2.0",
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/reearth/navara.git",
|
|
16
|
+
"directory": "web/navara_three_react"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": ">=19",
|
|
23
|
+
"three": ">=0.183.0",
|
|
24
|
+
"@navaramap/three": "0.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/react": "19.2.17",
|
|
28
|
+
"@types/react-dom": "19.2.3",
|
|
29
|
+
"@types/three": "0.185.1",
|
|
30
|
+
"three": "0.185.1",
|
|
31
|
+
"@navaramap/three": "0.0.0"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"type-check": "tsc && tsc -p tsconfig.node.json",
|
|
35
|
+
"build": "pnpm type-check && vite build --watch=false",
|
|
36
|
+
"build:dev": "vite build --mode development",
|
|
37
|
+
"build:dev:watch": "pnpm build:dev --watch",
|
|
38
|
+
"test": "vitest --passWithNoTests --watch=false",
|
|
39
|
+
"lint": "eslint . --fix",
|
|
40
|
+
"lint:check": "eslint .",
|
|
41
|
+
"format": "prettier --write --ignore-path ../../.gitignore --ignore-path ../../.prettierignore .",
|
|
42
|
+
"format:check": "prettier --check --ignore-path ../../.gitignore --ignore-path ../../.prettierignore ."
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/Layer.tsx
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type BaseDesc,
|
|
3
|
+
type BaseHandle,
|
|
4
|
+
type MeshDesc as MeshDescClass,
|
|
5
|
+
type LightDesc as LightDescClass,
|
|
6
|
+
type EffectDesc as EffectDescClass,
|
|
7
|
+
type Layer as NavaraLayer,
|
|
8
|
+
type LayerDescription,
|
|
9
|
+
type MeshConfig,
|
|
10
|
+
type LightConfig,
|
|
11
|
+
type EffectConfig,
|
|
12
|
+
type BuiltInEffectDescription,
|
|
13
|
+
} from "@navaramap/three";
|
|
14
|
+
import { useCallback, useEffect, useRef, type PropsWithChildren } from "react";
|
|
15
|
+
|
|
16
|
+
import { useViewContext } from "./ViewContext";
|
|
17
|
+
|
|
18
|
+
type LayerProps = {
|
|
19
|
+
config: LayerDescription;
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
21
|
+
onReady?: (handle: NavaraLayer) => (() => void) | void;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function Layer({ config, onReady }: PropsWithChildren<LayerProps>) {
|
|
25
|
+
const { view } = useViewContext();
|
|
26
|
+
|
|
27
|
+
const handleRef = useRef<NavaraLayer | null>(null);
|
|
28
|
+
|
|
29
|
+
const configRef = useRef(config);
|
|
30
|
+
const onReadyRef = useRef(onReady);
|
|
31
|
+
|
|
32
|
+
// Sync the latest-value refs outside render (react-hooks/refs). This effect
|
|
33
|
+
// is declared first so it runs before the mount effect below re-runs.
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
configRef.current = config;
|
|
36
|
+
onReadyRef.current = onReady;
|
|
37
|
+
}, [config, onReady]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
const handle = view.addLayer(configRef.current);
|
|
41
|
+
handleRef.current = handle;
|
|
42
|
+
const unmount = onReadyRef.current?.(handle);
|
|
43
|
+
return () => {
|
|
44
|
+
unmount?.();
|
|
45
|
+
handle.delete();
|
|
46
|
+
handleRef.current = null;
|
|
47
|
+
};
|
|
48
|
+
}, [view]);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (handleRef.current) {
|
|
52
|
+
if ("data" in config) {
|
|
53
|
+
const { data: _data, ...withoutData } = config;
|
|
54
|
+
handleRef.current.update(withoutData);
|
|
55
|
+
} else {
|
|
56
|
+
handleRef.current.update(config);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}, [config]);
|
|
60
|
+
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function useDeclarationLayer<L extends BaseDesc>(
|
|
65
|
+
addFn: (config: Record<string, unknown>) => BaseHandle<L>,
|
|
66
|
+
view: object,
|
|
67
|
+
config: Record<string, unknown>,
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
69
|
+
onReady?: (handle: BaseHandle<L>) => (() => void) | void,
|
|
70
|
+
) {
|
|
71
|
+
const handleRef = useRef<BaseHandle<L> | null>(null);
|
|
72
|
+
const addFnRef = useRef(addFn);
|
|
73
|
+
const configRef = useRef(config);
|
|
74
|
+
const onReadyRef = useRef(onReady);
|
|
75
|
+
|
|
76
|
+
// Sync the latest-value refs outside render (react-hooks/refs). This effect
|
|
77
|
+
// is declared first so it runs before the mount effect below re-runs.
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
addFnRef.current = addFn;
|
|
80
|
+
configRef.current = config;
|
|
81
|
+
onReadyRef.current = onReady;
|
|
82
|
+
}, [addFn, config, onReady]);
|
|
83
|
+
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const handle = addFnRef.current(configRef.current);
|
|
86
|
+
handleRef.current = handle;
|
|
87
|
+
const unmount = onReadyRef.current?.(handle);
|
|
88
|
+
return () => {
|
|
89
|
+
unmount?.();
|
|
90
|
+
handle.delete();
|
|
91
|
+
handleRef.current = null;
|
|
92
|
+
};
|
|
93
|
+
}, [view]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (handleRef.current) {
|
|
97
|
+
handleRef.current.update(config as never);
|
|
98
|
+
}
|
|
99
|
+
}, [config]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type MeshDescProps<L extends MeshDescClass = MeshDescClass> = {
|
|
103
|
+
config: MeshConfig;
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
105
|
+
onReady?: (handle: BaseHandle<L>) => (() => void) | void;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export function MeshDesc<L extends MeshDescClass = MeshDescClass>({
|
|
109
|
+
config,
|
|
110
|
+
onReady,
|
|
111
|
+
}: PropsWithChildren<MeshDescProps<L>>) {
|
|
112
|
+
const { view } = useViewContext();
|
|
113
|
+
const addFn = useCallback(
|
|
114
|
+
(c: Record<string, unknown>) => view.addMesh<L>(c as MeshConfig),
|
|
115
|
+
[view],
|
|
116
|
+
);
|
|
117
|
+
useDeclarationLayer<L>(
|
|
118
|
+
addFn,
|
|
119
|
+
view,
|
|
120
|
+
config as Record<string, unknown>,
|
|
121
|
+
onReady,
|
|
122
|
+
);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
type LightDescProps<L extends LightDescClass = LightDescClass> = {
|
|
127
|
+
config: LightConfig;
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
129
|
+
onReady?: (handle: BaseHandle<L>) => (() => void) | void;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export function LightDesc<L extends LightDescClass = LightDescClass>({
|
|
133
|
+
config,
|
|
134
|
+
onReady,
|
|
135
|
+
}: PropsWithChildren<LightDescProps<L>>) {
|
|
136
|
+
const { view } = useViewContext();
|
|
137
|
+
const addFn = useCallback(
|
|
138
|
+
(c: Record<string, unknown>) => view.addLight<L>(c as LightConfig),
|
|
139
|
+
[view],
|
|
140
|
+
);
|
|
141
|
+
useDeclarationLayer<L>(
|
|
142
|
+
addFn,
|
|
143
|
+
view,
|
|
144
|
+
config as Record<string, unknown>,
|
|
145
|
+
onReady,
|
|
146
|
+
);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
type EffectDescProps<L extends EffectDescClass = EffectDescClass> = {
|
|
151
|
+
config: EffectConfig | BuiltInEffectDescription;
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
153
|
+
onReady?: (handle: BaseHandle<L>) => (() => void) | void;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export function EffectDesc<L extends EffectDescClass = EffectDescClass>({
|
|
157
|
+
config,
|
|
158
|
+
onReady,
|
|
159
|
+
}: PropsWithChildren<EffectDescProps<L>>) {
|
|
160
|
+
const { view } = useViewContext();
|
|
161
|
+
const addFn = useCallback(
|
|
162
|
+
(c: Record<string, unknown>) =>
|
|
163
|
+
view.addEffect<L>(c as EffectConfig | BuiltInEffectDescription),
|
|
164
|
+
[view],
|
|
165
|
+
);
|
|
166
|
+
useDeclarationLayer<L>(
|
|
167
|
+
addFn,
|
|
168
|
+
view,
|
|
169
|
+
config as Record<string, unknown>,
|
|
170
|
+
onReady,
|
|
171
|
+
);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import ThreeView from "@navaramap/three";
|
|
2
|
+
import type {
|
|
3
|
+
Plugin,
|
|
4
|
+
Options,
|
|
5
|
+
Descriptions,
|
|
6
|
+
EmptyDescriptions,
|
|
7
|
+
} from "@navaramap/three";
|
|
8
|
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
9
|
+
import type { FC, PropsWithChildren, RefObject } from "react";
|
|
10
|
+
|
|
11
|
+
type ViewContextValues<D extends Descriptions = EmptyDescriptions> = {
|
|
12
|
+
view?: ThreeView<D>;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ViewContext = createContext<ViewContextValues | undefined>(undefined);
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line react-refresh/only-export-components
|
|
18
|
+
export const useViewContext = <
|
|
19
|
+
D extends Descriptions = EmptyDescriptions,
|
|
20
|
+
>() => {
|
|
21
|
+
const ctx = useContext(ViewContext);
|
|
22
|
+
if (!ctx) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Navara React Error: You have to invoke this hook inside of ViewProvider.",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return ctx as Required<ViewContextValues<D>>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type ViewProviderProps = {
|
|
32
|
+
canvas?: HTMLCanvasElement | RefObject<HTMLCanvasElement>;
|
|
33
|
+
plugins?: Plugin[];
|
|
34
|
+
} & Options;
|
|
35
|
+
|
|
36
|
+
export const ViewProvider: FC<PropsWithChildren<ViewProviderProps>> = ({
|
|
37
|
+
canvas,
|
|
38
|
+
children,
|
|
39
|
+
plugins,
|
|
40
|
+
...opts
|
|
41
|
+
}) => {
|
|
42
|
+
const [view, setView] = useState<ThreeView | undefined>();
|
|
43
|
+
const [isReady, setIsReady] = useState(false);
|
|
44
|
+
// The ref is written when the view is created in the effect below; syncing it
|
|
45
|
+
// during render is both redundant and disallowed by react-hooks/refs.
|
|
46
|
+
const viewRef = useRef<ThreeView | undefined>(undefined);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (viewRef.current) {
|
|
50
|
+
console.warn("You need to recreate ThreeView.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const innerCanvas = canvas && "current" in canvas ? canvas.current : canvas;
|
|
55
|
+
|
|
56
|
+
const v = new ThreeView({ canvas: innerCanvas, ...opts });
|
|
57
|
+
if (plugins) {
|
|
58
|
+
for (const plugin of plugins) {
|
|
59
|
+
v.addPlugin(plugin);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
setView(v);
|
|
63
|
+
viewRef.current = v;
|
|
64
|
+
|
|
65
|
+
(async () => {
|
|
66
|
+
try {
|
|
67
|
+
await v.init();
|
|
68
|
+
setIsReady(true);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error("Navara init failed:", e);
|
|
71
|
+
}
|
|
72
|
+
})();
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
// TODO
|
|
76
|
+
// v.dispose();
|
|
77
|
+
};
|
|
78
|
+
}, [canvas, opts, plugins]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<ViewContext.Provider value={{ view }}>
|
|
82
|
+
{isReady ? children : null}
|
|
83
|
+
</ViewContext.Provider>
|
|
84
|
+
);
|
|
85
|
+
};
|
package/src/index.ts
ADDED