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.
- package/.claude/settings.local.json +78 -0
- package/demo.html +161 -0
- package/dist/cjs/main.js +520 -0
- package/dist/es/main.mjs +20 -0
- package/dist/es/main.mjs.map +1 -0
- package/dist/es/main10.mjs +33 -0
- package/dist/es/main10.mjs.map +1 -0
- package/dist/es/main11.mjs +12 -0
- package/dist/es/main11.mjs.map +1 -0
- package/dist/es/main12.mjs +14 -0
- package/dist/es/main12.mjs.map +1 -0
- package/dist/es/main13.mjs +12 -0
- package/dist/es/main13.mjs.map +1 -0
- package/dist/es/main14.mjs +5 -0
- package/dist/es/main14.mjs.map +1 -0
- package/dist/es/main15.mjs +12 -0
- package/dist/es/main15.mjs.map +1 -0
- package/dist/es/main16.mjs +25 -0
- package/dist/es/main16.mjs.map +1 -0
- package/dist/es/main17.mjs +54 -0
- package/dist/es/main17.mjs.map +1 -0
- package/dist/es/main18.mjs +88 -0
- package/dist/es/main18.mjs.map +1 -0
- package/dist/es/main19.mjs +18 -0
- package/dist/es/main19.mjs.map +1 -0
- package/dist/es/main2.mjs +9 -0
- package/dist/es/main2.mjs.map +1 -0
- package/dist/es/main20.mjs +21 -0
- package/dist/es/main20.mjs.map +1 -0
- package/dist/es/main21.mjs +61 -0
- package/dist/es/main21.mjs.map +1 -0
- package/dist/es/main3.mjs +46 -0
- package/dist/es/main3.mjs.map +1 -0
- package/dist/es/main4.mjs +23 -0
- package/dist/es/main4.mjs.map +1 -0
- package/dist/es/main5.mjs +69 -0
- package/dist/es/main5.mjs.map +1 -0
- package/dist/es/main6.mjs +35 -0
- package/dist/es/main6.mjs.map +1 -0
- package/dist/es/main7.mjs +65 -0
- package/dist/es/main7.mjs.map +1 -0
- package/dist/es/main8.mjs +14 -0
- package/dist/es/main8.mjs.map +1 -0
- package/dist/es/main9.mjs +26 -0
- package/dist/es/main9.mjs.map +1 -0
- package/dist/maplibre/cjs/main.js +520 -0
- package/dist/maplibre/es/main.mjs +20 -0
- package/dist/maplibre/es/main.mjs.map +1 -0
- package/dist/maplibre/es/main10.mjs +33 -0
- package/dist/maplibre/es/main10.mjs.map +1 -0
- package/dist/maplibre/es/main11.mjs +12 -0
- package/dist/maplibre/es/main11.mjs.map +1 -0
- package/dist/maplibre/es/main12.mjs +14 -0
- package/dist/maplibre/es/main12.mjs.map +1 -0
- package/dist/maplibre/es/main13.mjs +12 -0
- package/dist/maplibre/es/main13.mjs.map +1 -0
- package/dist/maplibre/es/main14.mjs +5 -0
- package/dist/maplibre/es/main14.mjs.map +1 -0
- package/dist/maplibre/es/main15.mjs +12 -0
- package/dist/maplibre/es/main15.mjs.map +1 -0
- package/dist/maplibre/es/main16.mjs +25 -0
- package/dist/maplibre/es/main16.mjs.map +1 -0
- package/dist/maplibre/es/main17.mjs +54 -0
- package/dist/maplibre/es/main17.mjs.map +1 -0
- package/dist/maplibre/es/main18.mjs +88 -0
- package/dist/maplibre/es/main18.mjs.map +1 -0
- package/dist/maplibre/es/main19.mjs +18 -0
- package/dist/maplibre/es/main19.mjs.map +1 -0
- package/dist/maplibre/es/main2.mjs +9 -0
- package/dist/maplibre/es/main2.mjs.map +1 -0
- package/dist/maplibre/es/main20.mjs +61 -0
- package/dist/maplibre/es/main20.mjs.map +1 -0
- package/dist/maplibre/es/main21.mjs +21 -0
- package/dist/maplibre/es/main21.mjs.map +1 -0
- package/dist/maplibre/es/main3.mjs +46 -0
- package/dist/maplibre/es/main3.mjs.map +1 -0
- package/dist/maplibre/es/main4.mjs +23 -0
- package/dist/maplibre/es/main4.mjs.map +1 -0
- package/dist/maplibre/es/main5.mjs +69 -0
- package/dist/maplibre/es/main5.mjs.map +1 -0
- package/dist/maplibre/es/main6.mjs +35 -0
- package/dist/maplibre/es/main6.mjs.map +1 -0
- package/dist/maplibre/es/main7.mjs +65 -0
- package/dist/maplibre/es/main7.mjs.map +1 -0
- package/dist/maplibre/es/main8.mjs +14 -0
- package/dist/maplibre/es/main8.mjs.map +1 -0
- package/dist/maplibre/es/main9.mjs +26 -0
- package/dist/maplibre/es/main9.mjs.map +1 -0
- package/dist/maplibre/types/api/canvas-props.d.ts +9 -0
- package/dist/maplibre/types/api/coordinates.d.ts +13 -0
- package/dist/maplibre/types/api/coords-to-vector-3.d.ts +3 -0
- package/dist/maplibre/types/api/coords.d.ts +5 -0
- package/dist/maplibre/types/api/index.d.ts +7 -0
- package/dist/maplibre/types/api/near-coordinates.d.ts +13 -0
- package/dist/maplibre/types/api/use-map.d.ts +3 -0
- package/dist/maplibre/types/api/vector-3-to-coords.d.ts +2 -0
- package/dist/maplibre/types/core/canvas-in-layer/use-canvas-in-layer.d.ts +15 -0
- package/dist/maplibre/types/core/canvas-in-layer/use-render.d.ts +15 -0
- package/dist/maplibre/types/core/canvas-in-layer/use-root.d.ts +11 -0
- package/dist/maplibre/types/core/canvas-overlay/canvas-portal.d.ts +10 -0
- package/dist/maplibre/types/core/canvas-overlay/init-canvas-fc.d.ts +11 -0
- package/dist/maplibre/types/core/canvas-overlay/render.d.ts +1 -0
- package/dist/maplibre/types/core/canvas-overlay/sync-camera-fc.d.ts +12 -0
- package/dist/maplibre/types/core/coords-to-matrix.d.ts +9 -0
- package/dist/maplibre/types/core/earth-radius.d.ts +1 -0
- package/dist/maplibre/types/core/generic-map.d.ts +49 -0
- package/dist/maplibre/types/core/matrix-utils.d.ts +7 -0
- package/dist/maplibre/types/core/sync-camera.d.ts +7 -0
- package/dist/maplibre/types/core/use-babylon-map.d.ts +32 -0
- package/dist/maplibre/types/core/use-coords-to-matrix.d.ts +6 -0
- package/dist/maplibre/types/core/use-coords.d.ts +5 -0
- package/dist/maplibre/types/core/use-function.d.ts +1 -0
- package/dist/maplibre/types/maplibre/canvas.d.ts +4 -0
- package/dist/maplibre/types/maplibre.index.d.ts +4 -0
- package/dist/types/api/canvas-props.d.ts +9 -0
- package/dist/types/api/coordinates.d.ts +13 -0
- package/dist/types/api/coords-to-vector-3.d.ts +3 -0
- package/dist/types/api/coords.d.ts +5 -0
- package/dist/types/api/index.d.ts +7 -0
- package/dist/types/api/near-coordinates.d.ts +13 -0
- package/dist/types/api/use-map.d.ts +3 -0
- package/dist/types/api/vector-3-to-coords.d.ts +2 -0
- package/dist/types/core/canvas-in-layer/use-canvas-in-layer.d.ts +15 -0
- package/dist/types/core/canvas-in-layer/use-render.d.ts +15 -0
- package/dist/types/core/canvas-in-layer/use-root.d.ts +11 -0
- package/dist/types/core/canvas-overlay/canvas-portal.d.ts +10 -0
- package/dist/types/core/canvas-overlay/init-canvas-fc.d.ts +11 -0
- package/dist/types/core/canvas-overlay/render.d.ts +1 -0
- package/dist/types/core/canvas-overlay/sync-camera-fc.d.ts +12 -0
- package/dist/types/core/coords-to-matrix.d.ts +9 -0
- package/dist/types/core/earth-radius.d.ts +1 -0
- package/dist/types/core/generic-map.d.ts +49 -0
- package/dist/types/core/matrix-utils.d.ts +7 -0
- package/dist/types/core/sync-camera.d.ts +7 -0
- package/dist/types/core/use-babylon-map.d.ts +32 -0
- package/dist/types/core/use-coords-to-matrix.d.ts +6 -0
- package/dist/types/core/use-coords.d.ts +5 -0
- package/dist/types/core/use-function.d.ts +1 -0
- package/dist/types/mapbox/canvas.d.ts +4 -0
- package/dist/types/mapbox.index.d.ts +4 -0
- package/package.json +58 -0
- package/plan.md +719 -0
- package/src/api/canvas-props.ts +10 -0
- package/src/api/coordinates.tsx +83 -0
- package/src/api/coords-to-vector-3.ts +39 -0
- package/src/api/coords.tsx +6 -0
- package/src/api/index.ts +7 -0
- package/src/api/near-coordinates.tsx +87 -0
- package/src/api/use-map.ts +8 -0
- package/src/api/vector-3-to-coords.ts +13 -0
- package/src/core/canvas-in-layer/use-canvas-in-layer.tsx +27 -0
- package/src/core/canvas-in-layer/use-render.ts +43 -0
- package/src/core/canvas-in-layer/use-root.tsx +82 -0
- package/src/core/canvas-overlay/canvas-portal.tsx +98 -0
- package/src/core/canvas-overlay/init-canvas-fc.tsx +45 -0
- package/src/core/canvas-overlay/render.tsx +1 -0
- package/src/core/canvas-overlay/sync-camera-fc.tsx +83 -0
- package/src/core/coords-to-matrix.ts +21 -0
- package/src/core/earth-radius.ts +1 -0
- package/src/core/events.ts +55 -0
- package/src/core/generic-map.ts +59 -0
- package/src/core/map-engine.tsx +70 -0
- package/src/core/matrix-utils.ts +22 -0
- package/src/core/sync-camera.ts +29 -0
- package/src/core/use-babylon-map.ts +46 -0
- package/src/core/use-coords-to-matrix.ts +13 -0
- package/src/core/use-coords.tsx +22 -0
- package/src/core/use-function.ts +10 -0
- package/src/mapbox/canvas.tsx +59 -0
- package/src/mapbox.index.ts +7 -0
- package/src/maplibre/canvas.tsx +59 -0
- package/src/maplibre.index.ts +7 -0
- package/src/vite-env.d.ts +1 -0
- package/stories/.ladle/components.tsx +50 -0
- package/stories/.ladle/style.css +63 -0
- package/stories/package.json +31 -0
- package/stories/pnpm-lock.yaml +5450 -0
- package/stories/sandbox.config.json +3 -0
- package/stories/src/adaptive-dpr.tsx +34 -0
- package/stories/src/billboard.stories.tsx +111 -0
- package/stories/src/buildings-3d.stories.tsx +280 -0
- package/stories/src/canvas/mapbox.stories.tsx +113 -0
- package/stories/src/canvas/maplibre.stories.tsx +93 -0
- package/stories/src/comparison.stories.tsx +161 -0
- package/stories/src/extrude/chaillot.ts +8 -0
- package/stories/src/exude-coordinates.stories.tsx +139 -0
- package/stories/src/free-3d-buildings/get-buildings-data.ts +49 -0
- package/stories/src/html-on-top.stories.tsx +156 -0
- package/stories/src/ifc/ifc-to-babylon.ts +97 -0
- package/stories/src/ifc/ifc.main.ts +904 -0
- package/stories/src/ifc/ifc2bb.ts +343 -0
- package/stories/src/ifc/model.ifc +14155 -0
- package/stories/src/ifc.stories.tsx +276 -0
- package/stories/src/mapbox/story-mapbox.tsx +97 -0
- package/stories/src/maplibre/story-maplibre.tsx +36 -0
- package/stories/src/multi-coordinates.stories.tsx +115 -0
- package/stories/src/pivot-controls.stories.tsx +148 -0
- package/stories/src/postprocessing.stories.tsx +125 -0
- package/stories/src/render-on-demand.stories.tsx +76 -0
- package/stories/src/story-map.tsx +44 -0
- package/stories/src/sunlight.stories.tsx +215 -0
- package/stories/src/vite-env.d.ts +1 -0
- package/stories/tsconfig.json +32 -0
- package/stories/tsconfig.node.json +10 -0
- package/stories/vite.config.ts +27 -0
- package/tsconfig.json +31 -0
- package/tsconfig.mapbox.json +7 -0
- package/tsconfig.maplibre.json +7 -0
- package/tsconfig.node.json +10 -0
- package/tsconfig.types.json +25 -0
- package/vite.config.ts +65 -0
|
@@ -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
|
+
}
|