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,276 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
+ import {
3
+ Mesh, StandardMaterial, Vector3, Color3,
4
+ HemisphericLight, DirectionalLight, PointLight, ShadowGenerator, TransformNode,
5
+ VertexData, type Scene,
6
+ } from "@babylonjs/core";
7
+ import { button, folder, useControls } from "leva";
8
+ import { useBabylonMap } from "react-babylon-map";
9
+ import { suspend } from "suspend-react";
10
+ import { StoryMap } from "./story-map";
11
+ import { loadIFC } from "./ifc/ifc-to-babylon";
12
+ import type { MeshData } from "@ifc-lite/geometry";
13
+ import modelUrl from "./ifc/model.ifc?url";
14
+
15
+ export default { title: "IFC" };
16
+
17
+ export function Default() {
18
+ const [path, setPath] = useState(modelUrl);
19
+
20
+ const loadIfcClick = useCallback(async () => {
21
+ try {
22
+ setPath(await getLocalFileUrl());
23
+ } catch (error) {
24
+ console.warn(error);
25
+ }
26
+ }, []);
27
+
28
+ useControls({
29
+ "load IFC file": button(() => loadIfcClick()),
30
+ });
31
+
32
+ const { latitude, longitude, position, rotation, scale } = useControls({
33
+ coords: folder({
34
+ latitude: { value: 51.508775, pad: 6 },
35
+ longitude: { value: -0.1261, pad: 6 },
36
+ }),
37
+ position: { value: { x: 0, y: 0.32, z: 0 }, step: 1, pad: 2 },
38
+ rotation: { value: 0, step: 1 },
39
+ scale: 1,
40
+ });
41
+
42
+ return (
43
+ <StoryMap
44
+ latitude={latitude}
45
+ longitude={longitude}
46
+ zoom={20}
47
+ pitch={75}
48
+ bearing={-45}
49
+ >
50
+ <Lights />
51
+ <IfcModel
52
+ path={path}
53
+ position={position}
54
+ rotation={rotation}
55
+ scale={scale}
56
+ />
57
+ </StoryMap>
58
+ );
59
+ }
60
+
61
+ function Lights() {
62
+ const { scene } = useBabylonMap();
63
+ useEffect(() => {
64
+ if (!scene) return;
65
+
66
+ const hemi = new HemisphericLight("ambient", new Vector3(0, 1, 0), scene);
67
+ hemi.intensity = 0.5 * Math.PI;
68
+
69
+ const dir = new DirectionalLight(
70
+ "dir",
71
+ new Vector3(-2.5, -50, -5).normalize(),
72
+ scene
73
+ );
74
+ dir.intensity = 1.5 * Math.PI;
75
+ dir.position = new Vector3(2.5, 50, 5);
76
+
77
+ const shadowGen = new ShadowGenerator(1024, dir);
78
+ shadowGen.useBlurExponentialShadowMap = true;
79
+ shadowGen.blurKernel = 32;
80
+
81
+ const p1 = new PointLight("point1", new Vector3(-10, 0, -20), scene);
82
+ p1.intensity = Math.PI;
83
+ const p2 = new PointLight("point2", new Vector3(0, -10, 0), scene);
84
+ p2.intensity = Math.PI;
85
+
86
+ return () => {
87
+ shadowGen.dispose();
88
+ hemi.dispose();
89
+ dir.dispose();
90
+ p1.dispose();
91
+ p2.dispose();
92
+ };
93
+ }, [scene]);
94
+ return null;
95
+ }
96
+
97
+ function IfcModel({
98
+ path,
99
+ position,
100
+ rotation,
101
+ scale,
102
+ }: {
103
+ path: string;
104
+ position: { x: number; y: number; z: number };
105
+ rotation: number;
106
+ scale: number;
107
+ }) {
108
+ const { scene } = useBabylonMap();
109
+ const parentRef = useRef<TransformNode | null>(null);
110
+
111
+ const meshDataArray = suspend(() => loadIFC(path), [path]);
112
+
113
+ useEffect(() => {
114
+ if (!scene || meshDataArray.length === 0) return;
115
+
116
+ const parent = new TransformNode("ifcParent", scene);
117
+ parent.rotation.x = -Math.PI / 2;
118
+ parent.position.set(position.x, position.y, position.z);
119
+ parent.scaling.setAll(scale);
120
+ parentRef.current = parent;
121
+
122
+ const createdMeshes: Mesh[] = [];
123
+ const createdMats: StandardMaterial[] = [];
124
+
125
+ // Separate opaque / transparent (matching official ifc2bb.ts)
126
+ const valid = meshDataArray.filter(m => m.positions.length > 0);
127
+ const opaque = valid.filter(m => m.color[3] >= 1);
128
+ const transparent = valid.filter(m => m.color[3] < 1);
129
+
130
+ if (opaque.length > 0) {
131
+ const mesh = buildBatchMesh(opaque, scene, false);
132
+ mesh.parent = parent;
133
+ createdMeshes.push(mesh);
134
+ createdMats.push(mesh.material as StandardMaterial);
135
+ }
136
+
137
+ if (transparent.length > 0) {
138
+ const alphaGroups = new Map<number, MeshData[]>();
139
+ for (const m of transparent) {
140
+ const a = Math.round(m.color[3] * 100) / 100;
141
+ let bucket = alphaGroups.get(a);
142
+ if (!bucket) { bucket = []; alphaGroups.set(a, bucket); }
143
+ bucket.push(m);
144
+ }
145
+ for (const [, group] of alphaGroups) {
146
+ const mesh = buildBatchMesh(group, scene, true);
147
+ mesh.parent = parent;
148
+ createdMeshes.push(mesh);
149
+ createdMats.push(mesh.material as StandardMaterial);
150
+ }
151
+ }
152
+
153
+ const dir = scene.lights.find(l => l.name === "dir");
154
+ if (dir) {
155
+ const sg = dir.getShadowGenerator();
156
+ if (sg) createdMeshes.forEach(m => sg.addShadowCaster(m));
157
+ }
158
+
159
+ return () => {
160
+ createdMeshes.forEach(m => m.dispose());
161
+ createdMats.forEach(m => m.dispose());
162
+ parent.dispose();
163
+ parentRef.current = null;
164
+ };
165
+ }, [scene, meshDataArray]);
166
+
167
+ useEffect(() => {
168
+ const parent = parentRef.current;
169
+ if (!parent) return;
170
+ parent.position.set(position.x, position.y, position.z);
171
+ parent.rotation.y = (rotation * Math.PI) / 180;
172
+ parent.scaling.setAll(scale);
173
+ }, [position, rotation, scale]);
174
+
175
+ return null;
176
+ }
177
+
178
+ /**
179
+ * Merge MeshData into one Babylon Mesh with per-vertex RGB colors.
180
+ * Y↔Z swap converts IFC Z-up → Babylon Y-up.
181
+ * Winding reversal compensates for the swap's reflection.
182
+ * Matches official ifc2bb.ts for color handling.
183
+ */
184
+ function buildBatchMesh(
185
+ meshes: MeshData[],
186
+ scene: Scene,
187
+ transparent: boolean,
188
+ ): Mesh {
189
+ let totalVerts = 0;
190
+ let totalIdx = 0;
191
+ for (const m of meshes) {
192
+ totalVerts += m.positions.length / 3;
193
+ totalIdx += m.indices.length;
194
+ }
195
+
196
+ const positions = new Float32Array(totalVerts * 3);
197
+ const normals = new Float32Array(totalVerts * 3);
198
+ const colors = new Float32Array(totalVerts * 4);
199
+ const indices = new Uint32Array(totalIdx);
200
+
201
+ let vOff = 0;
202
+ let iOff = 0;
203
+
204
+ for (const m of meshes) {
205
+ const vc = m.positions.length / 3;
206
+
207
+ // Y↔Z swap: IFC Z-up → Babylon Y-up
208
+ for (let i = 0; i < m.positions.length; i += 3) {
209
+ positions[vOff * 3 + i] = m.positions[i];
210
+ positions[vOff * 3 + i + 1] = m.positions[i + 2];
211
+ positions[vOff * 3 + i + 2] = m.positions[i + 1];
212
+ }
213
+ for (let i = 0; i < m.normals.length; i += 3) {
214
+ normals[vOff * 3 + i] = m.normals[i];
215
+ normals[vOff * 3 + i + 1] = m.normals[i + 2];
216
+ normals[vOff * 3 + i + 2] = m.normals[i + 1];
217
+ }
218
+
219
+ // Vertex colors: RGB from mesh color, A=1
220
+ const [r, g, b] = m.color;
221
+ for (let v = 0; v < vc; v++) {
222
+ colors[(vOff + v) * 4 + 0] = r;
223
+ colors[(vOff + v) * 4 + 1] = g;
224
+ colors[(vOff + v) * 4 + 2] = b;
225
+ colors[(vOff + v) * 4 + 3] = 1;
226
+ }
227
+
228
+ // Indices with winding reversal to compensate for Y↔Z reflection
229
+ for (let i = 0; i < m.indices.length; i += 3) {
230
+ indices[iOff + i] = m.indices[i] + vOff;
231
+ indices[iOff + i + 1] = m.indices[i + 2] + vOff;
232
+ indices[iOff + i + 2] = m.indices[i + 1] + vOff;
233
+ }
234
+
235
+ vOff += vc;
236
+ iOff += m.indices.length;
237
+ }
238
+
239
+ const mesh = new Mesh("batched", scene);
240
+ const vd = new VertexData();
241
+ vd.positions = positions;
242
+ vd.normals = normals;
243
+ vd.colors = colors;
244
+ vd.indices = indices;
245
+ vd.applyToMesh(mesh);
246
+
247
+ const material = new StandardMaterial("batched-mat", scene);
248
+ material.diffuseColor = new Color3(1, 1, 1);
249
+ material.specularColor = new Color3(0, 0, 0);
250
+
251
+ if (transparent) {
252
+ material.alpha = meshes[0].color[3];
253
+ material.backFaceCulling = false;
254
+ }
255
+
256
+ mesh.material = material;
257
+ mesh.hasVertexAlpha = false;
258
+
259
+ return mesh;
260
+ }
261
+
262
+ async function getLocalFileUrl(): Promise<string> {
263
+ return new Promise((resolve) => {
264
+ const onChange = (e: Event) => {
265
+ if (!(e.target instanceof HTMLInputElement) || !e.target.files) return;
266
+ const file = e.target.files[0];
267
+ if (!file) return;
268
+ resolve(URL.createObjectURL(file));
269
+ };
270
+ const input = document.createElement("input");
271
+ input.type = "file";
272
+ input.addEventListener("change", onChange);
273
+ input.accept = ".ifc";
274
+ input.click();
275
+ });
276
+ }
@@ -0,0 +1,97 @@
1
+ import { ThemeState, useLadleContext } from '@ladle/react';
2
+ import { useControls } from 'leva';
3
+ import Mapbox from "mapbox-gl";
4
+ import 'mapbox-gl/dist/mapbox-gl.css';
5
+ import { FC, memo } from "react";
6
+ import Map, { Layer } from 'react-map-gl/mapbox';
7
+ import { Canvas } from 'react-babylon-map';
8
+ import { StoryMapProps } from '../story-map';
9
+
10
+ /** `<Map>` styled for stories */
11
+ export const StoryMapbox: FC<Omit<StoryMapProps, 'maplibreChildren'>> = ({
12
+ latitude, longitude, canvas, children, mapChildren, mapboxChildren, ...rest
13
+ }) => {
14
+
15
+ const { mapboxToken } = useControls({
16
+ mapboxToken: {
17
+ value: import.meta.env.VITE_MAPBOX_TOKEN || '',
18
+ label: 'mapbox token',
19
+ }
20
+ })
21
+
22
+ const theme = useLadleContext().globalState.theme;
23
+
24
+ const mapStyle = theme === ThemeState.Dark
25
+ ? "mapbox://styles/mapbox/dark-v11"
26
+ : "mapbox://styles/mapbox/streets-v12";
27
+
28
+ Mapbox.accessToken = mapboxToken;
29
+
30
+ const { showBuildings3D } = useControls({
31
+ showBuildings3D: {
32
+ value: true,
33
+ label: 'show 3D buildings'
34
+ }
35
+ })
36
+
37
+ return <div style={{ height: '100vh', position: 'relative' }}>
38
+ {!mapboxToken && <Center>Add a mapbox token to load this component</Center>}
39
+ {!!mapboxToken && <Map
40
+ antialias
41
+ initialViewState={{ latitude, longitude, ...rest }}
42
+ maxPitch={rest.pitch ? Math.min(rest.pitch, 85) : undefined}
43
+ mapStyle={mapStyle}
44
+ mapboxAccessToken={mapboxToken}
45
+ >
46
+ {mapChildren}
47
+ {mapboxChildren}
48
+ <Canvas latitude={latitude} longitude={longitude} {...canvas}>
49
+ {children}
50
+ </Canvas>
51
+ {showBuildings3D && <Buildings3D />}
52
+ </Map>}
53
+ </div>
54
+ }
55
+
56
+ const Center: FC<{ children: React.ReactNode }> = ({ children }) => (
57
+ <div style={{
58
+ display: 'flex',
59
+ height: '100%',
60
+ width: '100%',
61
+ alignItems: 'center',
62
+ justifyContent: 'center',
63
+ }}>{children}</div>
64
+ )
65
+
66
+ const Buildings3D = memo(() => {
67
+ return <Layer
68
+ id="3d-buildings"
69
+ type="fill-extrusion"
70
+ source="composite"
71
+ source-layer="building"
72
+ minzoom={15}
73
+ filter={['==', 'extrude', 'true']}
74
+ paint={{
75
+ "fill-extrusion-color": "#656565",
76
+ "fill-extrusion-height": [
77
+ "interpolate",
78
+ ["linear"],
79
+ ["zoom"],
80
+ 15,
81
+ 0,
82
+ 15.05,
83
+ ["get", "height"],
84
+ ],
85
+ "fill-extrusion-base": [
86
+ "interpolate",
87
+ ["linear"],
88
+ ["zoom"],
89
+ 15,
90
+ 0,
91
+ 15.05,
92
+ ["get", "min_height"],
93
+ ],
94
+ "fill-extrusion-opacity": 1.0,
95
+ }} />
96
+ })
97
+ Buildings3D.displayName = 'Buildings3D';
@@ -0,0 +1,36 @@
1
+ import { ThemeState, useLadleContext } from '@ladle/react';
2
+ import { useControls } from 'leva';
3
+ import 'maplibre-gl/dist/maplibre-gl.css';
4
+ import { FC, memo } from "react";
5
+ import Map from 'react-map-gl/maplibre';
6
+ import { Canvas } from 'react-babylon-map/maplibre';
7
+ import { StoryMapProps } from '../story-map';
8
+
9
+ /** `<Map>` styled for stories */
10
+ export const StoryMaplibre: FC<Omit<StoryMapProps, 'mapboxChildren'>> = ({
11
+ latitude, longitude, canvas, children, mapChildren, maplibreChildren, ...rest
12
+ }) => {
13
+
14
+ const theme = useLadleContext().globalState.theme;
15
+
16
+ const mapStyle = theme === ThemeState.Dark
17
+ ? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
18
+ : "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json";
19
+
20
+ return <div style={{ height: '100vh', position: 'relative' }}>
21
+ <Map
22
+ canvasContextAttributes={{
23
+ antialias: true,
24
+ }}
25
+ initialViewState={{ latitude, longitude, ...rest }}
26
+ maxPitch={rest.pitch ? Math.min(rest.pitch, 85) : undefined}
27
+ mapStyle={mapStyle}
28
+ >
29
+ {mapChildren}
30
+ {maplibreChildren}
31
+ <Canvas latitude={latitude} longitude={longitude} {...canvas}>
32
+ {children}
33
+ </Canvas>
34
+ </Map>
35
+ </div>
36
+ }
@@ -0,0 +1,115 @@
1
+ import { FC, memo, useEffect, useRef, useState } from "react";
2
+ import { MeshBuilder, StandardMaterial, Vector3, Color3, HemisphericLight } from '@babylonjs/core';
3
+ import { Coordinates, NearCoordinates, useBabylonMap } from 'react-babylon-map';
4
+ import { levaStore, useControls } from 'leva';
5
+ import { StoryMap } from "./story-map";
6
+
7
+ enum CoordinatesType {
8
+ NearCoordinates = 'NearCoordinates',
9
+ Coordinates = 'Coordinates',
10
+ }
11
+
12
+ function Lights() {
13
+ const { scene } = useBabylonMap();
14
+ useEffect(() => {
15
+ if (!scene) return;
16
+ const light = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), scene);
17
+ light.intensity = 1;
18
+ return () => { light.dispose(); };
19
+ }, [scene]);
20
+ return null;
21
+ }
22
+
23
+ const MyBox: FC<{ color: string; scale: number; position?: [number, number, number] }> = memo(({ color, scale, position }) => {
24
+ const { scene } = useBabylonMap();
25
+ const [hovered, setHovered] = useState(false);
26
+ const boxRef = useRef<any>(null);
27
+ const matRef = useRef<any>(null);
28
+
29
+ const s = scale * (hovered ? 1.5 : 1);
30
+ const height = 7;
31
+
32
+ useEffect(() => {
33
+ if (!scene) return;
34
+ const box = MeshBuilder.CreateBox("myBox", { width: 1, height, depth: 1 }, scene);
35
+ box.position.y = s * height * 0.5;
36
+ box.scaling.setAll(s, s, s);
37
+ if (position) {
38
+ box.position.x += position[0];
39
+ box.position.y += position[1];
40
+ box.position.z += position[2];
41
+ }
42
+ boxRef.current = box;
43
+ const mat = new StandardMaterial(`boxMat-${color}`, scene);
44
+ mat.diffuseColor = color === 'blue'
45
+ ? Color3.FromHexString("#0000FF")
46
+ : color === 'green'
47
+ ? Color3.FromHexString("#008000")
48
+ : Color3.FromHexString("#800080");
49
+ matRef.current = mat;
50
+ box.material = mat;
51
+ box.metadata = {
52
+ onPointerOver: () => setHovered(true),
53
+ onPointerOut: () => setHovered(false),
54
+ };
55
+ return () => { box.dispose(); mat.dispose(); };
56
+ }, [scene, color, s, position]);
57
+
58
+ return null;
59
+ });
60
+ MyBox.displayName = 'MyBox';
61
+
62
+ const CoordsControl: FC<{ longitude: number; latitude: number; children: React.ReactNode }> = (props) => {
63
+ const { coords } = useControls({
64
+ coords: { value: CoordinatesType.Coordinates, options: CoordinatesType }
65
+ });
66
+ return (
67
+ <>
68
+ {coords === CoordinatesType.Coordinates && <Coordinates {...props}>{props.children}</Coordinates>}
69
+ {coords === CoordinatesType.NearCoordinates && <NearCoordinates {...props}>{props.children}</NearCoordinates>}
70
+ </>
71
+ );
72
+ };
73
+
74
+ export default { title: 'Multi Coordinates' };
75
+
76
+ export function Default() {
77
+ const { blue, green, purple, scale } = useControls({
78
+ scale: 1,
79
+ blue: {
80
+ value: [-0.1261, 51.508775],
81
+ pad: 6,
82
+ step: 0.000001,
83
+ },
84
+ green: {
85
+ value: [-0.1261, 51.508775],
86
+ pad: 6,
87
+ step: 0.000001,
88
+ },
89
+ purple: {
90
+ value: [-0.1261, 51.508756],
91
+ pad: 6,
92
+ step: 0.000001,
93
+ },
94
+ });
95
+
96
+ useEffect(() => { levaStore.setValueAtPath('overlay', false, true); }, []);
97
+
98
+ return (
99
+ <StoryMap
100
+ longitude={blue[0]}
101
+ latitude={blue[1]}
102
+ zoom={20}
103
+ pitch={60}
104
+ >
105
+ <Lights />
106
+ <MyBox scale={scale} color="blue" position={[2, 0, 0]} />
107
+ <CoordsControl longitude={green[0]} latitude={green[1]}>
108
+ <MyBox scale={scale} color="green" position={[-2, 0, 0]} />
109
+ </CoordsControl>
110
+ <CoordsControl longitude={purple[0]} latitude={purple[1]}>
111
+ <MyBox scale={scale} color="purple" />
112
+ </CoordsControl>
113
+ </StoryMap>
114
+ );
115
+ }
@@ -0,0 +1,148 @@
1
+ import { FC, memo, useEffect, useMemo, useRef, useState } from "react";
2
+ import {
3
+ MeshBuilder, StandardMaterial, Vector3, Color3, AbstractMesh,
4
+ HemisphericLight, GizmoManager,
5
+ } from '@babylonjs/core';
6
+ import { useBabylonMap, vector3ToCoords } from 'react-babylon-map';
7
+ import { useControls } from 'leva';
8
+ import { StoryMap } from "./story-map";
9
+ import { Marker as MaplibreMarker } from 'react-map-gl/maplibre';
10
+ import { Marker as MapboxMarker } from 'react-map-gl/mapbox';
11
+
12
+ function Lights() {
13
+ const { scene } = useBabylonMap();
14
+ useEffect(() => {
15
+ if (!scene) return;
16
+ const light = new HemisphericLight("hemiLight", new Vector3(0, 1, 0), scene);
17
+ light.intensity = 1;
18
+ return () => { light.dispose(); };
19
+ }, [scene]);
20
+ return null;
21
+ }
22
+
23
+ /** Sphere mesh */
24
+ const MySphere: FC<{ position: [number, number, number] }> = memo(({ position }) => {
25
+ const { scene } = useBabylonMap();
26
+ useEffect(() => {
27
+ if (!scene) return;
28
+ const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 100 }, scene);
29
+ sphere.position.set(position[0], position[1], position[2]);
30
+ const mat = new StandardMaterial('sphereMat', scene);
31
+ mat.diffuseColor = Color3.FromHexString("#FFA500");
32
+ mat.emissiveColor = Color3.FromHexString("#FFA500").scale(0.2);
33
+ sphere.material = mat;
34
+ return () => { sphere.dispose(); mat.dispose(); };
35
+ }, [scene, position]);
36
+ return null;
37
+ });
38
+ MySphere.displayName = 'MySphere';
39
+
40
+ /** Drag gizmo using Babylon's built-in GizmoManager */
41
+ const DragGizmo: FC<{
42
+ position: [number, number, number];
43
+ onDrag: (newPosition: [number, number, number]) => void;
44
+ }> = memo(({ position, onDrag }) => {
45
+ const { scene, map } = useBabylonMap();
46
+ const anchorRef = useRef<AbstractMesh | null>(null);
47
+
48
+ useEffect(() => {
49
+ if (!scene || !map) return;
50
+
51
+ // Create an invisible anchor mesh at the position
52
+ const anchor = MeshBuilder.CreateBox('dragAnchor', { size: 0.01 }, scene);
53
+ anchor.position.set(position[0], position[1], position[2]);
54
+ anchor.visibility = 0;
55
+ anchorRef.current = anchor;
56
+
57
+ // Setup gizmo manager — only translation on X and Z axes
58
+ const gizmoManager = new GizmoManager(scene);
59
+ gizmoManager.positionGizmoEnabled = true;
60
+ gizmoManager.rotationGizmoEnabled = false;
61
+ gizmoManager.scaleGizmoEnabled = false;
62
+ gizmoManager.boundingBoxGizmoEnabled = false;
63
+
64
+ // Disable Y-axis drag (keep on ground plane)
65
+ if (gizmoManager.gizmos.positionGizmo) {
66
+ gizmoManager.gizmos.positionGizmo.yGizmo.dispose();
67
+ }
68
+
69
+ // Attach to our anchor
70
+ gizmoManager.attachToMesh(anchor);
71
+
72
+ // Disable map panning during drag
73
+ let wasDragPanEnabled = true;
74
+ let wasDragRotateEnabled = true;
75
+
76
+ gizmoManager.onAttachedToMeshObservable.add(() => {
77
+ wasDragPanEnabled = map.dragPan.isEnabled();
78
+ wasDragRotateEnabled = map.dragRotate.isEnabled();
79
+ });
80
+
81
+ // Track position changes
82
+ const observer = scene.onBeforeRenderObservable.add(() => {
83
+ if (!anchorRef.current) return;
84
+ const pos = anchorRef.current.position;
85
+ onDrag([pos.x, pos.y, pos.z]);
86
+ });
87
+
88
+ return () => {
89
+ scene.onBeforeRenderObservable.remove(observer);
90
+ gizmoManager.dispose();
91
+ anchor.dispose();
92
+ };
93
+ }, [scene, map, onDrag]);
94
+
95
+ // Update anchor position when prop changes
96
+ useEffect(() => {
97
+ if (anchorRef.current) {
98
+ anchorRef.current.position.set(position[0], position[1], position[2]);
99
+ }
100
+ }, [position]);
101
+
102
+ return null;
103
+ });
104
+ DragGizmo.displayName = 'DragGizmo';
105
+
106
+ export default { title: 'Pivot Controls' };
107
+
108
+ export function Default() {
109
+ const origin = useControls({
110
+ latitude: { value: 51, min: -90, max: 90 },
111
+ longitude: { value: 0, min: -180, max: 180 },
112
+ });
113
+
114
+ const [position, setPosition] = useState<[number, number, number]>([0, 25, 0]);
115
+ const geoPos = useMemo(() => vector3ToCoords(position, origin), [position, origin]);
116
+
117
+ // Reset on origin change
118
+ useEffect(() => setPosition([0, 25, 0]), [origin.latitude, origin.longitude]);
119
+
120
+ return (
121
+ <StoryMap
122
+ longitude={origin.longitude}
123
+ latitude={origin.latitude}
124
+ zoom={13}
125
+ pitch={60}
126
+ maplibreChildren={
127
+ <MaplibreMarker longitude={geoPos.longitude} latitude={geoPos.latitude}>
128
+ <div style={{ fontSize: 18 }}>
129
+ lat: {geoPos.latitude.toFixed(6)}<br />
130
+ lon: {geoPos.longitude.toFixed(6)}
131
+ </div>
132
+ </MaplibreMarker>
133
+ }
134
+ mapboxChildren={
135
+ <MapboxMarker longitude={geoPos.longitude} latitude={geoPos.latitude}>
136
+ <div style={{ fontSize: 18 }}>
137
+ lat: {geoPos.latitude.toFixed(6)}<br />
138
+ lon: {geoPos.longitude.toFixed(6)}
139
+ </div>
140
+ </MapboxMarker>
141
+ }
142
+ >
143
+ <Lights />
144
+ <MySphere position={position} />
145
+ <DragGizmo position={position} onDrag={setPosition} />
146
+ </StoryMap>
147
+ );
148
+ }