react-babylon-map 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.
Files changed (211) hide show
  1. package/.claude/settings.local.json +78 -0
  2. package/demo.html +161 -0
  3. package/dist/cjs/main.js +520 -0
  4. package/dist/es/main.mjs +20 -0
  5. package/dist/es/main.mjs.map +1 -0
  6. package/dist/es/main10.mjs +33 -0
  7. package/dist/es/main10.mjs.map +1 -0
  8. package/dist/es/main11.mjs +12 -0
  9. package/dist/es/main11.mjs.map +1 -0
  10. package/dist/es/main12.mjs +14 -0
  11. package/dist/es/main12.mjs.map +1 -0
  12. package/dist/es/main13.mjs +12 -0
  13. package/dist/es/main13.mjs.map +1 -0
  14. package/dist/es/main14.mjs +5 -0
  15. package/dist/es/main14.mjs.map +1 -0
  16. package/dist/es/main15.mjs +12 -0
  17. package/dist/es/main15.mjs.map +1 -0
  18. package/dist/es/main16.mjs +25 -0
  19. package/dist/es/main16.mjs.map +1 -0
  20. package/dist/es/main17.mjs +54 -0
  21. package/dist/es/main17.mjs.map +1 -0
  22. package/dist/es/main18.mjs +88 -0
  23. package/dist/es/main18.mjs.map +1 -0
  24. package/dist/es/main19.mjs +18 -0
  25. package/dist/es/main19.mjs.map +1 -0
  26. package/dist/es/main2.mjs +9 -0
  27. package/dist/es/main2.mjs.map +1 -0
  28. package/dist/es/main20.mjs +21 -0
  29. package/dist/es/main20.mjs.map +1 -0
  30. package/dist/es/main21.mjs +61 -0
  31. package/dist/es/main21.mjs.map +1 -0
  32. package/dist/es/main3.mjs +46 -0
  33. package/dist/es/main3.mjs.map +1 -0
  34. package/dist/es/main4.mjs +23 -0
  35. package/dist/es/main4.mjs.map +1 -0
  36. package/dist/es/main5.mjs +69 -0
  37. package/dist/es/main5.mjs.map +1 -0
  38. package/dist/es/main6.mjs +35 -0
  39. package/dist/es/main6.mjs.map +1 -0
  40. package/dist/es/main7.mjs +65 -0
  41. package/dist/es/main7.mjs.map +1 -0
  42. package/dist/es/main8.mjs +14 -0
  43. package/dist/es/main8.mjs.map +1 -0
  44. package/dist/es/main9.mjs +26 -0
  45. package/dist/es/main9.mjs.map +1 -0
  46. package/dist/maplibre/cjs/main.js +520 -0
  47. package/dist/maplibre/es/main.mjs +20 -0
  48. package/dist/maplibre/es/main.mjs.map +1 -0
  49. package/dist/maplibre/es/main10.mjs +33 -0
  50. package/dist/maplibre/es/main10.mjs.map +1 -0
  51. package/dist/maplibre/es/main11.mjs +12 -0
  52. package/dist/maplibre/es/main11.mjs.map +1 -0
  53. package/dist/maplibre/es/main12.mjs +14 -0
  54. package/dist/maplibre/es/main12.mjs.map +1 -0
  55. package/dist/maplibre/es/main13.mjs +12 -0
  56. package/dist/maplibre/es/main13.mjs.map +1 -0
  57. package/dist/maplibre/es/main14.mjs +5 -0
  58. package/dist/maplibre/es/main14.mjs.map +1 -0
  59. package/dist/maplibre/es/main15.mjs +12 -0
  60. package/dist/maplibre/es/main15.mjs.map +1 -0
  61. package/dist/maplibre/es/main16.mjs +25 -0
  62. package/dist/maplibre/es/main16.mjs.map +1 -0
  63. package/dist/maplibre/es/main17.mjs +54 -0
  64. package/dist/maplibre/es/main17.mjs.map +1 -0
  65. package/dist/maplibre/es/main18.mjs +88 -0
  66. package/dist/maplibre/es/main18.mjs.map +1 -0
  67. package/dist/maplibre/es/main19.mjs +18 -0
  68. package/dist/maplibre/es/main19.mjs.map +1 -0
  69. package/dist/maplibre/es/main2.mjs +9 -0
  70. package/dist/maplibre/es/main2.mjs.map +1 -0
  71. package/dist/maplibre/es/main20.mjs +61 -0
  72. package/dist/maplibre/es/main20.mjs.map +1 -0
  73. package/dist/maplibre/es/main21.mjs +21 -0
  74. package/dist/maplibre/es/main21.mjs.map +1 -0
  75. package/dist/maplibre/es/main3.mjs +46 -0
  76. package/dist/maplibre/es/main3.mjs.map +1 -0
  77. package/dist/maplibre/es/main4.mjs +23 -0
  78. package/dist/maplibre/es/main4.mjs.map +1 -0
  79. package/dist/maplibre/es/main5.mjs +69 -0
  80. package/dist/maplibre/es/main5.mjs.map +1 -0
  81. package/dist/maplibre/es/main6.mjs +35 -0
  82. package/dist/maplibre/es/main6.mjs.map +1 -0
  83. package/dist/maplibre/es/main7.mjs +65 -0
  84. package/dist/maplibre/es/main7.mjs.map +1 -0
  85. package/dist/maplibre/es/main8.mjs +14 -0
  86. package/dist/maplibre/es/main8.mjs.map +1 -0
  87. package/dist/maplibre/es/main9.mjs +26 -0
  88. package/dist/maplibre/es/main9.mjs.map +1 -0
  89. package/dist/maplibre/types/api/canvas-props.d.ts +9 -0
  90. package/dist/maplibre/types/api/coordinates.d.ts +13 -0
  91. package/dist/maplibre/types/api/coords-to-vector-3.d.ts +3 -0
  92. package/dist/maplibre/types/api/coords.d.ts +5 -0
  93. package/dist/maplibre/types/api/index.d.ts +7 -0
  94. package/dist/maplibre/types/api/near-coordinates.d.ts +13 -0
  95. package/dist/maplibre/types/api/use-map.d.ts +3 -0
  96. package/dist/maplibre/types/api/vector-3-to-coords.d.ts +2 -0
  97. package/dist/maplibre/types/core/canvas-in-layer/use-canvas-in-layer.d.ts +15 -0
  98. package/dist/maplibre/types/core/canvas-in-layer/use-render.d.ts +15 -0
  99. package/dist/maplibre/types/core/canvas-in-layer/use-root.d.ts +11 -0
  100. package/dist/maplibre/types/core/canvas-overlay/canvas-portal.d.ts +10 -0
  101. package/dist/maplibre/types/core/canvas-overlay/init-canvas-fc.d.ts +11 -0
  102. package/dist/maplibre/types/core/canvas-overlay/render.d.ts +1 -0
  103. package/dist/maplibre/types/core/canvas-overlay/sync-camera-fc.d.ts +12 -0
  104. package/dist/maplibre/types/core/coords-to-matrix.d.ts +9 -0
  105. package/dist/maplibre/types/core/earth-radius.d.ts +1 -0
  106. package/dist/maplibre/types/core/generic-map.d.ts +49 -0
  107. package/dist/maplibre/types/core/matrix-utils.d.ts +7 -0
  108. package/dist/maplibre/types/core/sync-camera.d.ts +7 -0
  109. package/dist/maplibre/types/core/use-babylon-map.d.ts +32 -0
  110. package/dist/maplibre/types/core/use-coords-to-matrix.d.ts +6 -0
  111. package/dist/maplibre/types/core/use-coords.d.ts +5 -0
  112. package/dist/maplibre/types/core/use-function.d.ts +1 -0
  113. package/dist/maplibre/types/maplibre/canvas.d.ts +4 -0
  114. package/dist/maplibre/types/maplibre.index.d.ts +4 -0
  115. package/dist/types/api/canvas-props.d.ts +9 -0
  116. package/dist/types/api/coordinates.d.ts +13 -0
  117. package/dist/types/api/coords-to-vector-3.d.ts +3 -0
  118. package/dist/types/api/coords.d.ts +5 -0
  119. package/dist/types/api/index.d.ts +7 -0
  120. package/dist/types/api/near-coordinates.d.ts +13 -0
  121. package/dist/types/api/use-map.d.ts +3 -0
  122. package/dist/types/api/vector-3-to-coords.d.ts +2 -0
  123. package/dist/types/core/canvas-in-layer/use-canvas-in-layer.d.ts +15 -0
  124. package/dist/types/core/canvas-in-layer/use-render.d.ts +15 -0
  125. package/dist/types/core/canvas-in-layer/use-root.d.ts +11 -0
  126. package/dist/types/core/canvas-overlay/canvas-portal.d.ts +10 -0
  127. package/dist/types/core/canvas-overlay/init-canvas-fc.d.ts +11 -0
  128. package/dist/types/core/canvas-overlay/render.d.ts +1 -0
  129. package/dist/types/core/canvas-overlay/sync-camera-fc.d.ts +12 -0
  130. package/dist/types/core/coords-to-matrix.d.ts +9 -0
  131. package/dist/types/core/earth-radius.d.ts +1 -0
  132. package/dist/types/core/generic-map.d.ts +49 -0
  133. package/dist/types/core/matrix-utils.d.ts +7 -0
  134. package/dist/types/core/sync-camera.d.ts +7 -0
  135. package/dist/types/core/use-babylon-map.d.ts +32 -0
  136. package/dist/types/core/use-coords-to-matrix.d.ts +6 -0
  137. package/dist/types/core/use-coords.d.ts +5 -0
  138. package/dist/types/core/use-function.d.ts +1 -0
  139. package/dist/types/mapbox/canvas.d.ts +4 -0
  140. package/dist/types/mapbox.index.d.ts +4 -0
  141. package/package.json +58 -0
  142. package/plan.md +719 -0
  143. package/src/api/canvas-props.ts +10 -0
  144. package/src/api/coordinates.tsx +83 -0
  145. package/src/api/coords-to-vector-3.ts +39 -0
  146. package/src/api/coords.tsx +6 -0
  147. package/src/api/index.ts +7 -0
  148. package/src/api/near-coordinates.tsx +87 -0
  149. package/src/api/use-map.ts +8 -0
  150. package/src/api/vector-3-to-coords.ts +13 -0
  151. package/src/core/canvas-in-layer/use-canvas-in-layer.tsx +27 -0
  152. package/src/core/canvas-in-layer/use-render.ts +43 -0
  153. package/src/core/canvas-in-layer/use-root.tsx +82 -0
  154. package/src/core/canvas-overlay/canvas-portal.tsx +98 -0
  155. package/src/core/canvas-overlay/init-canvas-fc.tsx +45 -0
  156. package/src/core/canvas-overlay/render.tsx +1 -0
  157. package/src/core/canvas-overlay/sync-camera-fc.tsx +83 -0
  158. package/src/core/coords-to-matrix.ts +21 -0
  159. package/src/core/earth-radius.ts +1 -0
  160. package/src/core/events.ts +55 -0
  161. package/src/core/generic-map.ts +59 -0
  162. package/src/core/map-engine.tsx +70 -0
  163. package/src/core/matrix-utils.ts +22 -0
  164. package/src/core/sync-camera.ts +29 -0
  165. package/src/core/use-babylon-map.ts +46 -0
  166. package/src/core/use-coords-to-matrix.ts +13 -0
  167. package/src/core/use-coords.tsx +22 -0
  168. package/src/core/use-function.ts +10 -0
  169. package/src/mapbox/canvas.tsx +59 -0
  170. package/src/mapbox.index.ts +7 -0
  171. package/src/maplibre/canvas.tsx +59 -0
  172. package/src/maplibre.index.ts +7 -0
  173. package/src/vite-env.d.ts +1 -0
  174. package/stories/.ladle/components.tsx +50 -0
  175. package/stories/.ladle/style.css +63 -0
  176. package/stories/package.json +31 -0
  177. package/stories/pnpm-lock.yaml +5450 -0
  178. package/stories/sandbox.config.json +3 -0
  179. package/stories/src/adaptive-dpr.tsx +34 -0
  180. package/stories/src/billboard.stories.tsx +111 -0
  181. package/stories/src/buildings-3d.stories.tsx +280 -0
  182. package/stories/src/canvas/mapbox.stories.tsx +113 -0
  183. package/stories/src/canvas/maplibre.stories.tsx +93 -0
  184. package/stories/src/comparison.stories.tsx +161 -0
  185. package/stories/src/extrude/chaillot.ts +8 -0
  186. package/stories/src/exude-coordinates.stories.tsx +139 -0
  187. package/stories/src/free-3d-buildings/get-buildings-data.ts +49 -0
  188. package/stories/src/html-on-top.stories.tsx +156 -0
  189. package/stories/src/ifc/ifc-to-babylon.ts +97 -0
  190. package/stories/src/ifc/ifc.main.ts +904 -0
  191. package/stories/src/ifc/ifc2bb.ts +343 -0
  192. package/stories/src/ifc/model.ifc +14155 -0
  193. package/stories/src/ifc.stories.tsx +276 -0
  194. package/stories/src/mapbox/story-mapbox.tsx +97 -0
  195. package/stories/src/maplibre/story-maplibre.tsx +36 -0
  196. package/stories/src/multi-coordinates.stories.tsx +115 -0
  197. package/stories/src/pivot-controls.stories.tsx +148 -0
  198. package/stories/src/postprocessing.stories.tsx +125 -0
  199. package/stories/src/render-on-demand.stories.tsx +76 -0
  200. package/stories/src/story-map.tsx +44 -0
  201. package/stories/src/sunlight.stories.tsx +215 -0
  202. package/stories/src/vite-env.d.ts +1 -0
  203. package/stories/tsconfig.json +32 -0
  204. package/stories/tsconfig.node.json +10 -0
  205. package/stories/vite.config.ts +27 -0
  206. package/tsconfig.json +31 -0
  207. package/tsconfig.mapbox.json +7 -0
  208. package/tsconfig.maplibre.json +7 -0
  209. package/tsconfig.node.json +10 -0
  210. package/tsconfig.types.json +25 -0
  211. package/vite.config.ts +65 -0
@@ -0,0 +1,10 @@
1
+ import { PropsWithChildren } from 'react';
2
+ import { Coords } from './coords';
3
+
4
+ export interface CanvasProps extends Coords, PropsWithChildren {
5
+ id?: string;
6
+ beforeId?: string;
7
+ frameloop?: 'always' | 'demand';
8
+ /** render on a separated `<canvas>` that sits on top of the map provider */
9
+ overlay?: boolean;
10
+ }
@@ -0,0 +1,83 @@
1
+ import { Color4, FreeCamera, HemisphericLight, Scene, Vector3 } from '@babylonjs/core';
2
+ import { memo, useEffect, useRef, useState } from 'react';
3
+ import { syncCamera } from '../core/sync-camera';
4
+ import { useCoordsToMatrix } from '../core/use-coords-to-matrix';
5
+ import { useBabylonMap, BabylonMapContext } from '../core/use-babylon-map';
6
+ import type { BabylonMap } from '../core/use-babylon-map';
7
+
8
+ export interface CoordinatesProps {
9
+ longitude: number;
10
+ latitude: number;
11
+ altitude?: number;
12
+ children?: React.ReactNode;
13
+ }
14
+
15
+ /**
16
+ * Place 3D content at specific geographic coordinates inside a `<Canvas>`.
17
+ * Creates a sub-scene with its own camera synced to the given geo position,
18
+ * mirroring react-three-map's createPortal approach.
19
+ */
20
+ export const Coordinates = memo<CoordinatesProps>(({
21
+ latitude, longitude, altitude = 0, children,
22
+ }) => {
23
+ const babylonMap = useBabylonMap();
24
+ const origin = useCoordsToMatrix({
25
+ latitude, longitude, altitude, fromLngLat: babylonMap.fromLngLat,
26
+ });
27
+
28
+ // Create a sub-scene for this coordinate portal
29
+ const [subScene] = useState(() => {
30
+ if (!babylonMap.engine) return null;
31
+ const sub = new Scene(babylonMap.engine);
32
+ sub.autoClear = false;
33
+ sub.clearColor = new Color4(0, 0, 0, 0);
34
+ return sub;
35
+ });
36
+
37
+ const cameraRef = useRef<FreeCamera | null>(null);
38
+
39
+ // Create camera + default light in sub-scene
40
+ useEffect(() => {
41
+ if (!subScene) return;
42
+ const cam = new FreeCamera('coordsCam', Vector3.Zero(), subScene);
43
+ cam.inputs.clear();
44
+ cam.minZ = 0;
45
+ cameraRef.current = cam;
46
+
47
+ // Add a default light so StandardMaterial isn't black
48
+ const light = new HemisphericLight('coordsLight', new Vector3(0, 1, 0), subScene);
49
+ light.intensity = 1;
50
+
51
+ return () => { cam.dispose(); light.dispose(); };
52
+ }, [subScene]);
53
+
54
+ // Render the sub-scene each frame, synced to its geographic position
55
+ useEffect(() => {
56
+ if (!subScene || !babylonMap.scene) return;
57
+ const mainScene = babylonMap.scene;
58
+
59
+ const observer = mainScene.onBeforeRenderObservable.add(() => {
60
+ if (!cameraRef.current) return;
61
+ syncCamera(cameraRef.current, origin, babylonMap.viewProjMx);
62
+ babylonMap.engine?.wipeCaches(true);
63
+ subScene.render(false);
64
+ });
65
+
66
+ return () => {
67
+ mainScene.onBeforeRenderObservable.remove(observer);
68
+ };
69
+ }, [subScene, babylonMap.scene, origin, babylonMap]);
70
+
71
+ // Provide child context pointing at the sub-scene
72
+ const childMap: BabylonMap = {
73
+ ...babylonMap,
74
+ scene: subScene || undefined,
75
+ };
76
+
77
+ return (
78
+ <BabylonMapContext.Provider value={childMap}>
79
+ {children}
80
+ </BabylonMapContext.Provider>
81
+ );
82
+ });
83
+ Coordinates.displayName = 'Coordinates';
@@ -0,0 +1,39 @@
1
+ import { earthRadius } from '../core/earth-radius';
2
+ import { Coords } from './coords';
3
+
4
+ const DEG2RAD = Math.PI / 180;
5
+
6
+ const mercatorScaleLookup: { [key: number]: number } = {};
7
+
8
+ function getMercatorScale(lat: number): number {
9
+ const index = Math.round(lat * 1000);
10
+ if (mercatorScaleLookup[index] === undefined) {
11
+ mercatorScaleLookup[index] = 1 / Math.cos(lat * DEG2RAD);
12
+ }
13
+ return mercatorScaleLookup[index];
14
+ }
15
+
16
+ export function averageMercatorScale(originLat: number, pointLat: number, steps = 10): number {
17
+ let totalScale = 0;
18
+ const latStep = (pointLat - originLat) / steps;
19
+ for (let i = 0; i <= steps; i++) {
20
+ const lat = originLat + latStep * i;
21
+ totalScale += getMercatorScale(lat);
22
+ }
23
+ return totalScale / (steps + 1);
24
+ }
25
+
26
+ export function coordsToVector3(point: Coords, origin: Coords): [number, number, number] {
27
+ const latitudeDiff = (point.latitude - origin.latitude) * DEG2RAD;
28
+ const longitudeDiff = (point.longitude - origin.longitude) * DEG2RAD;
29
+ const altitudeDiff = (point.altitude || 0) - (origin.altitude || 0);
30
+
31
+ const x = longitudeDiff * earthRadius * Math.cos(origin.latitude * DEG2RAD);
32
+ const y = altitudeDiff;
33
+
34
+ const steps = Math.ceil(Math.abs(point.latitude - origin.latitude)) * 100 + 1;
35
+ const avgScale = averageMercatorScale(origin.latitude, point.latitude, steps);
36
+
37
+ const z = ((-latitudeDiff * earthRadius) / getMercatorScale(origin.latitude)) * avgScale;
38
+ return [x, y, z];
39
+ }
@@ -0,0 +1,6 @@
1
+
2
+ export interface Coords {
3
+ longitude: number;
4
+ latitude: number;
5
+ altitude?: number;
6
+ }
@@ -0,0 +1,7 @@
1
+ export * from './canvas-props';
2
+ export * from './coordinates';
3
+ export * from './coords';
4
+ export * from './coords-to-vector-3';
5
+ export * from './near-coordinates';
6
+ export * from './vector-3-to-coords';
7
+ export { useBabylonMap, BabylonMapContext } from '../core/use-babylon-map';
@@ -0,0 +1,87 @@
1
+ import { Color4, FreeCamera, HemisphericLight, Scene, Vector3 } from '@babylonjs/core';
2
+ import { memo, useEffect, useRef, useState } from 'react';
3
+ import { useBabylonMap, BabylonMapContext } from '../core/use-babylon-map';
4
+ import type { BabylonMap } from '../core/use-babylon-map';
5
+ import { useCoords } from '../core/use-coords';
6
+ import { coordsToVector3 } from './coords-to-vector-3';
7
+
8
+ export interface NearCoordinatesProps {
9
+ longitude: number;
10
+ latitude: number;
11
+ altitude?: number;
12
+ children?: React.ReactNode;
13
+ }
14
+
15
+ /**
16
+ * Place 3D content at a nearby coordinate (relative to the Canvas origin).
17
+ * Uses the sub-scene approach like Coordinates but computes a relative offset
18
+ * for the camera's world matrix.
19
+ */
20
+ export const NearCoordinates = memo<NearCoordinatesProps>(({ children, ...coords }) => {
21
+ const { latitude, longitude, altitude } = useCoords();
22
+ const babylonMap = useBabylonMap();
23
+
24
+ const pos = coordsToVector3(coords, { latitude, longitude, altitude });
25
+
26
+ // Create a sub-scene for this coordinate portal
27
+ const [subScene] = useState(() => {
28
+ if (!babylonMap.engine) return null;
29
+ const sub = new Scene(babylonMap.engine);
30
+ sub.autoClear = false;
31
+ sub.clearColor = new Color4(0, 0, 0, 0);
32
+ return sub;
33
+ });
34
+
35
+ const cameraRef = useRef<FreeCamera | null>(null);
36
+
37
+ // Create camera + default light in sub-scene
38
+ useEffect(() => {
39
+ if (!subScene) return;
40
+ const cam = new FreeCamera('nearCoordsCam', new Vector3(pos[0], pos[1], pos[2]), subScene);
41
+ cam.inputs.clear();
42
+ cam.minZ = 0;
43
+ cameraRef.current = cam;
44
+
45
+ const light = new HemisphericLight('nearCoordsLight', new Vector3(0, 1, 0), subScene);
46
+ light.intensity = 1;
47
+
48
+ return () => { cam.dispose(); light.dispose(); };
49
+ }, [subScene]);
50
+
51
+ // Update camera position when offset changes
52
+ useEffect(() => {
53
+ if (!cameraRef.current) return;
54
+ cameraRef.current.position.set(pos[0], pos[1], pos[2]);
55
+ }, [pos]);
56
+
57
+ // Render the sub-scene each frame
58
+ useEffect(() => {
59
+ if (!subScene || !babylonMap.scene) return;
60
+ const mainScene = babylonMap.scene;
61
+
62
+ const observer = mainScene.onBeforeRenderObservable.add(() => {
63
+ if (!cameraRef.current) return;
64
+ // Use the same projection as the main scene but offset by local position
65
+ cameraRef.current.freezeProjectionMatrix(babylonMap.scene!.activeCamera!.getProjectionMatrix());
66
+ babylonMap.engine?.wipeCaches(true);
67
+ subScene.render(false);
68
+ });
69
+
70
+ return () => {
71
+ mainScene.onBeforeRenderObservable.remove(observer);
72
+ };
73
+ }, [subScene, babylonMap.scene, babylonMap]);
74
+
75
+ // Provide child context pointing at the sub-scene
76
+ const childMap: BabylonMap = {
77
+ ...babylonMap,
78
+ scene: subScene || undefined,
79
+ };
80
+
81
+ return (
82
+ <BabylonMapContext.Provider value={childMap}>
83
+ {children}
84
+ </BabylonMapContext.Provider>
85
+ );
86
+ });
87
+ NearCoordinates.displayName = 'NearCoordinates';
@@ -0,0 +1,8 @@
1
+ import { MapInstance } from '../core/generic-map';
2
+ import { useBabylonMap } from '../core/use-babylon-map';
3
+
4
+ /** Access the underlying map instance from inside a `<Canvas>`. */
5
+ export const useMap = <T extends MapInstance = MapInstance>(): T => {
6
+ const ctx = useBabylonMap();
7
+ return ctx.map as T;
8
+ };
@@ -0,0 +1,13 @@
1
+ import { earthRadius } from '../core/earth-radius';
2
+ import { Coords } from './coords';
3
+
4
+ const DEG2RAD = Math.PI / 180;
5
+ const RAD2DEG = 180 / Math.PI;
6
+
7
+ export function vector3ToCoords(position: [number, number, number], origin: Coords): Coords {
8
+ const [x, y, z] = position;
9
+ const latitude = origin.latitude + (-z / earthRadius) * RAD2DEG;
10
+ const longitude = origin.longitude + (x / earthRadius) * RAD2DEG / Math.cos(origin.latitude * DEG2RAD);
11
+ const altitude = (origin.altitude || 0) + y;
12
+ return { latitude, longitude, altitude };
13
+ }
@@ -0,0 +1,27 @@
1
+ import { CanvasProps } from '../../api/canvas-props';
2
+ import { FromLngLat, MapInstance } from '../generic-map';
3
+ import { useCoordsToMatrix } from '../use-coords-to-matrix';
4
+ import { useRender } from './use-render';
5
+ import { useRoot } from './use-root';
6
+
7
+ /** Get all the properties needed to render as a map `<Layer>` */
8
+ export function useCanvasInLayer(props: CanvasProps, fromLngLat: FromLngLat, map: MapInstance) {
9
+ const { latitude, longitude, altitude, frameloop } = props;
10
+
11
+ const origin = useCoordsToMatrix({
12
+ latitude, longitude, altitude, fromLngLat,
13
+ });
14
+
15
+ const { onRemove, scene, camera, babylonMap } = useRoot(fromLngLat, map, props);
16
+
17
+ const render = useRender({ origin, frameloop, scene, camera, map, babylonMap });
18
+
19
+ return {
20
+ id: props.id,
21
+ beforeId: props.beforeId,
22
+ onRemove,
23
+ render,
24
+ type: 'custom' as const,
25
+ renderingMode: '3d' as const,
26
+ };
27
+ }
@@ -0,0 +1,43 @@
1
+ import { FreeCamera, Matrix, Scene } from '@babylonjs/core';
2
+ import { MapInstance } from '../generic-map';
3
+ import { syncCamera } from '../sync-camera';
4
+ import { useFunction } from '../use-function';
5
+ import { BabylonMap } from '../use-babylon-map';
6
+
7
+ export function useRender({
8
+ map, origin, scene, camera, frameloop, babylonMap,
9
+ }: {
10
+ map: MapInstance;
11
+ origin: Matrix;
12
+ scene: Scene;
13
+ camera: FreeCamera;
14
+ frameloop?: 'always' | 'demand';
15
+ babylonMap: BabylonMap;
16
+ }) {
17
+ const render = useFunction((
18
+ _gl: WebGL2RenderingContext,
19
+ projViewMx: number[] | { defaultProjectionData: { mainMatrix: Record<string, number> } },
20
+ ) => {
21
+ const pVMx = 'defaultProjectionData' in projViewMx
22
+ ? Object.values(projViewMx.defaultProjectionData.mainMatrix)
23
+ : projViewMx;
24
+
25
+ // Update shared context
26
+ babylonMap.viewProjMx = pVMx as number[];
27
+
28
+ // Sync camera
29
+ syncCamera(camera, origin, pVMx as number[]);
30
+
31
+ // Wipe Babylon's internal GL state caches so the next render
32
+ // re-applies everything from scratch. This is critical when
33
+ // sharing a GL context with MapLibre.
34
+ scene.getEngine().wipeCaches(true);
35
+
36
+ // Render the Babylon scene into the shared GL context.
37
+ scene.render(false);
38
+
39
+ if (!frameloop || frameloop === 'always') map.triggerRepaint();
40
+ });
41
+
42
+ return render;
43
+ }
@@ -0,0 +1,82 @@
1
+ import { Color4, Engine, FreeCamera, Scene, Vector3 } from '@babylonjs/core';
2
+ import { createRoot, Root } from 'react-dom/client';
3
+ import { useEffect, useState } from 'react';
4
+ import { CanvasProps } from '../../api/canvas-props';
5
+ import { BabylonMap, BabylonMapContext, createBabylonMap } from '../use-babylon-map';
6
+ import { FromLngLat, MapInstance } from '../generic-map';
7
+ import { useFunction } from '../use-function';
8
+
9
+ interface RootState {
10
+ engine: Engine;
11
+ scene: Scene;
12
+ camera: FreeCamera;
13
+ reactRoot: Root;
14
+ container: HTMLDivElement;
15
+ babylonMap: BabylonMap;
16
+ }
17
+
18
+ export function useRoot(
19
+ fromLngLat: FromLngLat,
20
+ map: MapInstance,
21
+ { longitude, latitude, altitude, frameloop, ...props }: CanvasProps,
22
+ ) {
23
+ const [{ engine, scene, camera, reactRoot, babylonMap }] = useState<RootState>(() => {
24
+ const canvas = map.getCanvas();
25
+ const gl = (canvas.getContext('webgl2') || canvas.getContext('webgl')) as WebGLRenderingContext;
26
+
27
+ // Create Babylon Engine from MapLibre's GL context (same pattern as demo.html)
28
+ const engine = new Engine(
29
+ gl as WebGL2RenderingContext,
30
+ true,
31
+ { useHighPrecisionMatrix: true },
32
+ true,
33
+ );
34
+
35
+ const scene = new Scene(engine);
36
+ scene.autoClear = false;
37
+ scene.clearColor = new Color4(0, 0, 0, 0);
38
+ scene.detachControl(); // let MapLibre handle pointer events
39
+
40
+ const camera = new FreeCamera('cam', Vector3.Zero(), scene);
41
+ camera.inputs.clear();
42
+ camera.minZ = 0;
43
+
44
+ const babylonMap = createBabylonMap({ map, fromLngLat, engine, scene });
45
+
46
+ // Separate React root for children
47
+ const container = document.createElement('div');
48
+ const reactRoot = createRoot(container);
49
+
50
+ return { engine, scene, camera, reactRoot, container, babylonMap };
51
+ });
52
+
53
+ const onResize = useFunction(() => {
54
+ engine.resize();
55
+ });
56
+
57
+ const onRemove = useFunction(() => {
58
+ reactRoot.unmount();
59
+ scene.dispose();
60
+ engine.dispose();
61
+ });
62
+
63
+ // Update coords on babylonMap
64
+ babylonMap.coords = { longitude, latitude, altitude };
65
+
66
+ // Resize listener
67
+ useEffect(() => {
68
+ map.on('resize', onResize);
69
+ return () => { map.off('resize', onResize); };
70
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
71
+
72
+ // Render children into the separate React root
73
+ useEffect(() => {
74
+ reactRoot.render(
75
+ <BabylonMapContext.Provider value={babylonMap}>
76
+ {props.children}
77
+ </BabylonMapContext.Provider>,
78
+ );
79
+ }, [props.children]); // eslint-disable-line react-hooks/exhaustive-deps
80
+
81
+ return { onRemove, engine, scene, camera, babylonMap };
82
+ }
@@ -0,0 +1,98 @@
1
+ import { Color4, Engine, FreeCamera, Scene, Vector3 } from '@babylonjs/core';
2
+ import { memo, useEffect, useRef, useState } from 'react';
3
+ import { CanvasProps } from '../../api/canvas-props';
4
+ import { BabylonMap, BabylonMapContext, createBabylonMap } from '../use-babylon-map';
5
+ import { FromLngLat, MapInstance } from '../generic-map';
6
+ import { useFunction } from '../use-function';
7
+ import { SyncCameraFC } from './sync-camera-fc';
8
+
9
+ interface CanvasPortalProps extends CanvasProps {
10
+ setOnRender: (callback: () => (mx: number[]) => void) => void;
11
+ map: MapInstance;
12
+ fromLngLat: FromLngLat;
13
+ }
14
+
15
+ const canvasStyle: React.CSSProperties = {
16
+ position: 'absolute',
17
+ top: 0,
18
+ left: 0,
19
+ width: '100%',
20
+ height: '100%',
21
+ pointerEvents: 'none',
22
+ };
23
+
24
+ export const CanvasPortal = memo<CanvasPortalProps>(({
25
+ children, latitude, longitude, altitude,
26
+ setOnRender, map, fromLngLat,
27
+ }) => {
28
+
29
+ const canvasRef = useRef<HTMLCanvasElement>(null);
30
+ const mapCanvas = map.getCanvas();
31
+
32
+ const [babylonMap, setBabylonMap] = useState<BabylonMap | null>(null);
33
+ const [ready, setReady] = useState(false);
34
+
35
+ // Create Engine + Scene on mount
36
+ useEffect(() => {
37
+ const canvas = canvasRef.current!;
38
+ const engine = new Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true, useHighPrecisionMatrix: true });
39
+ const scene = new Scene(engine);
40
+ scene.clearColor = new Color4(0, 0, 0, 0);
41
+
42
+ const camera = new FreeCamera('cam', Vector3.Zero(), scene);
43
+ camera.inputs.clear();
44
+ camera.minZ = 0;
45
+
46
+ setBabylonMap(createBabylonMap({ map, fromLngLat, engine, scene }));
47
+
48
+ return () => {
49
+ scene.dispose();
50
+ engine.dispose();
51
+ };
52
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
53
+
54
+ // Resize handling
55
+ useEffect(() => {
56
+ if (!babylonMap?.engine) return;
57
+ const engine = babylonMap.engine;
58
+
59
+ const onResize = () => {
60
+ const c = canvasRef.current;
61
+ if (!c) return;
62
+ c.width = mapCanvas.clientWidth * window.devicePixelRatio;
63
+ c.height = mapCanvas.clientHeight * window.devicePixelRatio;
64
+ c.style.width = `${mapCanvas.clientWidth}px`;
65
+ c.style.height = `${mapCanvas.clientHeight}px`;
66
+ engine.resize();
67
+ };
68
+
69
+ onResize();
70
+ map.on('resize', onResize);
71
+ return () => { map.off('resize', onResize); };
72
+ }, [babylonMap, map, mapCanvas]);
73
+
74
+ const onReady = useFunction(() => {
75
+ setReady(true);
76
+ });
77
+
78
+ return (
79
+ <>
80
+ <canvas ref={canvasRef} style={canvasStyle} />
81
+ {babylonMap && (
82
+ <BabylonMapContext.Provider value={babylonMap}>
83
+ <SyncCameraFC
84
+ latitude={latitude}
85
+ longitude={longitude}
86
+ altitude={altitude}
87
+ setOnRender={setOnRender}
88
+ onReady={onReady}
89
+ map={map}
90
+ canvasRef={canvasRef}
91
+ />
92
+ {ready && children}
93
+ </BabylonMapContext.Provider>
94
+ )}
95
+ </>
96
+ );
97
+ });
98
+ CanvasPortal.displayName = 'CanvasPortal';
@@ -0,0 +1,45 @@
1
+ import { memo, useEffect, useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { CanvasProps } from '../../api/canvas-props';
4
+ import { FromLngLat, MapInstance } from '../generic-map';
5
+ import { CanvasPortal } from './canvas-portal';
6
+
7
+ interface InitCanvasFCProps extends CanvasProps {
8
+ map: MapInstance;
9
+ setOnRender: (callback: () => (mx: number[]) => void) => void;
10
+ frameloop?: 'always' | 'demand';
11
+ fromLngLat: FromLngLat;
12
+ }
13
+
14
+ export const InitCanvasFC = memo<InitCanvasFCProps>((props) => {
15
+ const canvas = props.map.getCanvas();
16
+
17
+ const [el] = useState(() => {
18
+ const el = document.createElement('div');
19
+ el.style.position = 'absolute';
20
+ el.style.top = '0';
21
+ el.style.bottom = '0';
22
+ el.style.left = '0';
23
+ el.style.right = '0';
24
+ el.style.pointerEvents = 'none';
25
+ return el;
26
+ });
27
+
28
+ useEffect(() => {
29
+ const parent = canvas.parentElement!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
30
+ parent.appendChild(el);
31
+ return () => {
32
+ parent.removeChild(el);
33
+ };
34
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
35
+
36
+ return (
37
+ <>
38
+ {createPortal(
39
+ <CanvasPortal {...props} />,
40
+ el,
41
+ )}
42
+ </>
43
+ );
44
+ });
45
+ InitCanvasFC.displayName = 'InitCanvasFC';
@@ -0,0 +1 @@
1
+ export type Render = (gl: WebGLRenderingContext, matrix: number[]) => void;
@@ -0,0 +1,83 @@
1
+ import { FreeCamera } from '@babylonjs/core';
2
+ import { memo, RefObject, useEffect, useRef, useMemo } from 'react';
3
+ import { Coords } from '../../api/coords';
4
+ import { MapInstance } from '../generic-map';
5
+ import { syncCamera } from '../sync-camera';
6
+ import { useCoordsToMatrix } from '../use-coords-to-matrix';
7
+ import { useFunction } from '../use-function';
8
+ import { useBabylonMap } from '../use-babylon-map';
9
+
10
+ interface SyncCameraFCProps extends Coords {
11
+ setOnRender?: (callback: () => (mx: number[]) => void) => void;
12
+ onReady?: () => void;
13
+ map: MapInstance;
14
+ canvasRef: RefObject<HTMLCanvasElement | null>;
15
+ }
16
+
17
+ /** React component to sync the Babylon camera with the map provider on each render. */
18
+ export const SyncCameraFC = memo<SyncCameraFCProps>(({
19
+ latitude, longitude, altitude = 0, setOnRender, onReady, map, canvasRef,
20
+ }) => {
21
+
22
+ const mapCanvas = map.getCanvas();
23
+ const babylonMap = useBabylonMap();
24
+ const scene = babylonMap.scene!;
25
+ const engine = babylonMap.engine!;
26
+
27
+ const origin = useCoordsToMatrix({ latitude, longitude, altitude, fromLngLat: babylonMap.fromLngLat });
28
+
29
+ const ready = useRef(false);
30
+
31
+ const triggerRepaint = useMemo(() => map.triggerRepaint, [map]);
32
+ const mapPaintRequests = useRef(0);
33
+ const triggerRepaintOff = useFunction(() => {
34
+ mapPaintRequests.current++;
35
+ });
36
+
37
+ // Render callback — called by MapLibre on each paint frame
38
+ const onRender = useFunction((viewProjMx: number[] | { defaultProjectionData: { mainMatrix: Record<string, number> } }) => {
39
+ map.triggerRepaint = triggerRepaintOff;
40
+
41
+ // Resize if needed
42
+ if (canvasRef.current && engine) {
43
+ if (canvasRef.current.width !== mapCanvas.width || canvasRef.current.height !== mapCanvas.height) {
44
+ canvasRef.current.width = mapCanvas.clientWidth * window.devicePixelRatio;
45
+ canvasRef.current.height = mapCanvas.clientHeight * window.devicePixelRatio;
46
+ canvasRef.current.style.width = `${mapCanvas.clientWidth}px`;
47
+ canvasRef.current.style.height = `${mapCanvas.clientHeight}px`;
48
+ engine.resize();
49
+ }
50
+ }
51
+
52
+ const pVMx = 'defaultProjectionData' in viewProjMx
53
+ ? Object.values(viewProjMx.defaultProjectionData.mainMatrix)
54
+ : viewProjMx;
55
+ babylonMap.viewProjMx = pVMx as number[];
56
+
57
+ // Sync camera
58
+ syncCamera(scene.activeCamera as FreeCamera, origin, babylonMap.viewProjMx);
59
+
60
+ // Wipe caches and render one frame
61
+ engine.wipeCaches(true);
62
+ scene.render(false);
63
+
64
+ if (!ready.current && onReady) {
65
+ ready.current = true;
66
+ onReady();
67
+ }
68
+
69
+ // Restore triggerRepaint for demand-based rendering
70
+ map.triggerRepaint = triggerRepaint;
71
+ if (mapPaintRequests.current > 0) {
72
+ mapPaintRequests.current = 0;
73
+ map.triggerRepaint();
74
+ }
75
+ });
76
+
77
+ useEffect(() => {
78
+ setOnRender && setOnRender(() => onRender);
79
+ }, [setOnRender, onRender]);
80
+
81
+ return null;
82
+ });
83
+ SyncCameraFC.displayName = 'SyncCameraFC';
@@ -0,0 +1,21 @@
1
+ import { Matrix, Quaternion, Vector3 } from '@babylonjs/core';
2
+ import { FromLngLat } from './generic-map';
3
+ import { Coords } from '../api/coords';
4
+
5
+ /** Calculate Babylon.js Matrix from coordinates.
6
+ * Returns a Babylon Matrix (world transform at the given geo coord).
7
+ * Matches the pattern from MapLibre + Babylon.js integration demo. */
8
+ export function coordsToMatrix({
9
+ longitude, latitude, altitude, fromLngLat
10
+ }: Coords & { fromLngLat: FromLngLat }): Matrix {
11
+ const center = fromLngLat([longitude, latitude], altitude);
12
+ const scaleUnit = center.meterInMercatorCoordinateUnits();
13
+
14
+ const position = new Vector3(center.x, center.y, center.z || 0);
15
+ const scaling = new Vector3(scaleUnit, scaleUnit, scaleUnit);
16
+ // Babylon default: +x east, +y up, +z north
17
+ // MapLibre default: +x east, -y north, +z up
18
+ const rotation = Quaternion.FromEulerAngles(Math.PI / 2, 0, 0);
19
+
20
+ return Matrix.Compose(scaling, rotation, position);
21
+ }
@@ -0,0 +1 @@
1
+ export const earthRadius = 6371008.8;