@cn-map/gaode-react-extra 0.0.0-stage → 0.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Jungzl.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,117 @@
1
- # Temporary Holding Version
1
+ # @cn-map/gaode-react-extra
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 高德地图 react 扩展组件,基于 [`@cn-map/gaode-react`](https://github.com/Jungzl/amap/tree/main/packages/react) 的 `GLCustomLayer` 与 [`@react-three/fiber`](https://r3f.docs.pmnd.rs) 集成 three.js 3D 渲染图层。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @cn-map/gaode-react-extra three@0.162.0 @types/three@0.162.0
9
+ ```
10
+
11
+ ```bash
12
+ yarn add @cn-map/gaode-react-extra three@0.162.0 @types/three@0.162.0
13
+ ```
14
+
15
+ ```bash
16
+ pnpm add @cn-map/gaode-react-extra three@0.162.0 @types/three@0.162.0
17
+ ```
18
+
19
+ ```bash
20
+ bun add @cn-map/gaode-react-extra three@0.162.0 @types/three@0.162.0
21
+ ```
22
+
23
+ > ⚠️ 由于 [`r163`](https://github.com/mrdoob/three.js/releases/tag/r163) 移除了 WebGL 1 支持,且高德 JS API 2.0 的 GL 上下文基于 WebGL 1,`three` 版本不得高于 `0.162.0`。
24
+
25
+ > `@cn-map/gaode-react` 与 `@react-three/fiber` 是 peer 依赖,npm 7+ / pnpm 会自动安装,yarn 需手动添加。
26
+
27
+ TypeScript 类型见 [`@cn-map/gaode-types`](../types/README.md)。
28
+
29
+ ## 使用
30
+
31
+ | 导出名称 | 类型 | 描述 |
32
+ | ---------- | ---- | --------------------------------------------- |
33
+ | `R3FLayer` | 组件 | 基于 `GLCustomLayer` 的 `three.js` 3D渲染图层 |
34
+
35
+ ```tsx
36
+ import { R3FLayer } from '@cn-map/gaode-react-extra'
37
+
38
+ const Demo = () => (
39
+ <R3FLayer>
40
+ <ambientLight intensity={Math.PI / 2} />
41
+ <spotLight angle={0.15} decay={0} intensity={Math.PI} penumbra={1} position={[10, 10, 10]} />
42
+ <pointLight decay={0} intensity={Math.PI} position={[-10, -10, -10]} />
43
+ <mesh>
44
+ <boxGeometry args={[1, 1, 1]} />
45
+ <meshStandardMaterial color="orange" />
46
+ </mesh>
47
+ </R3FLayer>
48
+ )
49
+ ```
50
+
51
+ ## 地理坐标
52
+
53
+ `GeoGroup` 可以直接用经纬度放置对象,内部会通过 `map.customCoords` 转换到当前图层坐标系;children 仍使用相对该经纬度的局部坐标。
54
+
55
+ ```tsx
56
+ import { GeoGroup, R3FLayer } from '@cn-map/gaode-react-extra'
57
+
58
+ const Demo = () => (
59
+ <R3FLayer>
60
+ <GeoGroup altitude={100} position={[120.1, 30.2]}>
61
+ <mesh>
62
+ <boxGeometry />
63
+ <meshStandardMaterial />
64
+ </mesh>
65
+ </GeoGroup>
66
+ </R3FLayer>
67
+ )
68
+ ```
69
+
70
+ 需要批量转换或读取转换函数时使用 `useMapCoordinates()`:
71
+
72
+ ```tsx
73
+ const coordinates = useMapCoordinates()
74
+ const positions = coordinates.lngLatsToScene(lnglats)
75
+ ```
76
+
77
+ 转换使用的 origin 与图层相机的 origin 一致,因此 2D/3D、平移、缩放和旋转下都保持对齐。
78
+
79
+ `position` 也支持三元组 `[lng, lat, altitude]`;如果同时传了 `altitude`,以 `altitude` 为准。
80
+
81
+ ## 2D 地图
82
+
83
+ 高德地图的 `viewMode` 默认为 `2D`。`R3FLayer` 会根据当前视图自动选择相机:`2D` 使用 `OrthographicCamera`,`3D` 使用 `PerspectiveCamera`,并在每个地图帧同步 AMap 的相机参数,无需额外配置。地图旋转(`rotation`)也会同步到相机。地图平移时相机会跟随当前地图中心,固定世界坐标的对象保持与地图的地理锚定。
84
+
85
+ > 如果通过 `camera` 传入自定义相机,或在子组件中通过 `makeDefault`(如 drei 的 `<PerspectiveCamera makeDefault />`)切换相机,2D 下必须使用正交相机、3D 下必须使用透视相机;相机类型不匹配时会输出 warning,并可能渲染空白或位置错误。
86
+
87
+ > ⚠️ 一个地图 canvas 只支持挂载一个 `R3FLayer`。第二个实例会输出 warning 并跳过挂载,不会破坏第一个图层。第二个实例不会自动接管;需要在第一个卸载后重新挂载。
88
+
89
+ ## renderer 所有权
90
+
91
+ 通过 `gl` 传入 renderer 实例或同步工厂时,`R3FLayer` 会在挂载期间临时修改 `render`、`autoClear` 和 `forceContextLoss`,并在卸载完成后恢复原值;不会 dispose renderer,也不会让 AMap 的 GL context 丢失。若要在别处复用同一个 renderer,请等 unmount 完成后再使用。
92
+
93
+ ## 更多用法
94
+
95
+ - `origin` 指定 Three.js 坐标系原点,省略时使用初始地图中心点。
96
+ - `ref` 指向底层 `AMap.GLCustomLayer`;`onComplete` 会转发图层 complete 事件。
97
+ - `gl` 支持 `WebGLRenderer` 实例、同步工厂或 `WebGLRendererParameters`;异步工厂以及使用其他 canvas/context 的 renderer 会被拒绝。
98
+ - React 19 的 `StrictMode` 和 `Activity` 已覆盖测试;`Activity` 隐藏时 R3F root 保留、帧循环暂停。
99
+
100
+ ## 渲染循环模式
101
+
102
+ | `frameloop` | 说明 |
103
+ | ------------------ | ----------------------------------------------------------------------------- |
104
+ | `demand`(默认值) | 按需渲染,需在子组件中调用 `invalidate()`(例如在 `useFrame` 中)请求地图重绘 |
105
+ | `always` | 持续渲染,内部通过 `requestAnimationFrame` 持续触发地图重绘 |
106
+ | `never` | 不主动渲染,需自行调用 `advance()` 请求渲染(用于完全接管渲染时机) |
107
+
108
+ ```tsx
109
+ import { useFrame } from '@react-three/fiber'
110
+
111
+ const Animation = () => {
112
+ // frameloop="demand"(默认值)时,需要调用 invalidate() 请求地图重绘
113
+ useFrame(({ invalidate }) => invalidate())
114
+
115
+ return null
116
+ }
117
+ ```
@@ -0,0 +1,68 @@
1
+ import { MapCoordinates } from "@cn-map/gaode-extra-shared";
2
+ import { FC, PropsWithChildren, ReactElement } from "react";
3
+ import { GLCustomLayerProps } from "@cn-map/gaode-react";
4
+ import { GLProps, RenderProps, Renderer } from "@react-three/fiber";
5
+ import { WebGLRenderer, WebGLRendererParameters } from "three";
6
+ /**
7
+ * Access the coordinate converter of the surrounding `R3FLayer`.
8
+ *
9
+ * The converter uses the same origin as the layer camera, so converted
10
+ * positions remain aligned with the map.
11
+ */
12
+ export declare function useMapCoordinates(): MapCoordinates;
13
+ interface GeoGroupProps extends PropsWithChildren {
14
+ /** Geographic position of the object. */
15
+ position: AMap.LngLatLike;
16
+ /**
17
+ * Altitude mapped to the scene's Z axis.
18
+ * Overrides a third tuple element such as `[lng, lat, altitude]`.
19
+ */
20
+ altitude?: number;
21
+ }
22
+ /**
23
+ * Place a R3F object using a geographic position.
24
+ *
25
+ * Children keep their local coordinates relative to the geographic origin.
26
+ */
27
+ declare const GeoGroup: ({ position, altitude, children }: GeoGroupProps) => ReactElement;
28
+ type GLDefaults = Omit<WebGLRendererParameters, "canvas"> & {
29
+ canvas: HTMLCanvasElement;
30
+ };
31
+ type GLFactory = (...args: never[]) => unknown;
32
+ type GLObjectProps = Exclude<GLProps, Renderer | GLFactory>;
33
+ type SyncRendererFactory = (defaults: GLDefaults) => WebGLRenderer;
34
+ /**
35
+ * `R3FLayer` needs the renderer before its first map frame, so async factories are excluded.
36
+ * Renderer results must also reuse the AMap canvas and context, which is checked at runtime.
37
+ */
38
+ type R3FLayerGLProps = WebGLRenderer | SyncRendererFactory | GLObjectProps;
39
+ interface R3FLayerProps extends PropsWithChildren<Omit<RenderProps<HTMLCanvasElement>, "frameloop" | "gl"> & Omit<GLCustomLayerProps, "init" | "render">> {
40
+ /**
41
+ * R3F renderer、同步工厂或 renderer 参数。
42
+ *
43
+ * 工厂必须同步返回 renderer,且 renderer 必须复用 `GLCustomLayer` 的 `canvas` 与 WebGL `context`。
44
+ * renderer 参数中的 `canvas` / `context` 会被忽略,始终使用图层的上下文。
45
+ * 异步工厂或使用其他 canvas/context 的 renderer 会被拒绝。
46
+ */
47
+ gl?: R3FLayerGLProps;
48
+ /**
49
+ * Three.js渲染坐标系原点经纬度
50
+ * @default 地图中心点
51
+ */
52
+ origin?: AMap.LngLatLike;
53
+ /**
54
+ * R3F's render mode. Set to `demand` to only render on state change or `never` to take control.
55
+ * 与 `@react-three/fiber` 的 `Canvas` 不同,默认值为 `"demand"`
56
+ *
57
+ * @default "demand"
58
+ *
59
+ * @see https://docs.pmnd.rs/react-three-fiber/advanced/scaling-performance#on-demand-rendering
60
+ */
61
+ frameloop?: RenderProps<HTMLCanvasElement>["frameloop"];
62
+ }
63
+ /**
64
+ * `GLCustomLayer` 与 [`@react-three/fiber`](https://r3f.docs.pmnd.rs) 结合使用的组件
65
+ * 可以直接在子组件中使用 `@react-three/fiber` 组件,无需手动处理 `GLCustomLayer` 的 `init` 和 `render`
66
+ */
67
+ declare const R3FLayer: FC<R3FLayerProps>;
68
+ export { GeoGroup, type GeoGroupProps, R3FLayer, type R3FLayerGLProps, type R3FLayerProps };
package/dist/index.js ADDED
@@ -0,0 +1,534 @@
1
+ import { createMapCoordinates, releaseCanvas, syncMapCamera, tryAcquireCanvas } from "@cn-map/gaode-extra-shared";
2
+ import { logger, lostContextError } from "@cn-map/gaode-shared/utils";
3
+ import * as React from "react";
4
+ import { Fragment, StrictMode, Suspense, createContext, use, useCallback, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
+ import { jsx } from "react/jsx-runtime";
6
+ import { GLCustomLayer, splitProps, useMapInstance } from "@cn-map/gaode-react";
7
+ import { FiberProvider, traverseFiber, useActivityBridge, useContextBridge, useFiber } from "its-fine";
8
+ import { _roots, applyProps, createRoot, events, unmountComponentAtNode } from "@react-three/fiber";
9
+ import { WebGLRenderer } from "three";
10
+ // src/r3f-layer/coordinates-context.ts
11
+ const EMPTY = Symbol("No MapCoordinates");
12
+ const MapCoordinatesContext = createContext(EMPTY);
13
+ /**
14
+ * Access the coordinate converter of the surrounding `R3FLayer`.
15
+ *
16
+ * The converter uses the same origin as the layer camera, so converted
17
+ * positions remain aligned with the map.
18
+ */
19
+ function useMapCoordinates() {
20
+ const context = use(MapCoordinatesContext);
21
+ if (context === EMPTY) throw lostContextError("`MapCoordinates`");
22
+ return useMemo(() => createMapCoordinates(context.map, context.origin), [context]);
23
+ }
24
+ // src/r3f-layer/geo-group.tsx
25
+ /**
26
+ * Place a R3F object using a geographic position.
27
+ *
28
+ * Children keep their local coordinates relative to the geographic origin.
29
+ */
30
+ const GeoGroup = ({ position, altitude, children }) => {
31
+ const coordinates = useMapCoordinates();
32
+ const scenePosition = useMemo(() => coordinates.lngLatToScene(position, altitude), [
33
+ altitude,
34
+ coordinates,
35
+ position
36
+ ]);
37
+ return /* @__PURE__ */ jsx("group", {
38
+ position: scenePosition,
39
+ children
40
+ });
41
+ };
42
+ // src/r3f-layer/map-frame-bridge.ts
43
+ /**
44
+ * 帧外请求与 AMap 帧之间的桥
45
+ *
46
+ * 核心约束:
47
+ * - 同一帧内 `advance` 优先,不能被随后到达的 `redraw` 降级(否则 `useFrame` 链会断掉)
48
+ * - `runInMapFrame` 在回调前取走并清空请求,保证一次请求只消费一次
49
+ * - `inMapFrame` 支持嵌套,回调抛错时也会恢复
50
+ */
51
+ var MapFrameBridge = class {
52
+ inMapFrame = false;
53
+ pending = null;
54
+ get isInMapFrame() {
55
+ return this.inMapFrame;
56
+ }
57
+ requestAdvance() {
58
+ this.pending = { kind: "advance" };
59
+ }
60
+ requestRedraw(scene, camera) {
61
+ if (this.pending?.kind === "advance") return;
62
+ this.pending = {
63
+ kind: "redraw",
64
+ draw: [scene, camera]
65
+ };
66
+ }
67
+ runInMapFrame(callback) {
68
+ const wasInMapFrame = this.inMapFrame;
69
+ const pending = this.pending;
70
+ this.pending = null;
71
+ this.inMapFrame = true;
72
+ try {
73
+ callback(pending);
74
+ } finally {
75
+ this.inMapFrame = wasInMapFrame;
76
+ }
77
+ }
78
+ };
79
+ // src/r3f-layer/use-frame-loop.ts
80
+ /**
81
+ * 将 R3F 的 frameloop 接入 AMap 的帧调度
82
+ */
83
+ function useFrameLoop(store, frameloop, requestAdvance) {
84
+ useLayoutEffect(() => {
85
+ if (!store) return;
86
+ if (!store.getState().gl) return;
87
+ if (frameloop !== "demand") return;
88
+ const setState = store.setState;
89
+ const { invalidate } = store.getState();
90
+ setState({
91
+ frameloop,
92
+ invalidate: () => requestAdvance()
93
+ });
94
+ requestAdvance();
95
+ return () => {
96
+ setState({
97
+ frameloop: "never",
98
+ invalidate
99
+ });
100
+ };
101
+ }, [
102
+ frameloop,
103
+ requestAdvance,
104
+ store
105
+ ]);
106
+ useLayoutEffect(() => {
107
+ if (!store || frameloop !== "always") return;
108
+ let raf = 0;
109
+ const tick = () => {
110
+ requestAdvance();
111
+ raf = requestAnimationFrame(tick);
112
+ };
113
+ raf = requestAnimationFrame(tick);
114
+ return () => cancelAnimationFrame(raf);
115
+ }, [
116
+ frameloop,
117
+ requestAdvance,
118
+ store
119
+ ]);
120
+ }
121
+ // src/r3f-layer/use-r3f-layer-render.ts
122
+ /**
123
+ * 同步 AMap 相机,并在 AMap 帧内执行 R3F 的 advance/redraw
124
+ */
125
+ function useR3FLayerRender({ frameBridge, originLngLat }) {
126
+ const warnedCameraMismatchRef = useRef(false);
127
+ return useCallback(({ canvas }, { map }, { viewMode }) => {
128
+ const store = _roots.get(canvas)?.store;
129
+ if (!store) return;
130
+ const state = store.getState();
131
+ if (!state.gl) return;
132
+ state.gl.resetState();
133
+ const result = syncMapCamera({
134
+ map,
135
+ camera: state.camera,
136
+ viewMode,
137
+ origin: originLngLat
138
+ });
139
+ if (!result.synced && !warnedCameraMismatchRef.current) {
140
+ warnedCameraMismatchRef.current = true;
141
+ logger.warn(`[R3FLayer] ${result.viewMode} view requires a ${result.expected}`);
142
+ }
143
+ try {
144
+ frameBridge.runInMapFrame((pending) => {
145
+ if (pending?.kind === "advance") state.advance(performance.now(), true);
146
+ else if (pending?.kind === "redraw") state.gl.render(pending.draw[0], pending.draw[1]);
147
+ else state.gl.render(state.scene, state.camera);
148
+ });
149
+ } finally {
150
+ state.gl.resetState();
151
+ }
152
+ }, [frameBridge, originLngLat]);
153
+ }
154
+ // src/r3f-layer/use-r3f-root-tree.tsx
155
+ const resolveActivityBridge = React["Activity"] ? useActivityBridge : () => Fragment;
156
+ /**
157
+ * 来自 `@react-three/fiber` 的 `useBridge`
158
+ * 桥接上层 Context、StrictMode 和 Activity 可见性
159
+ * @see https://github.com/pmndrs/react-three-fiber/blob/13e5e0e152562a9cb64c31dee1d10a2af7aa39c4/packages/fiber/src/core/utils/react.tsx#L70-L95
160
+ */
161
+ function useBridge() {
162
+ const fiber = useFiber();
163
+ const ContextBridge = useContextBridge();
164
+ const ActivityBridge = resolveActivityBridge();
165
+ return useMemo(() => ({ children }) => {
166
+ const Root = !!traverseFiber(fiber, true, (node) => node.type === StrictMode) ? StrictMode : Fragment;
167
+ return /* @__PURE__ */ jsx(Root, { children: /* @__PURE__ */ jsx(ActivityBridge, { children: /* @__PURE__ */ jsx(ContextBridge, { children }) }) });
168
+ }, [
169
+ fiber,
170
+ ContextBridge,
171
+ ActivityBridge
172
+ ]);
173
+ }
174
+ /**
175
+ * 将 React children 渲染到 R3F root
176
+ */
177
+ function useR3FRootTree({ children, root }) {
178
+ const Bridge = useBridge();
179
+ useLayoutEffect(() => {
180
+ if (!root) return;
181
+ root.render(/* @__PURE__ */ jsx(Bridge, { children: /* @__PURE__ */ jsx(Suspense, { children }) }));
182
+ }, [
183
+ children,
184
+ Bridge,
185
+ root
186
+ ]);
187
+ }
188
+ // src/r3f-layer/use-r3f-store.ts
189
+ const DEFAULT_GL_PROPS = {
190
+ powerPreference: "high-performance",
191
+ antialias: true,
192
+ alpha: true
193
+ };
194
+ function isRenderer(value) {
195
+ return !!value && typeof value === "object" && "render" in value && "domElement" in value && "getContext" in value;
196
+ }
197
+ function isPromiseLike(value) {
198
+ return !!value && typeof value.then === "function";
199
+ }
200
+ function getRendererProps(gl) {
201
+ if (!gl || typeof gl === "function" || isRenderer(gl)) return void 0;
202
+ const { canvas: _canvas, context: _context, ...props } = gl;
203
+ return props;
204
+ }
205
+ function assertRendererUsesMapSurface(renderer, canvas, context) {
206
+ if (renderer.domElement !== canvas || renderer.getContext() !== context) throw new Error("[R3FLayer] the gl renderer must reuse the AMap canvas and WebGL context; use the canvas/context from the factory defaults");
207
+ }
208
+ function logConfigureError(error) {
209
+ logger.error("[R3FLayer] failed to configure the R3F root", error);
210
+ }
211
+ /**
212
+ * 在 R3F 之外创建 renderer,保证 AMap 的 canvas/context 不会被用户配置覆盖。
213
+ * 实例传给 `root.configure` 后 R3F 只释放缓存,不会 dispose renderer 或丢失上下文。
214
+ */
215
+ function createRenderer(canvas, context, gl) {
216
+ const defaults = {
217
+ ...DEFAULT_GL_PROPS,
218
+ canvas,
219
+ context
220
+ };
221
+ if (typeof gl === "function") {
222
+ const renderer = gl(defaults);
223
+ if (isPromiseLike(renderer)) {
224
+ Promise.resolve(renderer).catch(() => {});
225
+ throw new Error("[R3FLayer] gl factories must return a renderer synchronously; async factories are not supported");
226
+ }
227
+ if (!isRenderer(renderer)) throw new Error("[R3FLayer] gl factories must return a WebGLRenderer");
228
+ assertRendererUsesMapSurface(renderer, canvas, context);
229
+ return renderer;
230
+ }
231
+ if (isRenderer(gl)) {
232
+ assertRendererUsesMapSurface(gl, canvas, context);
233
+ return gl;
234
+ }
235
+ const params = gl ?? {};
236
+ return new WebGLRenderer({
237
+ ...defaults,
238
+ ...params,
239
+ canvas,
240
+ context
241
+ });
242
+ }
243
+ /**
244
+ * 创建并同步 R3F root/store,同时管理尺寸监听和资源清理
245
+ */
246
+ function useR3FStoreSync({ frameBridge, map, renderProps, requestAdvance, requestRedraw }) {
247
+ const canvasRef = useRef(null);
248
+ const rootEntryRef = useRef(null);
249
+ const renderPropsRef = useRef(renderProps);
250
+ const lifecycleTokenRef = useRef(0);
251
+ const pendingRootCreatesRef = useRef(/* @__PURE__ */ new Map());
252
+ const claimedCanvasRef = useRef(null);
253
+ const ownerTokenRef = useRef(Symbol("R3FLayer"));
254
+ const warnedCanvasOwnershipRef = useRef(false);
255
+ const [ready, setReady] = useState(null);
256
+ renderPropsRef.current = renderProps;
257
+ const flushPendingRoot = useCallback((canvas) => {
258
+ const create = pendingRootCreatesRef.current.get(canvas);
259
+ if (!create) return;
260
+ pendingRootCreatesRef.current.delete(canvas);
261
+ create();
262
+ }, []);
263
+ const disposeRoot = useCallback((entry) => {
264
+ lifecycleTokenRef.current++;
265
+ pendingRootCreatesRef.current.delete(entry.canvas);
266
+ releaseCanvas(entry.canvas, ownerTokenRef.current);
267
+ if (claimedCanvasRef.current === entry.canvas) claimedCanvasRef.current = null;
268
+ if (rootEntryRef.current === entry) {
269
+ rootEntryRef.current = null;
270
+ if (canvasRef.current === entry.canvas) canvasRef.current = null;
271
+ }
272
+ unmountComponentAtNode(entry.canvas, () => {
273
+ entry.restoreRenderer?.();
274
+ entry.restoreRenderer = void 0;
275
+ flushPendingRoot(entry.canvas);
276
+ });
277
+ }, [flushPendingRoot]);
278
+ const releaseRoot = useCallback((entry) => {
279
+ disposeRoot(entry);
280
+ setReady((current) => current?.root === entry.root ? null : current);
281
+ }, [disposeRoot]);
282
+ useInsertionEffect(() => {
283
+ return () => {
284
+ pendingRootCreatesRef.current.clear();
285
+ const entry = rootEntryRef.current;
286
+ if (entry) disposeRoot(entry);
287
+ else if (claimedCanvasRef.current) {
288
+ releaseCanvas(claimedCanvasRef.current, ownerTokenRef.current);
289
+ claimedCanvasRef.current = null;
290
+ }
291
+ };
292
+ }, [disposeRoot]);
293
+ useLayoutEffect(() => {
294
+ const gl = map?.getGL();
295
+ if (!gl) {
296
+ const entry = rootEntryRef.current;
297
+ if (entry) releaseRoot(entry);
298
+ return;
299
+ }
300
+ const canvas = gl.canvas;
301
+ const existingEntry = rootEntryRef.current;
302
+ if (existingEntry?.canvas === canvas) return;
303
+ if (existingEntry) releaseRoot(existingEntry);
304
+ if (!tryAcquireCanvas(canvas, ownerTokenRef.current)) {
305
+ if (!warnedCanvasOwnershipRef.current) {
306
+ warnedCanvasOwnershipRef.current = true;
307
+ logger.warn("[R3FLayer] only one R3FLayer can be mounted per map canvas; skipping this layer");
308
+ }
309
+ return;
310
+ }
311
+ canvasRef.current = canvas;
312
+ claimedCanvasRef.current = canvas;
313
+ const token = ++lifecycleTokenRef.current;
314
+ const create = () => {
315
+ if (lifecycleTokenRef.current !== token) return;
316
+ if (_roots.has(canvas)) {
317
+ pendingRootCreatesRef.current.set(canvas, create);
318
+ unmountComponentAtNode(canvas, () => flushPendingRoot(canvas));
319
+ return;
320
+ }
321
+ const root = createRoot(canvas);
322
+ const entry = {
323
+ canvas,
324
+ root
325
+ };
326
+ rootEntryRef.current = entry;
327
+ const configure = (renderer, rendererProps) => {
328
+ if (lifecycleTokenRef.current !== token) return;
329
+ const originalRender = renderer.render;
330
+ const originalAutoClear = renderer.autoClear;
331
+ const originalForceContextLoss = renderer.forceContextLoss;
332
+ entry.restoreRenderer = () => {
333
+ renderer.render = originalRender;
334
+ renderer.autoClear = originalAutoClear;
335
+ renderer.forceContextLoss = originalForceContextLoss;
336
+ };
337
+ renderer.autoClear = false;
338
+ renderer.forceContextLoss = () => {};
339
+ const currentRenderProps = renderPropsRef.current;
340
+ let configured;
341
+ try {
342
+ configured = root.configure({
343
+ dpr: window.devicePixelRatio,
344
+ events,
345
+ ...currentRenderProps,
346
+ orthographic: map?.getView().type === "2D",
347
+ frameloop: "never",
348
+ gl: renderer,
349
+ onCreated: (state) => {
350
+ const draw = state.gl.render.bind(state.gl);
351
+ state.gl.render = (scene, camera) => {
352
+ if (frameBridge.isInMapFrame) {
353
+ draw(scene, camera);
354
+ return;
355
+ }
356
+ requestRedraw(scene, camera);
357
+ };
358
+ currentRenderProps.onCreated?.(state);
359
+ },
360
+ camera: {
361
+ aspect: canvas.clientWidth / canvas.clientHeight,
362
+ ...currentRenderProps.camera
363
+ },
364
+ size: {
365
+ width: canvas.clientWidth,
366
+ height: canvas.clientHeight,
367
+ top: canvas.offsetTop,
368
+ left: canvas.offsetLeft,
369
+ ...currentRenderProps.size
370
+ }
371
+ });
372
+ } catch (error) {
373
+ logConfigureError(error);
374
+ releaseRoot(entry);
375
+ return;
376
+ }
377
+ const onReady = () => {
378
+ if (lifecycleTokenRef.current !== token || rootEntryRef.current !== entry) return;
379
+ if (rendererProps) try {
380
+ applyProps(renderer, rendererProps);
381
+ } catch (error) {
382
+ logger.error("[R3FLayer] failed to apply gl renderer props", error);
383
+ }
384
+ const store = _roots.get(canvas)?.store;
385
+ if (!store) {
386
+ logger.error("[R3FLayer] the R3F root store is not available after configuration");
387
+ releaseRoot(entry);
388
+ return;
389
+ }
390
+ setReady({
391
+ root,
392
+ store
393
+ });
394
+ };
395
+ if (configured.status !== "fulfilled") {
396
+ logConfigureError(configured.status === "rejected" ? configured.reason : /* @__PURE__ */ new Error("[R3FLayer] R3F root configuration did not complete synchronously"));
397
+ releaseRoot(entry);
398
+ return;
399
+ }
400
+ onReady();
401
+ };
402
+ let renderer;
403
+ const glConfig = renderPropsRef.current.gl;
404
+ const rendererProps = getRendererProps(glConfig);
405
+ try {
406
+ renderer = createRenderer(canvas, gl, glConfig);
407
+ } catch (error) {
408
+ logger.error("[R3FLayer] failed to prepare the R3F renderer", error);
409
+ releaseRoot(entry);
410
+ return;
411
+ }
412
+ configure(renderer, rendererProps);
413
+ };
414
+ create();
415
+ }, [
416
+ flushPendingRoot,
417
+ frameBridge,
418
+ map,
419
+ releaseRoot,
420
+ requestRedraw
421
+ ]);
422
+ useLayoutEffect(() => {
423
+ if (!ready) return;
424
+ const { store } = ready;
425
+ const { advance } = store.getState();
426
+ store.setState({ advance: (timestamp, runGlobalEffects) => {
427
+ if (frameBridge.isInMapFrame) {
428
+ advance(timestamp, runGlobalEffects);
429
+ return;
430
+ }
431
+ requestAdvance();
432
+ } });
433
+ return () => {
434
+ store.setState({ advance });
435
+ };
436
+ }, [
437
+ frameBridge,
438
+ map,
439
+ requestAdvance,
440
+ ready
441
+ ]);
442
+ useLayoutEffect(() => {
443
+ if (!map) return;
444
+ if (!ready) return;
445
+ const canvas = canvasRef.current;
446
+ if (!canvas) return;
447
+ const { store } = ready;
448
+ const onResize = () => {
449
+ const { clientWidth, clientHeight, offsetLeft, offsetTop } = canvas;
450
+ const { setDpr, setSize, gl, camera } = store.getState();
451
+ if (map.getView().type === "3D") camera.aspect = clientWidth / clientHeight;
452
+ camera.updateProjectionMatrix();
453
+ gl.setSize(clientWidth, clientHeight);
454
+ setDpr(window.devicePixelRatio);
455
+ setSize(clientWidth, clientHeight, offsetTop, offsetLeft);
456
+ };
457
+ const ro = new ResizeObserver(onResize);
458
+ ro.observe(canvas);
459
+ return () => {
460
+ ro.disconnect();
461
+ };
462
+ }, [map, ready]);
463
+ return ready;
464
+ }
465
+ // src/r3f-layer/r3f-layer.tsx
466
+ const noopInit = () => {};
467
+ /**
468
+ * `GLCustomLayer` 与 [`@react-three/fiber`](https://r3f.docs.pmnd.rs) 结合使用的组件(内部实现)
469
+ */
470
+ const R3FLayerImpl = ({ children, frameloop = "demand", origin, ref, onComplete, map: _map, ...props }) => {
471
+ const map = useMapInstance(_map);
472
+ const [layerProps, r3fProps] = splitProps(props, [
473
+ "cacheSize",
474
+ "ce",
475
+ "depthTest",
476
+ "opacity",
477
+ "rejectMapMask",
478
+ "visible",
479
+ "zIndex",
480
+ "zooms"
481
+ ]);
482
+ const originLngLat = useMemo(() => {
483
+ if (origin) return origin;
484
+ return map?.getCenter().toArray() || [0, 0];
485
+ }, [origin, map]);
486
+ const [frameBridge] = useState(() => new MapFrameBridge());
487
+ const requestAdvance = useCallback(() => {
488
+ frameBridge.requestAdvance();
489
+ map?.render();
490
+ }, [frameBridge, map]);
491
+ const ready = useR3FStoreSync({
492
+ frameBridge,
493
+ map,
494
+ renderProps: r3fProps,
495
+ requestAdvance,
496
+ requestRedraw: useCallback((scene, camera) => {
497
+ frameBridge.requestRedraw(scene, camera);
498
+ map?.render();
499
+ }, [frameBridge, map])
500
+ });
501
+ const root = ready?.root ?? null;
502
+ useFrameLoop(ready?.store ?? null, frameloop, requestAdvance);
503
+ const coordinateContext = useMemo(() => map ? {
504
+ map,
505
+ origin: originLngLat
506
+ } : null, [map, originLngLat]);
507
+ useR3FRootTree({
508
+ children: useMemo(() => coordinateContext ? /* @__PURE__ */ jsx(MapCoordinatesContext, {
509
+ value: coordinateContext,
510
+ children
511
+ }) : children, [children, coordinateContext]),
512
+ root
513
+ });
514
+ const render = useR3FLayerRender({
515
+ frameBridge,
516
+ originLngLat
517
+ });
518
+ return /* @__PURE__ */ jsx(GLCustomLayer, {
519
+ ref,
520
+ init: noopInit,
521
+ render,
522
+ onComplete,
523
+ ...layerProps,
524
+ map: map ?? void 0
525
+ });
526
+ };
527
+ /**
528
+ * `GLCustomLayer` 与 [`@react-three/fiber`](https://r3f.docs.pmnd.rs) 结合使用的组件
529
+ * 可以直接在子组件中使用 `@react-three/fiber` 组件,无需手动处理 `GLCustomLayer` 的 `init` 和 `render`
530
+ */
531
+ const R3FLayer = (props) => {
532
+ return /* @__PURE__ */ jsx(FiberProvider, { children: /* @__PURE__ */ jsx(R3FLayerImpl, { ...props }) });
533
+ };
534
+ export { GeoGroup, R3FLayer, useMapCoordinates };
package/package.json CHANGED
@@ -1,6 +1,88 @@
1
1
  {
2
2
  "name": "@cn-map/gaode-react-extra",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "高德地图 react 扩展组件",
5
+ "keywords": [
6
+ "amap",
7
+ "react",
8
+ "three",
9
+ "高德",
10
+ "高德地图"
11
+ ],
12
+ "bugs": {
13
+ "url": "https://gtihub.com/Jungzl/amap/issues"
14
+ },
15
+ "license": "MIT",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/Jungzl/amap.git",
19
+ "directory": "packages/react-extra"
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "main": "dist/index.js",
27
+ "module": "dist/index.js",
28
+ "types": "dist/index.d.ts",
29
+ "imports": {
30
+ "#lib": "./src/index.ts"
31
+ },
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "dependencies": {
40
+ "@cn-map/gaode-extra-shared": "^0.0.1",
41
+ "@cn-map/gaode-shared": "^0.0.1"
42
+ },
43
+ "devDependencies": {
44
+ "@cn-map/gaode-react": "0.0.1",
45
+ "@cn-map/gaode-types": "^2.1.0",
46
+ "@react-three/fiber": "^9.8.2",
47
+ "@types/react": "^19.3.0",
48
+ "@types/react-dom": "^19.3.0",
49
+ "@types/three": "0.162.0",
50
+ "@vitejs/plugin-react": "^6.3.0",
51
+ "its-fine": "^2.1.1",
52
+ "react": "^19.3.1",
53
+ "react-dom": "^19.3.1",
54
+ "three": "0.162.0",
55
+ "vitest-browser-react": "^2.3.0"
56
+ },
57
+ "peerDependencies": {
58
+ "@cn-map/gaode-react": "^0.0.1",
59
+ "@cn-map/gaode-types": "^2.0.0",
60
+ "@react-three/fiber": "^9.8.1",
61
+ "@types/react": "^19.2.0",
62
+ "@types/three": "<=0.162.0",
63
+ "its-fine": "^2.1.1",
64
+ "react": "^19.2.0",
65
+ "react-dom": "^19.2.0",
66
+ "three": "<=0.162.0"
67
+ },
68
+ "peerDependenciesMeta": {
69
+ "@cn-map/gaode-types": {
70
+ "optional": true
71
+ },
72
+ "@types/react": {
73
+ "optional": true
74
+ },
75
+ "@types/three": {
76
+ "optional": true
77
+ }
78
+ },
79
+ "scripts": {
80
+ "build": "tsdown",
81
+ "dev": "tsdown --watch",
82
+ "check:type": "tsc",
83
+ "test": "vitest run",
84
+ "test:watch": "vitest",
85
+ "test:unit": "vitest run -p unit",
86
+ "test:e2e": "vitest run -p e2e"
87
+ }
6
88
  }