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,3 @@
1
+ {
2
+ "port": 61000
3
+ }
@@ -0,0 +1,34 @@
1
+ import { memo, useEffect, useState } from "react";
2
+ import { useBabylonMap } from 'react-babylon-map';
3
+
4
+ /**
5
+ * Adjusts the engine's hardware scaling level based on map movement.
6
+ * Decreases resolution during map interaction for performance,
7
+ * restores full resolution when stationary.
8
+ */
9
+ export const AdaptiveDpr = memo(() => {
10
+ const { engine, map } = useBabylonMap();
11
+ const [initialDpr, setInitialDpr] = useState(1);
12
+
13
+ useEffect(() => {
14
+ if (!engine) return;
15
+ setInitialDpr(engine.getHardwareScalingLevel());
16
+ }, [engine]);
17
+
18
+ useEffect(() => {
19
+ if (!engine || !map) return;
20
+
21
+ const decreaseDpr = () => engine.setHardwareScalingLevel(2);
22
+ const increaseDpr = () => engine.setHardwareScalingLevel(initialDpr);
23
+
24
+ map.on('movestart', decreaseDpr);
25
+ map.on('moveend', increaseDpr);
26
+ return () => {
27
+ map.off('movestart', decreaseDpr);
28
+ map.off('moveend', increaseDpr);
29
+ };
30
+ }, [engine, map, initialDpr]);
31
+
32
+ return null;
33
+ });
34
+ AdaptiveDpr.displayName = 'AdaptiveDpr';
@@ -0,0 +1,111 @@
1
+ import { FC, memo, useEffect } from "react";
2
+ import {
3
+ MeshBuilder, StandardMaterial, DynamicTexture,
4
+ HemisphericLight, Vector3, Color3, Scene,
5
+ } from '@babylonjs/core';
6
+ import { useBabylonMap } from 'react-babylon-map';
7
+ import { StoryMap } from "./story-map";
8
+
9
+ function Lights() {
10
+ const { scene } = useBabylonMap();
11
+ useEffect(() => {
12
+ if (!scene) return;
13
+ const light = new HemisphericLight("hemiLight", new Vector3(1, 4.5, 3), scene);
14
+ light.intensity = Math.PI;
15
+ light.groundColor = Color3.FromHexString("#60666C");
16
+ return () => { light.dispose(); };
17
+ }, [scene]);
18
+ return null;
19
+ }
20
+
21
+ /** Cylinder mesh — mirrors drei's <Cylinder> */
22
+ const Cylinder: FC<{ args: [number, number, number]; position: [number, number, number]; color: string }> = memo(({ args, position, color }) => {
23
+ const { scene } = useBabylonMap();
24
+ useEffect(() => {
25
+ if (!scene) return;
26
+ const [radiusTop, radiusBottom, height] = args;
27
+ const mesh = MeshBuilder.CreateCylinder('cylinder', {
28
+ diameterTop: radiusTop * 2,
29
+ diameterBottom: radiusBottom * 2,
30
+ height,
31
+ }, scene);
32
+ mesh.position.set(position[0], position[1], position[2]);
33
+ const mat = new StandardMaterial('cylMat', scene);
34
+ mat.diffuseColor = Color3.FromHexString(color);
35
+ mat.emissiveColor = Color3.FromHexString(color).scale(0.3);
36
+ mesh.material = mat;
37
+ return () => { mesh.dispose(); mat.dispose(); };
38
+ }, [scene, args, position, color]);
39
+ return null;
40
+ });
41
+ Cylinder.displayName = 'Cylinder';
42
+
43
+ /** Creates a plane with dynamic texture text — equivalent to drei's <Text> */
44
+ function createTextPlane(text: string, fontSize: number, color: string, scene: Scene) {
45
+ // In drei, fontSize is in world units. We render text large in texture and scale plane to match.
46
+ const textureWidth = 512;
47
+ const textureHeight = 256;
48
+ const tex = new DynamicTexture('textTex', { width: textureWidth, height: textureHeight }, scene, true);
49
+ const ctx = tex.getContext();
50
+ ctx.clearRect(0, 0, textureWidth, textureHeight);
51
+ // Fill text large relative to texture, then size plane to world units
52
+ ctx.font = `bold 200px Arial`;
53
+ ctx.fillStyle = color;
54
+ ctx.textAlign = 'center';
55
+ ctx.textBaseline = 'middle';
56
+ ctx.fillText(text, textureWidth / 2, textureHeight / 2);
57
+ tex.update();
58
+
59
+ // Plane size in world units: fontSize maps to world height
60
+ const planeWidth = fontSize * (textureWidth / textureHeight);
61
+ const planeHeight = fontSize;
62
+ const plane = MeshBuilder.CreatePlane('textPlane', { width: planeWidth, height: planeHeight }, scene);
63
+ const mat = new StandardMaterial('textMat', scene);
64
+ mat.diffuseTexture = tex;
65
+ mat.emissiveTexture = tex;
66
+ mat.emissiveColor = Color3.White();
67
+ mat.opacityTexture = tex;
68
+ mat.backFaceCulling = false;
69
+ mat.useAlphaFromDiffuseTexture = true;
70
+ plane.material = mat;
71
+ return { plane, mat, tex };
72
+ }
73
+
74
+ /** Text plane that always faces the camera (billboard effect) */
75
+ const BillboardText: FC<{ text: string; fontSize: number; color: string; position: [number, number, number] }> = memo(({ text, fontSize, color, position }) => {
76
+ const { scene } = useBabylonMap();
77
+ useEffect(() => {
78
+ if (!scene) return;
79
+ const { plane, mat, tex } = createTextPlane(text, fontSize, color, scene);
80
+ plane.position.set(position[0], position[1], position[2]);
81
+
82
+ // Billboard: face camera each frame by copying inverse camera rotation
83
+ const observer = scene.onBeforeRenderObservable.add(() => {
84
+ const cam = scene.activeCamera;
85
+ if (!cam) return;
86
+ // Invert camera rotation so the plane faces toward the viewer
87
+ plane.rotationQuaternion = cam.absoluteRotation.clone().invert();
88
+ });
89
+
90
+ return () => {
91
+ scene.onBeforeRenderObservable.remove(observer);
92
+ plane.dispose();
93
+ tex.dispose();
94
+ mat.dispose();
95
+ };
96
+ }, [scene, text, fontSize, color, position]);
97
+ return null;
98
+ });
99
+ BillboardText.displayName = 'BillboardText';
100
+
101
+ export default { title: 'Billboard' };
102
+
103
+ export function Default() {
104
+ return (
105
+ <StoryMap latitude={51} longitude={0} zoom={18} pitch={60}>
106
+ <Lights />
107
+ <Cylinder args={[10, 1, 40]} position={[0, 20, 0]} color="#FFFF00" />
108
+ <BillboardText text="Hi!" fontSize={17} color="#2592a8" position={[0, 50, 0]} />
109
+ </StoryMap>
110
+ );
111
+ }
@@ -0,0 +1,280 @@
1
+ import { FC, memo, useEffect, useMemo, useRef } from "react";
2
+ import {
3
+ Mesh, MeshBuilder, StandardMaterial, Vector3, Color3, VertexData, Scene,
4
+ HemisphericLight, DirectionalLight, DefaultRenderingPipeline,
5
+ } from "@babylonjs/core";
6
+ import { levaStore, useControls } from "leva";
7
+ import { useBabylonMap, coordsToVector3, Coords } from "react-babylon-map";
8
+ import { StoryMap } from "./story-map";
9
+ import { AdaptiveDpr } from "./adaptive-dpr";
10
+ import { getBuildingsData, OverpassElement } from "./free-3d-buildings/get-buildings-data";
11
+ import { suspend } from "suspend-react";
12
+
13
+ const coords: Coords = { latitude: 51.5074, longitude: -0.1278 };
14
+
15
+ function rand(min: number, max: number) {
16
+ return min + Math.random() * (max - min);
17
+ }
18
+
19
+ // Light accent colors (HSL ranges for animated buildings)
20
+ const night = new Color3(0, 0, 0.08);
21
+ const day = new Color3(1, 0.5, 0.1);
22
+
23
+ const _c0 = new Color3();
24
+ const _c1 = new Color3();
25
+
26
+ function lerpHSL(c0: Color3, c1: Color3, t: number): Color3 {
27
+ const hsv0 = c0.toHSVToRef(_c0);
28
+ const hsv1 = c1.toHSVToRef(_c1);
29
+ const h = hsv0.r + (hsv1.r - hsv0.r) * t;
30
+ const s = hsv0.g + (hsv1.g - hsv0.g) * t;
31
+ const v = hsv0.b + (hsv1.b - hsv0.b) * t;
32
+ return Color3.FromHSV(h, s, v);
33
+ }
34
+
35
+ interface BuildingAnimData {
36
+ c0: Color3;
37
+ c1: Color3;
38
+ animOffset: number;
39
+ animSpeed: number;
40
+ emissiveIntensity: number;
41
+ }
42
+
43
+ function createBuildingMesh(
44
+ poly: { lat: number; lon: number }[],
45
+ origin: Coords,
46
+ base: number,
47
+ height: number,
48
+ scene: Scene,
49
+ index: number,
50
+ ): { ribbon: Mesh; cap: Mesh } | null {
51
+ if (poly.length < 3) return null;
52
+
53
+ const n = poly.length;
54
+ const positions = poly.map(p =>
55
+ coordsToVector3({ longitude: p.lon, latitude: p.lat }, origin)
56
+ );
57
+
58
+ // Side walls via ribbon
59
+ const bottom: Vector3[] = [];
60
+ const top: Vector3[] = [];
61
+ for (let i = 0; i < n; i++) {
62
+ bottom.push(new Vector3(positions[i][0], base, positions[i][2]));
63
+ top.push(new Vector3(positions[i][0], height, positions[i][2]));
64
+ }
65
+ bottom.push(bottom[0].clone());
66
+ top.push(top[0].clone());
67
+
68
+ const ribbon = MeshBuilder.CreateRibbon(`building${index}`, {
69
+ pathArray: [bottom, top],
70
+ closePath: true,
71
+ closeArray: false,
72
+ }, scene);
73
+
74
+ // Top cap with fan triangulation
75
+ const capPositions: number[] = [];
76
+ const capIndices: number[] = [];
77
+ for (let i = 0; i < n; i++) {
78
+ capPositions.push(positions[i][0], height, positions[i][2]);
79
+ }
80
+ // Center vertex
81
+ let cx = 0, cz = 0;
82
+ for (const p of positions) { cx += p[0]; cz += p[2]; }
83
+ cx /= n; cz /= n;
84
+ capPositions.push(cx, height, cz);
85
+ const centerIdx = n;
86
+ for (let i = 0; i < n; i++) {
87
+ const next = (i + 1) % n;
88
+ capIndices.push(centerIdx, i, next);
89
+ }
90
+
91
+ const cap = MeshBuilder.CreateDisc(`buildingCap${index}`, { radius: 1, tessellation: n }, scene);
92
+ const capVD = new VertexData();
93
+ capVD.positions = capPositions;
94
+ capVD.indices = capIndices;
95
+ const normals = new Float32Array(capPositions.length);
96
+ VertexData.ComputeNormals(capPositions, capIndices, normals);
97
+ capVD.normals = normals;
98
+ capVD.applyToMesh(cap);
99
+
100
+ return { ribbon, cap };
101
+ }
102
+
103
+ const Buildings: FC<{
104
+ buildingsCenter: Coords;
105
+ origin: Coords;
106
+ }> = memo(({ buildingsCenter, origin }) => {
107
+ const { scene } = useBabylonMap();
108
+
109
+ const buildings = suspend(
110
+ () => {
111
+ const start = { ...buildingsCenter };
112
+ start.latitude -= 0.01;
113
+ start.longitude -= 0.01;
114
+ const end = { ...buildingsCenter };
115
+ end.latitude += 0.01;
116
+ end.longitude += 0.01;
117
+ return getBuildingsData({ start, end });
118
+ },
119
+ [buildingsCenter]
120
+ );
121
+
122
+ const lastTime = useRef(performance.now());
123
+
124
+ // Prepare animation data for each building
125
+ const animData = useMemo<BuildingAnimData[]>(() => {
126
+ return buildings.map(() => ({
127
+ c0: lerpHSL(night, day, rand(0, 1)).clone(),
128
+ c1: lerpHSL(day, night, rand(0, 1)).clone(),
129
+ animOffset: rand(0, 2 * Math.PI),
130
+ animSpeed: rand(1, 2),
131
+ emissiveIntensity: rand(0, 1) < 0.05 ? 3.5 : 0,
132
+ }));
133
+ }, [buildings]);
134
+
135
+ // Create meshes and materials
136
+ const meshData = useRef<{ meshes: Mesh[]; mats: StandardMaterial[] }>({ meshes: [], mats: [] });
137
+
138
+ useEffect(() => {
139
+ if (!scene) return;
140
+
141
+ // Clean up previous
142
+ meshData.current.meshes.forEach(m => m.dispose());
143
+ meshData.current.mats.forEach(m => m.dispose());
144
+
145
+ if (buildings.length === 0) return;
146
+
147
+ const meshes: Mesh[] = [];
148
+ const mats: StandardMaterial[] = [];
149
+
150
+ for (let i = 0; i < buildings.length; i++) {
151
+ const element = buildings[i];
152
+ const poly = element.geometry;
153
+ if (!poly || poly.length < 3) continue;
154
+
155
+ let height = parseFloat(element.tags?.height || "0");
156
+ if (!height) height = parseFloat(element.tags?.["building:levels"] || "1") * 3;
157
+ const base = parseFloat(element.tags?.min_height || "0");
158
+
159
+ const result = createBuildingMesh(poly, origin, base, height, scene, i);
160
+ if (!result) continue;
161
+
162
+ const mat = new StandardMaterial(`bMat${i}`, scene);
163
+ mat.diffuseColor = lerpHSL(night, day, rand(0, 1)).clone();
164
+ mat.specularPower = 64;
165
+ mat.emissiveColor = Color3.Black();
166
+ mat.alpha = 0.9;
167
+
168
+ result.ribbon.material = mat;
169
+ result.cap.material = mat;
170
+
171
+ meshes.push(result.ribbon, result.cap);
172
+ mats.push(mat);
173
+ }
174
+
175
+ meshData.current = { meshes, mats };
176
+
177
+ return () => {
178
+ meshes.forEach(m => m.dispose());
179
+ mats.forEach(m => m.dispose());
180
+ };
181
+ }, [scene, buildings, origin]);
182
+
183
+ // Animate colors
184
+ useEffect(() => {
185
+ if (!scene) return;
186
+
187
+ const observer = scene.onBeforeRenderObservable.add(() => {
188
+ const now = performance.now();
189
+ const delta = (now - lastTime.current) / 1000;
190
+ lastTime.current = now;
191
+
192
+ const mats = meshData.current.mats;
193
+ if (!mats.length) return;
194
+
195
+ for (let i = 0; i < animData.length && i < mats.length; i++) {
196
+ const d = animData[i];
197
+ d.animOffset += delta * d.animSpeed;
198
+ const sinValue = Math.abs(Math.sin(d.animOffset));
199
+ const color = lerpHSL(d.c0, d.c1, sinValue);
200
+ const mat = mats[i];
201
+ mat.diffuseColor.copyFrom(color);
202
+ if (d.emissiveIntensity > 0) {
203
+ const emissive = color.scale(d.emissiveIntensity);
204
+ mat.emissiveColor = emissive;
205
+ } else {
206
+ mat.emissiveColor = Color3.Black();
207
+ }
208
+ }
209
+ });
210
+
211
+ return () => { scene.onBeforeRenderObservable.remove(observer); };
212
+ }, [scene, animData]);
213
+
214
+ return null;
215
+ });
216
+ Buildings.displayName = "Buildings";
217
+
218
+ function Lights() {
219
+ const { scene } = useBabylonMap();
220
+ useEffect(() => {
221
+ if (!scene) return;
222
+ const hemi = new HemisphericLight("ambient", new Vector3(0, 1, 0), scene);
223
+ hemi.intensity = Math.PI;
224
+ const dir = new DirectionalLight("dir", new Vector3(-2.5, -50, -5).normalize(), scene);
225
+ dir.intensity = Math.PI;
226
+ return () => { hemi.dispose(); dir.dispose(); };
227
+ }, [scene]);
228
+ return null;
229
+ }
230
+
231
+ function Bloom({ enabled, intensity, threshold, levels }: {
232
+ enabled: boolean; intensity: number; threshold: number; levels: number;
233
+ }) {
234
+ const { scene } = useBabylonMap();
235
+ useEffect(() => {
236
+ if (!scene || !enabled) return;
237
+ const pipeline = new DefaultRenderingPipeline(
238
+ "bloomPipeline", true, scene,
239
+ scene.activeCamera ? [scene.activeCamera!] : undefined
240
+ );
241
+ pipeline.bloomEnabled = true;
242
+ pipeline.bloomWeight = intensity;
243
+ pipeline.bloomThreshold = threshold;
244
+ pipeline.bloomKernel = 64;
245
+ pipeline.bloomScale = levels > 0 ? levels / 5 : 0.5;
246
+ return () => { pipeline.dispose(); };
247
+ }, [scene, enabled, intensity, threshold, levels]);
248
+ return null;
249
+ }
250
+
251
+ export default { title: "Buildings 3D" };
252
+
253
+ export function Default() {
254
+ const { bloom, levels, intensity, threshold, smoothing } = useControls("bloom", {
255
+ bloom: { value: true },
256
+ levels: { value: 3, min: 0, max: 10, step: 0.01 },
257
+ intensity: { value: 1.62, min: 0, max: 2, step: 0.01 },
258
+ threshold: { value: 0.1, min: 0, max: 2, step: 0.01, label: "threshold" },
259
+ smoothing: { value: 2, min: 0, max: 5, step: 0.01, label: "smoothing" },
260
+ });
261
+
262
+ // Default to overlay mode
263
+ useEffect(() => {
264
+ const overlay = levaStore.get("overlay");
265
+ levaStore.setValueAtPath("overlay", true, true);
266
+ return () => {
267
+ if (overlay) return;
268
+ levaStore.setValueAtPath("overlay", overlay, true);
269
+ };
270
+ }, []);
271
+
272
+ return (
273
+ <StoryMap longitude={coords.longitude} latitude={coords.latitude} zoom={18} pitch={60}>
274
+ <Lights />
275
+ <AdaptiveDpr />
276
+ <Bloom enabled={bloom} intensity={intensity} threshold={threshold} levels={levels} />
277
+ <Buildings buildingsCenter={coords} origin={coords} />
278
+ </StoryMap>
279
+ );
280
+ }
@@ -0,0 +1,113 @@
1
+ import { FC, memo, useEffect, useRef, useState } from "react";
2
+ import { MeshBuilder, StandardMaterial, Vector3, Color3, HemisphericLight } from '@babylonjs/core';
3
+ import { Canvas } from 'react-babylon-map';
4
+ import MapboxGl from "mapbox-gl";
5
+ import 'mapbox-gl/dist/mapbox-gl.css';
6
+ import Map from 'react-map-gl/mapbox';
7
+ import { useControls } from "leva";
8
+ import { useBabylonMap } from 'react-babylon-map';
9
+
10
+ function Lights() {
11
+ const { scene } = useBabylonMap();
12
+ useEffect(() => {
13
+ if (!scene) return;
14
+ const light = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), scene);
15
+ light.intensity = 1;
16
+ const dirLight = new HemisphericLight("dirLight", new Vector3(1, 4.5, 3), scene);
17
+ dirLight.intensity = Math.PI;
18
+ dirLight.diffuse = Color3.FromHexString("#60666C");
19
+ return () => { light.dispose(); dirLight.dispose(); };
20
+ }, [scene]);
21
+ return null;
22
+ }
23
+
24
+ Lights.displayName = 'Lights';
25
+
26
+ const BabylonBox: FC<{ scale?: number; position?: [number, number, number] }> =
27
+ memo(({ scale = 500, position }) => {
28
+ const { scene } = useBabylonMap();
29
+ const boxRef = useRef<any>(null);
30
+ const matRef = useRef<any>(null);
31
+ const [hovered, setHovered] = useState(false);
32
+ const [clicked, setClicked] = useState(false);
33
+
34
+ useEffect(() => {
35
+ if (!scene) return;
36
+ const box = MeshBuilder.CreateBox("box", { size: scale }, scene);
37
+ box.position.set(
38
+ position?.[0] || 0,
39
+ position?.[1] || 0,
40
+ position?.[2] || 0,
41
+ );
42
+ boxRef.current = box;
43
+ const mat = new StandardMaterial("boxMat", scene);
44
+ mat.diffuseColor = Color3.FromHexString("#ffa500");
45
+ matRef.current = mat;
46
+ box.material = mat;
47
+ return () => { box.dispose(); mat.dispose(); };
48
+ }, [scene, scale, position]);
49
+
50
+ useEffect(() => {
51
+ if (!scene || !boxRef.current) return;
52
+ const observer = scene.onBeforeRenderObservable.add(() => {
53
+ const dt = scene.getEngine().getDeltaTime() / 1000;
54
+ boxRef.current.rotation.x += dt;
55
+ boxRef.current.rotation.z -= dt;
56
+ });
57
+ return () => scene.onBeforeRenderObservable.remove(observer);
58
+ }, [scene]);
59
+
60
+ useEffect(() => {
61
+ if (!matRef.current) return;
62
+ matRef.current.diffuseColor = hovered
63
+ ? Color3.FromHexString("#ff69b4")
64
+ : Color3.FromHexString("#ffa500");
65
+ }, [hovered]);
66
+
67
+ useEffect(() => {
68
+ if (!boxRef.current) return;
69
+ boxRef.current.scaling.setAll(clicked ? 1.5 : 1);
70
+ }, [clicked]);
71
+
72
+ return null;
73
+ });
74
+ BabylonBox.displayName = 'BabylonBox';
75
+
76
+ export default { title: 'Canvas' };
77
+
78
+ export function Mapbox() {
79
+ const { mapboxToken } = useControls({
80
+ mapboxToken: {
81
+ value: import.meta.env.VITE_MAPBOX_TOKEN || '',
82
+ label: 'mapbox token',
83
+ }
84
+ })
85
+
86
+ MapboxGl.accessToken = mapboxToken;
87
+
88
+ return (
89
+ <div style={{ height: '100vh' }}>
90
+ {!mapboxToken && <Center>Add a mapbox token to load this component</Center>}
91
+ {!!mapboxToken && (
92
+ <Map
93
+ antialias
94
+ initialViewState={{ latitude: 51, longitude: 0, zoom: 13, pitch: 60 }}
95
+ mapStyle="mapbox://styles/mapbox/streets-v12"
96
+ mapboxAccessToken={mapboxToken}
97
+ >
98
+ <Canvas latitude={51} longitude={0}>
99
+ <Lights />
100
+ <BabylonBox scale={500} position={[-600, 250, 0]} />
101
+ <BabylonBox scale={500} position={[600, 250, 0]} />
102
+ </Canvas>
103
+ </Map>
104
+ )}
105
+ </div>
106
+ );
107
+ }
108
+
109
+ const Center: FC<{ children: React.ReactNode }> = ({ children }) => (
110
+ <div style={{
111
+ display: 'flex', height: '100%', width: '100%', alignItems: 'center', justifyContent: 'center',
112
+ }}>{children}</div>
113
+ );
@@ -0,0 +1,93 @@
1
+ import { FC, memo, useEffect, useRef, useState } from "react";
2
+ import { HemisphericLight, MeshBuilder, StandardMaterial, Vector3, Color3 } from '@babylonjs/core';
3
+ import { Canvas } from 'react-babylon-map/maplibre';
4
+ import 'maplibre-gl/dist/maplibre-gl.css';
5
+ import Map from 'react-map-gl/maplibre';
6
+ import { Leva } from "leva";
7
+
8
+ import { useBabylonMap } from "react-babylon-map/maplibre";
9
+
10
+ function Lights() {
11
+ const { scene } = useBabylonMap();
12
+ useEffect(() => {
13
+ if (!scene) return;
14
+ const light = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), scene);
15
+ light.intensity = 1;
16
+ const dirLight = new HemisphericLight("dirLight", new Vector3(1, 4.5, 3), scene);
17
+ dirLight.intensity = Math.PI;
18
+ dirLight.diffuse = Color3.FromHexString("#60666C");
19
+ return () => { light.dispose(); dirLight.dispose(); };
20
+ }, [scene]);
21
+ return null;
22
+ }
23
+
24
+ Lights.displayName = 'Lights';
25
+
26
+ const BabylonBox: FC<{ scale?: number; position?: [number, number, number]; hovered?: boolean }> =
27
+ memo(({ scale = 500, position, hovered }) => {
28
+ const { scene } = useBabylonMap();
29
+ const boxRef = useRef<any>(null);
30
+ const matRef = useRef<any>(null);
31
+
32
+ useEffect(() => {
33
+ if (!scene) return;
34
+ const box = MeshBuilder.CreateBox("box", { size: scale }, scene);
35
+ box.position.set(
36
+ position?.[0] || 0,
37
+ position?.[1] || 0,
38
+ position?.[2] || 0,
39
+ );
40
+ boxRef.current = box;
41
+
42
+ const mat = new StandardMaterial("boxMat", scene);
43
+ mat.diffuseColor = Color3.FromHexString("#ffa500");
44
+ matRef.current = mat;
45
+ box.material = mat;
46
+
47
+ return () => { box.dispose(); mat.dispose(); };
48
+ }, [scene, scale, position]);
49
+
50
+ // Rotation
51
+ useEffect(() => {
52
+ if (!scene || !boxRef.current) return;
53
+ const observer = scene.onBeforeRenderObservable.add(() => {
54
+ const dt = scene.getEngine().getDeltaTime() / 1000;
55
+ boxRef.current.rotation.x += dt;
56
+ boxRef.current.rotation.z -= dt;
57
+ });
58
+ return () => scene.onBeforeRenderObservable.remove(observer);
59
+ }, [scene]);
60
+
61
+ // Hover color
62
+ useEffect(() => {
63
+ if (!matRef.current) return;
64
+ matRef.current.diffuseColor = hovered
65
+ ? Color3.FromHexString("#ff69b4")
66
+ : Color3.FromHexString("#ffa500");
67
+ }, [hovered]);
68
+
69
+ return null;
70
+ });
71
+ BabylonBox.displayName = 'BabylonBox';
72
+
73
+ export default { title: 'Canvas' };
74
+
75
+ export function Maplibre() {
76
+ return (<>
77
+ <Leva theme={{ sizes: { rootWidth: '340px', controlWidth: '150px' } }} />
78
+ <div style={{ height: '100vh' }}>
79
+ <Map
80
+ canvasContextAttributes={{ antialias: true }}
81
+ initialViewState={{ latitude: 51, longitude: 0, zoom: 13, pitch: 60 }}
82
+ mapStyle="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
83
+ >
84
+ <Canvas latitude={51} longitude={0}>
85
+ <Lights />
86
+ <BabylonBox scale={500} position={[-600, 250, 0]} />
87
+ <BabylonBox scale={500} position={[600, 250, 0]} />
88
+ </Canvas>
89
+ </Map>
90
+ </div>
91
+ </>
92
+ )
93
+ }