├── .nvmrc ├── .github ├── CODEOWNERS ├── workflows │ ├── test.yml │ ├── build.yml │ ├── static.yml │ └── release.yml └── ISSUE_TEMPLATE │ └── 🐛-bug-report.md ├── .env.sample ├── .ladle ├── head.html ├── burger.svg ├── config.mjs ├── style.css └── components.tsx ├── stories ├── .env.sample ├── sandbox.config.json ├── .ladle │ ├── head.html │ ├── burger.svg │ ├── config.mjs │ ├── style.css │ └── components.tsx ├── src │ ├── vite-env.d.ts │ ├── screen-blend-effect │ │ ├── screen-blend.frag │ │ ├── screen-blend-effect.ts │ │ └── screen-blend.tsx │ ├── billboard.stories.tsx │ ├── screen-sizer.tsx │ ├── html-on-top.stories.tsx │ ├── adaptive-dpr.tsx │ ├── comparison.stories.tsx │ ├── render-on-demand.stories.tsx │ ├── free-3d-buildings │ │ ├── get-buildings-data.ts │ │ ├── batched-standard-material │ │ │ ├── batched-standard-material.ts │ │ │ └── batched-properties-texture.ts │ │ ├── buildings-3d.stories.tsx │ │ └── batched-buildings.tsx │ ├── story-map.tsx │ ├── postprocessing.stories.tsx │ ├── extrude │ │ ├── extrude-coordinates.stories.tsx │ │ └── chaillot.ts │ ├── canvas │ │ ├── maplibre.stories.tsx │ │ └── mapbox.stories.tsx │ ├── maplibre │ │ └── story-maplibre.tsx │ ├── sunlight │ │ ├── scene.tsx │ │ └── sunlight.stories.tsx │ ├── multi-coordinates.stories.tsx │ ├── mapbox │ │ └── story-mapbox.tsx │ ├── my-scene.tsx │ ├── pivot-controls.stories.tsx │ └── ifc │ │ └── load-ifc-model.stories.tsx ├── public │ ├── web-ifc.wasm │ └── favicon.svg ├── README.md ├── tsconfig.node.json ├── vite.config.ts ├── tsconfig.json └── package.json ├── example-mapbox ├── .env.sample ├── sandbox.config.json ├── .ladle │ ├── head.html │ ├── components.tsx │ ├── style.css │ └── config.mjs ├── src │ ├── vite-env.d.ts │ ├── html-on-top.stories.tsx │ ├── comparison.stories.tsx │ ├── render-on-demand.stories.tsx │ ├── story-map.tsx │ ├── my-scene.tsx │ └── canvas.basic.stories.tsx ├── tsconfig.node.json ├── vite.config.ts ├── public │ └── favicon.svg ├── tsconfig.json └── package.json ├── example-maplibre ├── sandbox.config.json ├── .ladle │ ├── head.html │ ├── components.tsx │ ├── style.css │ └── config.mjs ├── src │ ├── vite-env.d.ts │ ├── html-on-top.stories.tsx │ ├── story-map.tsx │ ├── comparison.stories.tsx │ ├── render-on-demand.stories.tsx │ ├── canvas.basic.stories.tsx │ └── my-scene.tsx ├── tsconfig.node.json ├── vite.config.ts ├── public │ └── favicon.svg ├── tsconfig.json └── package.json ├── src ├── vite-env.d.ts ├── core │ ├── earth-radius.ts │ ├── canvas-overlay │ │ ├── render.tsx │ │ ├── init-r3m.tsx │ │ ├── init-canvas-fc.tsx │ │ ├── canvas-portal.tsx │ │ └── sync-camera-fc.tsx │ ├── use-function.ts │ ├── use-coords-to-matrix.ts │ ├── coords-to-matrix.ts │ ├── canvas-in-layer │ │ ├── use-canvas-in-layer.tsx │ │ ├── use-render.ts │ │ └── use-root.tsx │ ├── events.ts │ ├── use-coords.tsx │ ├── use-r3m.ts │ ├── sync-camera.ts │ └── generic-map.ts ├── api │ ├── coords.tsx │ ├── index.ts │ ├── use-map.ts │ ├── canvas-props.ts │ ├── vector-3-to-coords.ts │ ├── near-coordinates.tsx │ ├── coords-to-vector-3.ts │ └── coordinates.tsx ├── mapbox.index.ts ├── maplibre.index.ts ├── test │ ├── __snapshots__ │ │ └── coords-to-matrix.json │ └── coords-to-matrix.test.ts ├── mapbox │ └── canvas.tsx └── maplibre │ └── canvas.tsx ├── docs ├── extrude.png ├── basic-app.gif └── coordinates.png ├── public ├── web-ifc.wasm └── favicon.svg ├── .codesandbox └── ci.json ├── made-with └── studio-carto-urban-project.webp ├── tsconfig.mapbox.json ├── tsconfig.maplibre.json ├── vitest.global.setup.ts ├── vitest.config.ts ├── maplibre └── package.json ├── tsconfig.node.json ├── .npmignore ├── .changeset ├── config.json └── README.md ├── .gitignore ├── .eslintrc.cjs ├── tsconfig.types.json ├── tsconfig.json ├── LICENSE ├── vite.config.ts ├── package.json ├── CHANGELOG.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20.11.1 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @RodrigoHamuy 2 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | VITE_MAPBOX_TOKEN=add-your-mapbox-key -------------------------------------------------------------------------------- /.ladle/head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stories/.env.sample: -------------------------------------------------------------------------------- 1 | VITE_MAPBOX_TOKEN=add-your-mapbox-key -------------------------------------------------------------------------------- /stories/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 61000 3 | } -------------------------------------------------------------------------------- /example-mapbox/.env.sample: -------------------------------------------------------------------------------- 1 | VITE_MAPBOX_TOKEN=add-your-mapbox-key -------------------------------------------------------------------------------- /example-mapbox/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 61000 3 | } -------------------------------------------------------------------------------- /example-maplibre/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "port": 61000 3 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /stories/.ladle/head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example-mapbox/.ladle/head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/core/earth-radius.ts: -------------------------------------------------------------------------------- 1 | export const earthRadius = 6371008.8; 2 | -------------------------------------------------------------------------------- /stories/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example-maplibre/.ladle/head.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example-mapbox/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example-maplibre/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /docs/extrude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoHamuy/react-three-map/HEAD/docs/extrude.png -------------------------------------------------------------------------------- /docs/basic-app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoHamuy/react-three-map/HEAD/docs/basic-app.gif -------------------------------------------------------------------------------- /public/web-ifc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoHamuy/react-three-map/HEAD/public/web-ifc.wasm -------------------------------------------------------------------------------- /docs/coordinates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoHamuy/react-three-map/HEAD/docs/coordinates.png -------------------------------------------------------------------------------- /src/core/canvas-overlay/render.tsx: -------------------------------------------------------------------------------- 1 | export type Render = (gl: WebGLRenderingContext, matrix: number[]) => void; 2 | -------------------------------------------------------------------------------- /stories/public/web-ifc.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoHamuy/react-three-map/HEAD/stories/public/web-ifc.wasm -------------------------------------------------------------------------------- /stories/README.md: -------------------------------------------------------------------------------- 1 | This folder is being used as both a CodeSandbox CI and also as the main main stories from the root folder. -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["/example-mapbox", "/example-maplibre", "/stories"], 3 | "node": "18" 4 | } 5 | -------------------------------------------------------------------------------- /src/api/coords.tsx: -------------------------------------------------------------------------------- 1 | 2 | export interface Coords { 3 | longitude: number; 4 | latitude: number; 5 | altitude?: number; 6 | } 7 | -------------------------------------------------------------------------------- /made-with/studio-carto-urban-project.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RodrigoHamuy/react-three-map/HEAD/made-with/studio-carto-urban-project.webp -------------------------------------------------------------------------------- /tsconfig.mapbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.types.json", 3 | "compilerOptions": { 4 | "outDir": "dist/types" 5 | }, 6 | "include": ["src/mapbox.index.ts"], 7 | } 8 | -------------------------------------------------------------------------------- /stories/src/screen-blend-effect/screen-blend.frag: -------------------------------------------------------------------------------- 1 | void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) { 2 | outputColor = vec4(0.0,0.0,0.0, inputColor.a); 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.maplibre.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.types.json", 3 | "compilerOptions": { 4 | "outDir": "dist/maplibre/types", 5 | }, 6 | "include": ["src/maplibre.index.ts"], 7 | } 8 | -------------------------------------------------------------------------------- /example-mapbox/.ladle/components.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalProvider } from "@ladle/react"; 2 | import './style.css'; 3 | 4 | // @ts-ignore 5 | export const Provider: GlobalProvider = ({ children }) => <>{children} -------------------------------------------------------------------------------- /example-maplibre/.ladle/components.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalProvider } from "@ladle/react"; 2 | import './style.css'; 3 | 4 | // @ts-ignore 5 | export const Provider: GlobalProvider = ({ children }) => <>{children} -------------------------------------------------------------------------------- /vitest.global.setup.ts: -------------------------------------------------------------------------------- 1 | Object.assign(global.URL, { 2 | createObjectURL: (buffer) => ( 3 | new Blob([buffer.toString('binary')], { type: 'application/octet-stream' }) 4 | ) 5 | }) 6 | 7 | export default undefined; -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config' 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | environment: 'happy-dom', 7 | setupFiles: './vitest.global.setup.ts' 8 | }, 9 | }) -------------------------------------------------------------------------------- /maplibre/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "internal": true, 3 | "main": "../dist/maplibre/cjs/main.js", 4 | "module": "../dist/maplibre/es/main.mjs", 5 | "types": "../dist/maplibre/types/maplibre.index.d.ts", 6 | "sideEffects": false 7 | } -------------------------------------------------------------------------------- /src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './canvas-props'; 2 | export * from './coordinates'; 3 | export * from './coords'; 4 | export * from './coords-to-vector-3'; 5 | export * from './near-coordinates'; 6 | export * from './vector-3-to-coords'; -------------------------------------------------------------------------------- /src/mapbox.index.ts: -------------------------------------------------------------------------------- 1 | import type { Map } from 'mapbox-gl'; 2 | import { useMap as useMapGeneric } from './api/use-map'; 3 | 4 | export * from './api'; 5 | export * from './mapbox/canvas'; 6 | 7 | export const useMap = useMapGeneric; 8 | -------------------------------------------------------------------------------- /src/maplibre.index.ts: -------------------------------------------------------------------------------- 1 | import type { Map } from 'maplibre-gl'; 2 | import { useMap as useMapGeneric } from './api/use-map'; 3 | 4 | export * from './api'; 5 | export * from './maplibre/canvas'; 6 | 7 | export const useMap = useMapGeneric; 8 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | .changeset 3 | .ladle 4 | .github 5 | .npmrc 6 | .vscode 7 | *.tgz 8 | node_modules 9 | vite.config.ts 10 | yarn-error.log 11 | public 12 | node_modules 13 | build 14 | src 15 | docs 16 | example-mapbox 17 | example-maplibre 18 | tsconfig.json 19 | tsconfig.*.json -------------------------------------------------------------------------------- /stories/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /stories/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | optimizeDeps: { 8 | exclude: ['react-three-map'] 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /example-mapbox/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /example-mapbox/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | optimizeDeps: { 8 | exclude: ['react-three-map'] 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /example-maplibre/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /src/api/use-map.ts: -------------------------------------------------------------------------------- 1 | import { useThree } from "@react-three/fiber"; 2 | import { MapInstance } from "../core/generic-map"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export const useMap = (): T => useThree((s: any) => { 6 | return s.r3m?.map; 7 | }); -------------------------------------------------------------------------------- /src/test/__snapshots__/coords-to-matrix.json: -------------------------------------------------------------------------------- 1 | [ 2 | 2.4981121214570498e-8, 3 | 0, 4 | -0, 5 | 0, 6 | 0, 7 | -5.5469231906736245e-24, 8 | 2.4981121214570498e-8, 9 | 0, 10 | 0, 11 | 2.4981121214570498e-8, 12 | 5.5469231906736245e-24, 13 | 0, 14 | 0.5, 15 | 0.5, 16 | 0, 17 | 1, 18 | ] -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "public", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /example-mapbox/.ladle/style.css: -------------------------------------------------------------------------------- 1 | .ladle-main{ 2 | padding: 0; 3 | } 4 | .ladle-aside { 5 | order: -1; 6 | } 7 | header.ladle-addons { 8 | max-width: 18em 9 | } 10 | header.ladle-addons ul{ 11 | flex-wrap: wrap; 12 | } 13 | header.ladle-addons li{ 14 | margin: 0.35em; 15 | } 16 | header.ladle-addons li:nth-of-type(4) { 17 | display: none; 18 | } -------------------------------------------------------------------------------- /example-maplibre/.ladle/style.css: -------------------------------------------------------------------------------- 1 | .ladle-main{ 2 | padding: 0; 3 | } 4 | .ladle-aside { 5 | order: -1; 6 | } 7 | header.ladle-addons { 8 | max-width: 18em 9 | } 10 | header.ladle-addons ul{ 11 | flex-wrap: wrap; 12 | } 13 | header.ladle-addons li{ 14 | margin: 0.35em; 15 | } 16 | header.ladle-addons li:nth-of-type(4) { 17 | display: none; 18 | } -------------------------------------------------------------------------------- /.ladle/burger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stories/.ladle/burger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stories/src/screen-blend-effect/screen-blend-effect.ts: -------------------------------------------------------------------------------- 1 | import { BlendFunction, Effect } from "postprocessing"; 2 | 3 | import fragmentShader from "./screen-blend.frag?raw"; 4 | 5 | export class ScreenBlendEffect extends Effect { 6 | 7 | constructor() { 8 | 9 | super("ScreenBlendEffect", fragmentShader); 10 | 11 | this.blendMode.blendFunction = BlendFunction.SCREEN; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/core/use-function.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useRef } from 'react'; 2 | 3 | /* eslint-disable @typescript-eslint/no-explicit-any */ 4 | export const useFunction = any>(callback: T): T => { 5 | const callbackRef = useRef(callback); 6 | callbackRef.current = callback; 7 | return useCallback((...args: any[]) => { 8 | return callbackRef.current(...args); 9 | }, []) as T; 10 | }; 11 | -------------------------------------------------------------------------------- /example-maplibre/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | import { lstatSync } from 'fs'; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [react()], 8 | ...( 9 | lstatSync('node_modules/react-three-map').isSymbolicLink() 10 | ? { optimizeDeps: { exclude: ['react-three-map'] } } 11 | : {} 12 | ) 13 | }) 14 | -------------------------------------------------------------------------------- /stories/src/screen-blend-effect/screen-blend.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react' 2 | import { ScreenBlendEffect } from './screen-blend-effect' 3 | 4 | export const ScreenBlend = () => { 5 | 6 | const effect = useMemo(() => new ScreenBlendEffect(), []) 7 | 8 | useEffect(() => { 9 | return () => { 10 | effect.dispose() 11 | } 12 | }, [effect]) 13 | 14 | return 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .npmrc 26 | *.tgz 27 | build 28 | .env 29 | 30 | .vscode 31 | 32 | coverage -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react-hooks/recommended', 7 | ], 8 | parser: '@typescript-eslint/parser', 9 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 10 | plugins: ['react-refresh'], 11 | rules: { 12 | 'react-refresh/only-export-components': 'warn', 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /src/api/canvas-props.ts: -------------------------------------------------------------------------------- 1 | import { RenderProps } from "@react-three/fiber"; 2 | import { PropsWithChildren } from "react"; 3 | import { Coords } from "./coords"; 4 | 5 | export interface CanvasProps extends Coords, Omit, 'frameloop'>, PropsWithChildren { 6 | id?: string; 7 | beforeId?: string; 8 | frameloop?: 'always' | 'demand', 9 | /** render on a separated `` that sits on top of the map provider */ 10 | overlay?: boolean, 11 | } 12 | -------------------------------------------------------------------------------- /stories/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | addons: { 3 | a11y: { 4 | enabled: false, 5 | }, 6 | action: { 7 | enabled: false, 8 | }, 9 | control: { 10 | enabled: false, 11 | }, 12 | ladle: { 13 | enabled: false, 14 | }, 15 | mode: { 16 | enabled: true, 17 | }, 18 | rtl: { 19 | enabled: false, 20 | defaultState: false, 21 | }, 22 | width: { 23 | enabled: false, 24 | }, 25 | } 26 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | test: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 10 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version-file: '.nvmrc' 19 | - run: yarn install --frozen-lockfile 20 | - run: yarn test -------------------------------------------------------------------------------- /example-mapbox/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | addons: { 3 | a11y: { 4 | enabled: false, 5 | }, 6 | action: { 7 | enabled: false, 8 | }, 9 | control: { 10 | enabled: false, 11 | }, 12 | ladle: { 13 | enabled: false, 14 | }, 15 | mode: { 16 | enabled: true, 17 | }, 18 | rtl: { 19 | enabled: false, 20 | defaultState: false, 21 | }, 22 | width: { 23 | enabled: false, 24 | }, 25 | } 26 | } -------------------------------------------------------------------------------- /example-maplibre/.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | addons: { 3 | a11y: { 4 | enabled: false, 5 | }, 6 | action: { 7 | enabled: false, 8 | }, 9 | control: { 10 | enabled: false, 11 | }, 12 | ladle: { 13 | enabled: false, 14 | }, 15 | mode: { 16 | enabled: true, 17 | }, 18 | rtl: { 19 | enabled: false, 20 | defaultState: false, 21 | }, 22 | width: { 23 | enabled: false, 24 | }, 25 | } 26 | } -------------------------------------------------------------------------------- /src/core/use-coords-to-matrix.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from "react"; 2 | import { coordsToMatrix } from "./coords-to-matrix"; 3 | 4 | type Props = Parameters[0]; 5 | 6 | /** calculate matrix from coordinates */ 7 | export function useCoordsToMatrix({latitude, longitude, altitude, fromLngLat}: Props) { 8 | const m4 = useMemo(() => coordsToMatrix({ 9 | latitude, longitude, altitude, fromLngLat, 10 | }), [latitude, longitude, altitude, fromLngLat]); 11 | 12 | return m4; 13 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | concurrency: ${{ github.workflow }}-${{ github.ref }} 8 | 9 | jobs: 10 | build: 11 | name: Build 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 10 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version-file: '.nvmrc' 19 | - run: yarn install --frozen-lockfile 20 | - run: yarn ts:check 21 | - run: yarn build 22 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.ladle/config.mjs: -------------------------------------------------------------------------------- 1 | export default { 2 | stories: ["stories/src/**/*.stories.{js,jsx,ts,tsx,mdx}"], 3 | storyOrder: () => ['canvas--maplibre', 'canvas--mapbox', 'comparison--*','*'], 4 | addons: { 5 | a11y: { 6 | enabled: false, 7 | }, 8 | action: { 9 | enabled: false, 10 | }, 11 | control: { 12 | enabled: false, 13 | }, 14 | ladle: { 15 | enabled: false, 16 | }, 17 | mode: { 18 | enabled: true, 19 | }, 20 | rtl: { 21 | enabled: false, 22 | defaultState: false, 23 | }, 24 | width: { 25 | enabled: false, 26 | }, 27 | } 28 | } -------------------------------------------------------------------------------- /src/api/vector-3-to-coords.ts: -------------------------------------------------------------------------------- 1 | import { MathUtils, Vector3Tuple } from "three"; 2 | import { Coords } from "./coords"; 3 | import { earthRadius } from "../core/earth-radius"; 4 | 5 | export function vector3ToCoords(position: Vector3Tuple, origin: Coords): Coords { 6 | const [x, y, z] = position; 7 | const latitude = origin.latitude + (-z / earthRadius) * MathUtils.RAD2DEG; 8 | const longitude = origin.longitude + (x / earthRadius) * MathUtils.RAD2DEG / Math.cos(origin.latitude * MathUtils.DEG2RAD); 9 | const altitude = (origin.altitude || 0) + y; 10 | const coords: Coords = { latitude, longitude, altitude }; 11 | return coords; 12 | } -------------------------------------------------------------------------------- /src/core/canvas-overlay/init-r3m.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | import { FromLngLat, MapInstance } from "../generic-map"; 3 | import { useInitR3M } from "../use-r3m"; 4 | import { useSetCoords } from "../use-coords"; 5 | import { Coords } from "../../api/coords"; 6 | 7 | interface InitR3MProps extends Coords { 8 | map: MapInstance, 9 | fromLngLat: FromLngLat, 10 | } 11 | 12 | /** Initialises the `R3M` hook */ 13 | export const InitR3M = memo(({ 14 | longitude, latitude, altitude, ...props 15 | }) => { 16 | useInitR3M(props); 17 | useSetCoords({longitude, latitude, altitude}); 18 | return <> 19 | }) 20 | InitR3M.displayName = 'InitR3M'; -------------------------------------------------------------------------------- /stories/src/billboard.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Billboard, Cylinder, Text } from "@react-three/drei"; 2 | import { StoryMap } from "./story-map"; 3 | 4 | export function Default() { 5 | 6 | return 7 | 12 | 13 | 14 | 15 | 16 | 17 | Hi! 18 | 19 | 20 | 21 | } -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "emitDeclarationOnly": true, 5 | 6 | "target": "ES2020", 7 | "useDefineForClassFields": true, 8 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 9 | "module": "ESNext", 10 | "skipLibCheck": true, 11 | 12 | /* Bundler mode */ 13 | "moduleResolution": "bundler", 14 | "allowImportingTsExtensions": true, 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "react-jsx", 18 | 19 | /* Linting */ 20 | "strict": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noFallthroughCasesInSwitch": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /stories/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example-mapbox/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example-maplibre/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example-maplibre/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Node", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /src/api/near-coordinates.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useMemo } from "react"; 2 | import { useCoords } from "../core/use-coords"; 3 | import { CoordinatesProps } from "./coordinates"; 4 | import { coordsToVector3 } from "./coords-to-vector-3"; 5 | 6 | export const NearCoordinates = memo(({children, ...coords})=>{ 7 | const {latitude, longitude, altitude} = useCoords(); 8 | const pos = useMemo(()=>coordsToVector3(coords, {latitude, longitude, altitude}), [ // eslint-disable-line react-hooks/exhaustive-deps 9 | latitude, longitude, altitude, coords.latitude, coords.longitude, coords.altitude 10 | ]); 11 | return {children} 12 | }) 13 | NearCoordinates.displayName = 'NearCoordinates'; -------------------------------------------------------------------------------- /example-mapbox/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Node", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "allowSyntheticDefaultImports": true 24 | }, 25 | "include": ["src"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /example-mapbox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-mapbox", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "yarn ladle serve", 7 | "ts": "tsc -w" 8 | }, 9 | "dependencies": { 10 | "@ladle/react": "^4.0.2", 11 | "@react-three/drei": "^9.77.10", 12 | "@react-three/fiber": "^8.13.4", 13 | "leva": "^0.9.35", 14 | "mapbox-gl": "^3.10.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-map-gl": "^8.0.1", 18 | "react-three-map": "^0.8.1", 19 | "three": "~0.153.0", 20 | "three-stdlib": "^2.23.10" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.2.14", 24 | "@types/react-dom": "^18.2.6", 25 | "@types/three": "^0.152.1", 26 | "typescript": "^5.1.6", 27 | "vite": "^4.3.9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example-maplibre/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-maplibre", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "yarn ladle serve", 7 | "ts": "tsc -w" 8 | }, 9 | "dependencies": { 10 | "@ladle/react": "^4.0.2", 11 | "@react-three/drei": "^9.77.10", 12 | "@react-three/fiber": "^8.13.4", 13 | "leva": "^0.9.35", 14 | "maplibre-gl": "^5.4.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-map-gl": "8.0.1", 18 | "react-three-map": "^0.8.1", 19 | "three": "~0.153.0", 20 | "three-stdlib": "^2.23.10" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^18.2.14", 24 | "@types/react-dom": "^18.2.6", 25 | "@types/three": "^0.152.1", 26 | "typescript": "^5.1.6", 27 | "vite": "^4.3.9" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /stories/src/screen-sizer.tsx: -------------------------------------------------------------------------------- 1 | import { calculateScaleFactor } from '@react-three/drei'; 2 | import { Object3DProps, useFrame } from '@react-three/fiber'; 3 | import { memo, useRef } from 'react'; 4 | import { Object3D, Vector3 } from 'three'; 5 | 6 | const worldPos = new Vector3(); 7 | 8 | export const ScreenSizer = memo & {scale?: number}>(({ 9 | scale = 1, ...props 10 | }) => { 11 | const container = useRef(null); 12 | 13 | useFrame((state) => { 14 | const obj = container.current; 15 | if(!obj) return; 16 | const sf = calculateScaleFactor(obj.getWorldPosition(worldPos), scale, state.camera, state.size); 17 | obj.scale.setScalar(sf * scale); 18 | }); 19 | 20 | return ; 21 | }); 22 | 23 | ScreenSizer.displayName = 'ScreenSizer'; -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Deploy static content to Pages 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | deploy: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version-file: '.nvmrc' 27 | - run: yarn install --frozen-lockfile 28 | - run: yarn build:stories 29 | - uses: actions/configure-pages@v4 30 | - uses: actions/upload-pages-artifact@v3 31 | with: 32 | path: build 33 | - uses: actions/deploy-pages@v4 34 | -------------------------------------------------------------------------------- /src/core/coords-to-matrix.ts: -------------------------------------------------------------------------------- 1 | import { Euler, Matrix4, Quaternion, Vector3 } from "three"; 2 | import { FromLngLat } from "./generic-map"; 3 | import { Coords } from "../api/coords"; 4 | 5 | const quat = new Quaternion(); 6 | const euler = new Euler(); 7 | const pos = new Vector3(); 8 | const scale = new Vector3(); 9 | const m4 = new Matrix4(); 10 | 11 | /** calculate Matrix4 from coordinates */ 12 | export function coordsToMatrix({ 13 | longitude, latitude, altitude, fromLngLat 14 | }: Coords & { fromLngLat: FromLngLat }) { 15 | const center = fromLngLat([longitude, latitude], altitude); 16 | const scaleUnit = center.meterInMercatorCoordinateUnits(); 17 | pos.set(center.x, center.y, center.z || 0); 18 | scale.set(scaleUnit, -scaleUnit, scaleUnit); 19 | quat.setFromEuler(euler.set(-Math.PI * .5, 0, 0)); 20 | return m4.compose(pos, quat, scale).toArray(); 21 | } 22 | -------------------------------------------------------------------------------- /stories/src/html-on-top.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Html } from "@react-three/drei"; 2 | import { useState } from "react"; 3 | import { MathUtils } from "three"; 4 | import { StoryMap } from "./story-map"; 5 | 6 | export function Default() { 7 | 8 | const [hovered, hover] = useState(false); 9 | 10 | return 11 | Some HTML
content! 12 | hover(true)} 17 | onPointerOut={() => hover(false)} 18 | material-color={hovered ? 'purple' : 'orange'} 19 | /> 20 |
21 | } -------------------------------------------------------------------------------- /src/test/coords-to-matrix.test.ts: -------------------------------------------------------------------------------- 1 | import { MercatorCoordinate as MercatorCoordinateBox } from "mapbox-gl"; 2 | import { MercatorCoordinate as MercatorCoordinateLibre } from "maplibre-gl"; 3 | import { expect, it } from 'vitest'; 4 | import { coordsToMatrix } from '../core/coords-to-matrix'; 5 | 6 | it('coordsToMatrix works with mapbox', ()=>{ 7 | const value = coordsToMatrix({ 8 | latitude: 0, 9 | longitude: 0, 10 | altitude: 0, 11 | fromLngLat: MercatorCoordinateBox.fromLngLat, 12 | }); 13 | expect(value).toMatchFileSnapshot(`./__snapshots__/coords-to-matrix.json`); 14 | }) 15 | 16 | it('coordsToMatrix works with maplibre', ()=>{ 17 | const value = coordsToMatrix({ 18 | latitude: 0, 19 | longitude: 0, 20 | altitude: 0, 21 | fromLngLat: MercatorCoordinateLibre.fromLngLat, 22 | }); 23 | expect(value).toMatchFileSnapshot(`./__snapshots__/coords-to-matrix.json`); 24 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | "paths": { 18 | "react-three-map": ["./src/mapbox.index.ts"], 19 | "react-three-map/maplibre": ["./src/maplibre.index.ts"], 20 | }, 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true 27 | }, 28 | "include": ["src", "stories/src"], 29 | "exclude": ["node_modules"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /stories/src/adaptive-dpr.tsx: -------------------------------------------------------------------------------- 1 | import { useThree } from "@react-three/fiber"; 2 | import { memo, useEffect, useState } from "react"; 3 | import { useMap } from "react-three-map"; 4 | 5 | export const AdaptiveDpr = memo(() => { 6 | const initialDpr = useThree(s => s.viewport.initialDpr) 7 | const [dpr, _setDpr] = useState(initialDpr) 8 | const setDpr = useThree(s => s.setDpr); 9 | const map = useMap(); 10 | useEffect(() => { 11 | if(!map) return; 12 | const decreaseDpr = () => _setDpr(0.5) 13 | const increaseDpr = () => _setDpr(initialDpr); 14 | 15 | map.on('movestart', decreaseDpr); 16 | map.on('moveend', increaseDpr); 17 | return () => { 18 | map.off('movestart', decreaseDpr); 19 | map.off('moveend', increaseDpr); 20 | }; 21 | }, [map, setDpr, initialDpr]); 22 | 23 | useEffect(() => setDpr(dpr), [dpr, setDpr]); 24 | 25 | return <>; 26 | }); 27 | AdaptiveDpr.displayName = 'AdaptiveDpr'; 28 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | jobs: 11 | release: 12 | name: Release 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | permissions: 16 | contents: write # to create release (changesets/action) 17 | issues: write # to post issue comments (changesets/action) 18 | pull-requests: write # to create pull request (changesets/action) 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | with: 23 | node-version-file: '.nvmrc' 24 | - run: yarn install --frozen-lockfile 25 | - id: changesets 26 | uses: changesets/action@v1 27 | with: 28 | publish: yarn release 29 | env: 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /example-mapbox/src/html-on-top.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Html } from "@react-three/drei"; 2 | import { useState } from "react"; 3 | import { Canvas } from "react-three-map"; 4 | import { MathUtils } from "three"; 5 | import { StoryMap } from "./story-map"; 6 | 7 | export function Default() { 8 | 9 | const [hovered, hover] = useState(false); 10 | 11 | return 12 | 13 | Some HTML
content! 14 | hover(true)} 19 | onPointerOut={() => hover(false)} 20 | material-color={hovered ? 'purple' : 'orange'} 21 | /> 22 |
23 |
24 | } -------------------------------------------------------------------------------- /example-maplibre/src/html-on-top.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Html } from "@react-three/drei"; 2 | import { useState } from "react"; 3 | import { Canvas } from "react-three-map/maplibre"; 4 | import { MathUtils } from "three"; 5 | import { StoryMap } from "./story-map"; 6 | 7 | export function Default() { 8 | 9 | const [hovered, hover] = useState(false); 10 | 11 | return 12 | 13 | Some HTML
content! 14 | hover(true)} 19 | onPointerOut={() => hover(false)} 20 | material-color={hovered ? 'purple' : 'orange'} 21 | /> 22 |
23 |
24 | } -------------------------------------------------------------------------------- /stories/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "Node", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | "allowSyntheticDefaultImports": true, 24 | 25 | /* toggle these one for development */ 26 | "paths": { 27 | "react-three-map": ["../src/mapbox.index.ts"], 28 | "react-three-map/maplibre": ["../src/maplibre.index.ts"], 29 | } 30 | }, 31 | "include": ["src"], 32 | "exclude": ["node_modules"], 33 | "references": [{ "path": "./tsconfig.node.json" }] 34 | } 35 | -------------------------------------------------------------------------------- /src/core/canvas-in-layer/use-canvas-in-layer.tsx: -------------------------------------------------------------------------------- 1 | import { CanvasProps } from "../../api/canvas-props"; 2 | import { FromLngLat, MapInstance } from "../generic-map"; 3 | import { useCoordsToMatrix } from "../use-coords-to-matrix"; 4 | import { useRender } from "./use-render"; 5 | import { useRoot } from "./use-root"; 6 | 7 | /** get all the properties that you need to render as a map `` */ 8 | export function useCanvasInLayer(props: CanvasProps,fromLngLat: FromLngLat, map: MapInstance) { 9 | 10 | const {latitude, longitude, altitude, frameloop } = props; 11 | 12 | const origin = useCoordsToMatrix({ 13 | latitude, longitude, altitude, fromLngLat, 14 | }); 15 | 16 | const { onRemove, useThree, r3m } = useRoot(fromLngLat, map, props); 17 | 18 | const render = useRender({origin, frameloop, useThree, map, r3m}); 19 | 20 | return { 21 | id: props.id, 22 | beforeId: props.beforeId, 23 | onRemove, 24 | render, 25 | type: 'custom', 26 | renderingMode: '3d' 27 | } as const 28 | 29 | } -------------------------------------------------------------------------------- /.ladle/style.css: -------------------------------------------------------------------------------- 1 | .ladle-main { 2 | padding: 0; 3 | } 4 | 5 | .ladle-aside { 6 | order: -1; 7 | padding: 1em 1.5em; 8 | } 9 | 10 | header.ladle-addons { 11 | max-width: 18em 12 | } 13 | 14 | header.ladle-addons ul { 15 | flex-wrap: wrap; 16 | } 17 | 18 | header.ladle-addons li { 19 | margin: 0.35em; 20 | } 21 | 22 | header.ladle-addons li:nth-of-type(4) { 23 | display: none; 24 | } 25 | 26 | .story-menu-btn { 27 | border: none; 28 | background: none; 29 | position: fixed; 30 | right: 1rem; 31 | } 32 | 33 | .story-menu-btn img { 34 | width: 2rem; 35 | height: 2rem; 36 | } 37 | 38 | @media (max-width: 767px) { 39 | .ladle-aside h1 { 40 | margin: 0; 41 | } 42 | 43 | .ladle-aside { 44 | position: fixed; 45 | left: 0; 46 | right: 0; 47 | top: 0; 48 | } 49 | 50 | .hide.ladle-aside>*:not(:first-child) { 51 | display: none; 52 | } 53 | 54 | .hide .story-header { 55 | display: none; 56 | } 57 | } 58 | 59 | @media (min-width: 768px) { 60 | .story-menu-btn { 61 | display: none; 62 | } 63 | } -------------------------------------------------------------------------------- /stories/.ladle/style.css: -------------------------------------------------------------------------------- 1 | .ladle-main { 2 | padding: 0; 3 | } 4 | 5 | .ladle-aside { 6 | order: -1; 7 | padding: 1em 1.5em; 8 | } 9 | 10 | header.ladle-addons { 11 | max-width: 18em 12 | } 13 | 14 | header.ladle-addons ul { 15 | flex-wrap: wrap; 16 | } 17 | 18 | header.ladle-addons li { 19 | margin: 0.35em; 20 | } 21 | 22 | header.ladle-addons li:nth-of-type(4) { 23 | display: none; 24 | } 25 | 26 | .story-menu-btn { 27 | border: none; 28 | background: none; 29 | position: fixed; 30 | right: 1rem; 31 | } 32 | 33 | .story-menu-btn img { 34 | width: 2rem; 35 | height: 2rem; 36 | } 37 | 38 | @media (max-width: 767px) { 39 | .ladle-aside h1 { 40 | margin: 0; 41 | } 42 | 43 | .ladle-aside { 44 | position: fixed; 45 | left: 0; 46 | right: 0; 47 | top: 0; 48 | } 49 | 50 | .hide.ladle-aside>*:not(:first-child) { 51 | display: none; 52 | } 53 | 54 | .hide .story-header { 55 | display: none; 56 | } 57 | } 58 | 59 | @media (min-width: 768px) { 60 | .story-menu-btn { 61 | display: none; 62 | } 63 | } -------------------------------------------------------------------------------- /stories/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stories", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "dev": "yarn ladle serve" 7 | }, 8 | "dependencies": { 9 | "@ladle/react": "^4.0.2", 10 | "@react-three/drei": "^9.97.6", 11 | "@react-three/fiber": "^8.15.12", 12 | "@react-three/postprocessing": "^2.15.11", 13 | "@types/luxon": "^3.3.7", 14 | "@types/suncalc": "^1.9.2", 15 | "@types/tz-lookup": "^6.1.2", 16 | "leva": "^0.9.35", 17 | "luxon": "^3.4.4", 18 | "mapbox-gl": "^3.9.4", 19 | "maplibre-gl": "^5.4.0", 20 | "react": "^18.2.0", 21 | "react-dom": "^18.2.0", 22 | "react-map-gl": "^8.0.1", 23 | "react-three-map": "^0.8.1", 24 | "suncalc": "^1.9.0", 25 | "three": "^0.159.0", 26 | "three-stdlib": "^2.28.7", 27 | "tz-lookup": "^6.1.25", 28 | "web-ifc-three": "^0.0.125" 29 | }, 30 | "devDependencies": { 31 | "@types/react": "^18.2.15", 32 | "@types/react-dom": "^18.2.7", 33 | "@types/three": "^0.159.0", 34 | "@vitejs/plugin-react": "^4.0.3", 35 | "typescript": "^5.1.6", 36 | "vite": "^4.4.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 react-three-map authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example-maplibre/src/story-map.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeState, useLadleContext } from '@ladle/react'; 2 | import 'maplibre-gl/dist/maplibre-gl.css'; 3 | import { FC, PropsWithChildren } from "react"; 4 | import Map from 'react-map-gl/maplibre'; 5 | 6 | export interface StoryMapProps extends PropsWithChildren { 7 | latitude: number, 8 | longitude: number, 9 | zoom?: number, 10 | pitch?: number, 11 | } 12 | 13 | /** `` styled for stories */ 14 | export const StoryMap: FC = ({ 15 | latitude, longitude, zoom, children 16 | }) => { 17 | 18 | const theme = useLadleContext().globalState.theme; 19 | 20 | const mapStyle = theme === ThemeState.Dark 21 | ? "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json" 22 | : "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"; 23 | 24 | return
25 | 37 | {children} 38 | 39 |
40 | } -------------------------------------------------------------------------------- /example-maplibre/src/comparison.stories.tsx: -------------------------------------------------------------------------------- 1 | import { MapControls } from "@react-three/drei"; 2 | import { Canvas as FiberCanvas } from "@react-three/fiber"; 3 | import { useControls } from "leva"; 4 | import { Canvas } from "react-three-map/maplibre"; 5 | import { MyScene } from "./my-scene"; 6 | import { StoryMap } from "./story-map"; 7 | 8 | export function WithMap() { 9 | const showCamHelper = useShowCamHelper() 10 | return 11 | 12 | 13 | 14 | 15 | } 16 | 17 | export const WithoutMap = () => { 18 | const showCamHelper = useShowCamHelper() 19 | return
20 | 21 | 22 | 23 | 24 |
25 | } 26 | 27 | const useShowCamHelper = () => { 28 | const { showCamHelper } = useControls({ 29 | showCamHelper: { 30 | value: false, 31 | label: 'show camera helper' 32 | } 33 | }); 34 | return showCamHelper 35 | } -------------------------------------------------------------------------------- /src/core/events.ts: -------------------------------------------------------------------------------- 1 | import { Canvas, events as fiberEvents } from "@react-three/fiber"; 2 | import { Matrix4 } from "three"; 3 | 4 | /** projection * view matrix inverted */ 5 | const projViewInv = new Matrix4() 6 | 7 | type Events = Parameters[0]['events']; 8 | 9 | export const events: Events = (store) => { 10 | const originalEvents = fiberEvents(store); 11 | return { 12 | ...originalEvents, 13 | connect: target => { 14 | if (!originalEvents.connect) return; 15 | originalEvents.connect(target.parentElement!); // eslint-disable-line @typescript-eslint/no-non-null-assertion 16 | }, 17 | compute: (event, state) => { 18 | 19 | state.pointer.x = (event.offsetX / state.size.width) * 2 - 1; 20 | state.pointer.y = 1 - (event.offsetY / state.size.height) * 2; 21 | 22 | if (state.camera.userData.projByViewInv) projViewInv.fromArray(state.camera.userData.projByViewInv); 23 | 24 | state.raycaster.camera = state.camera; 25 | state.raycaster.ray.origin.setScalar(0).applyMatrix4(projViewInv); 26 | state.raycaster.ray.direction 27 | .set(state.pointer.x, state.pointer.y, 1) 28 | .applyMatrix4(projViewInv) 29 | .sub(state.raycaster.ray.origin) 30 | .normalize(); 31 | 32 | }, 33 | }; 34 | }; -------------------------------------------------------------------------------- /example-mapbox/src/comparison.stories.tsx: -------------------------------------------------------------------------------- 1 | import { MapControls } from "@react-three/drei"; 2 | import { Canvas as FiberCanvas } from "@react-three/fiber"; 3 | import { Canvas } from "react-three-map"; 4 | import { MyScene } from "./my-scene"; 5 | import { StoryMap } from "./story-map"; 6 | import { useControls } from "leva"; 7 | 8 | export function WithMap() { 9 | const showCamHelper = useShowCamHelper() 10 | return
11 | 12 | 13 | 14 | 15 | 16 |
17 | } 18 | 19 | export const WithoutMap = () => { 20 | const showCamHelper = useShowCamHelper() 21 | return
22 | 23 | 24 | 25 | 26 |
27 | } 28 | 29 | const useShowCamHelper = () => { 30 | const { showCamHelper } = useControls({ 31 | showCamHelper: { 32 | value: false, 33 | label: 'show camera helper' 34 | } 35 | }); 36 | return showCamHelper 37 | } -------------------------------------------------------------------------------- /stories/src/comparison.stories.tsx: -------------------------------------------------------------------------------- 1 | import { MapControls } from "@react-three/drei"; 2 | import { Canvas as FiberCanvas } from "@react-three/fiber"; 3 | import { useControls } from "leva"; 4 | import { MyScene } from "./my-scene"; 5 | import { StoryMap } from "./story-map"; 6 | 7 | export function WithMap() { 8 | const showCamHelper = useShowCamHelper() 9 | const {animate} = useControls({animate: true}); 10 | return 17 | 18 | 19 | } 20 | 21 | export const WithoutMap = () => { 22 | const showCamHelper = useShowCamHelper() 23 | const {animate} = useControls({animate: true}); 24 | return
25 | 26 | 27 | 28 | 29 |
30 | } 31 | 32 | const useShowCamHelper = () => { 33 | const { showCamHelper } = useControls({ 34 | showCamHelper: { 35 | value: false, 36 | label: 'show camera helper' 37 | } 38 | }); 39 | return showCamHelper 40 | } -------------------------------------------------------------------------------- /stories/src/render-on-demand.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Stats } from "@react-three/drei"; 2 | import { useRef, useState } from "react"; 3 | import { MathUtils } from "three"; 4 | import { StoryMap } from "./story-map"; 5 | 6 | export function Default() { 7 | 8 | const [hovered, hover] = useState(false); 9 | 10 | const ref = useRef(null) 11 | 12 | return
13 | 14 | 20 | hover(true)} 25 | onPointerOut={() => hover(false)} 26 | material-color={hovered ? 'purple' : 'orange'} 27 | /> 28 | 29 | 30 |
31 | Hover over the box, it will only render once to change colour, or when you move the camera. Look at the stats to confirm. 32 |
33 |
34 | } -------------------------------------------------------------------------------- /src/core/canvas-in-layer/use-render.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from "@react-three/fiber"; 2 | import { Matrix4Tuple, PerspectiveCamera } from "three"; 3 | import { UseBoundStore } from "zustand"; 4 | import { MapInstance } from "../generic-map"; 5 | import { syncCamera } from "../sync-camera"; 6 | import { useFunction } from "../use-function"; 7 | import { R3M } from "../use-r3m"; 8 | 9 | export function useRender({ 10 | map, origin, useThree, frameloop, r3m, 11 | } :{ 12 | map: MapInstance, 13 | origin: Matrix4Tuple, 14 | useThree: UseBoundStore, 15 | frameloop?: 'always' | 'demand', 16 | r3m: R3M 17 | }) { 18 | const render = useFunction((_gl: WebGL2RenderingContext, projViewMx: number[] | {defaultProjectionData: {mainMatrix: Record}}) => { 19 | const pVMx = 'defaultProjectionData' in projViewMx ? Object.values(projViewMx.defaultProjectionData.mainMatrix) : projViewMx; 20 | r3m.viewProjMx.splice(0, 16, ...pVMx) 21 | const state = useThree.getState(); 22 | const camera = state.camera as PerspectiveCamera; 23 | const {gl, advance} = state; 24 | syncCamera(camera as PerspectiveCamera, origin, pVMx as Matrix4Tuple); 25 | gl.resetState(); 26 | advance(Date.now() * 0.001, true); 27 | if (!frameloop || frameloop === 'always') map.triggerRepaint(); 28 | }) 29 | return render; 30 | } -------------------------------------------------------------------------------- /src/core/use-coords.tsx: -------------------------------------------------------------------------------- 1 | import { RootState, _roots, useThree } from "@react-three/fiber"; 2 | import { useMemo } from "react"; 3 | import { UseBoundStore } from 'zustand'; 4 | import { Coords } from "../api/coords"; 5 | 6 | export function useCoords() { 7 | const coords = useThree(s=>(s as any).coords) as Coords; // eslint-disable-line @typescript-eslint/no-explicit-any 8 | return coords; 9 | } 10 | 11 | export function useSetCoords({longitude, latitude, altitude}: Coords) { 12 | 13 | const canvas = useThree(s => s.gl.domElement); 14 | useMemo(()=>{ 15 | const store = _roots.get(canvas)!.store; // eslint-disable-line @typescript-eslint/no-non-null-assertion 16 | const coords : Coords = { longitude, latitude, altitude }; 17 | setCoords(store, coords); 18 | }, [longitude, latitude, altitude]) // eslint-disable-line react-hooks/exhaustive-deps 19 | } 20 | 21 | export function useSetRootCoords(store:UseBoundStore, { 22 | longitude, latitude, altitude 23 | }: Coords) { 24 | useMemo(()=>{ 25 | setCoords(store, {longitude, latitude, altitude}); 26 | }, [longitude, latitude, altitude]) // eslint-disable-line react-hooks/exhaustive-deps 27 | } 28 | 29 | export function setCoords(store:UseBoundStore, coords: Coords) { 30 | store.setState({coords} as any) // eslint-disable-line @typescript-eslint/no-explicit-any 31 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/🐛-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: "Bugs, missing documentation, or unexpected behaviour \U0001F914." 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 19 | 20 | - `three` version: 21 | - `@react-three/fiber` version: 22 | - `react-three-map` version: 23 | - `node` version: 24 | - `npm` (or `yarn`) version: 25 | 26 | ### Problem description: 27 | 28 | 29 | 30 | ### Relevant code: 31 | 32 | 33 | 34 | ```js 35 | let your = (code, tell) => `the ${story}` 36 | ``` 37 | 38 | ### Suggested solution: 39 | 40 | 44 | -------------------------------------------------------------------------------- /example-mapbox/src/render-on-demand.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Stats } from "@react-three/drei"; 2 | import { useRef, useState } from "react"; 3 | import { Canvas } from "react-three-map"; 4 | import { MathUtils } from "three"; 5 | import { StoryMap } from "./story-map"; 6 | 7 | export function Default() { 8 | 9 | const [hovered, hover] = useState(false); 10 | 11 | const ref = useRef(null) 12 | 13 | return
14 | 15 | 16 | 17 | hover(true)} 22 | onPointerOut={() => hover(false)} 23 | material-color={hovered ? 'purple' : 'orange'} 24 | /> 25 | 26 | 27 | 28 |
29 | Hover over the box, it will only render once to change colour, or when you move the camera. Look at the stats to confirm. 30 |
31 |
32 | } -------------------------------------------------------------------------------- /stories/src/free-3d-buildings/get-buildings-data.ts: -------------------------------------------------------------------------------- 1 | import { Coords } from "react-three-map"; 2 | 3 | export interface OverpassElement { 4 | type: "node" | "way" | "relation"; 5 | id: number; 6 | lat?: number; 7 | lon?: number; 8 | tags?: { 9 | height?: string; 10 | min_height?: string; 11 | ['building:levels']?: string; 12 | }; 13 | nodes?: number[]; 14 | geometry?: Array<{ 15 | lat: number; 16 | lon: number; 17 | }>; 18 | } 19 | interface OverpassApiResponse { 20 | version: number; 21 | generator: string; 22 | elements: OverpassElement[]; 23 | } 24 | 25 | export async function getBuildingsData({ start, end }: { start: Coords, end: Coords }) { 26 | const overpassApiUrl = "https://overpass-api.de/api/interpreter"; 27 | const bbox = [start.latitude, start.longitude, end.latitude, end.longitude].join(','); 28 | const query = ` 29 | [out:json]; 30 | ( 31 | way["building"](${bbox}); 32 | way["building:part"](${bbox}); 33 | relation["building"](${bbox}); 34 | relation["building:part"](${bbox}); 35 | ); 36 | out body; 37 | out geom; 38 | `; 39 | 40 | const response: OverpassApiResponse = await (await fetch(overpassApiUrl, { 41 | method: 'POST', 42 | body: query 43 | })).json(); 44 | 45 | const buildings = response.elements.filter(e => e.geometry); 46 | 47 | return buildings; 48 | } 49 | -------------------------------------------------------------------------------- /example-maplibre/src/render-on-demand.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Stats } from "@react-three/drei"; 2 | import { useRef, useState } from "react"; 3 | import { Canvas } from "react-three-map/maplibre"; 4 | import { MathUtils } from "three"; 5 | import { StoryMap } from "./story-map"; 6 | 7 | export function Default() { 8 | 9 | const [hovered, hover] = useState(false); 10 | 11 | const ref = useRef(null) 12 | 13 | return
14 | 15 | 16 | 17 | hover(true)} 22 | onPointerOut={() => hover(false)} 23 | material-color={hovered ? 'purple' : 'orange'} 24 | /> 25 | 26 | 27 | 28 |
29 | Hover over the box, it will only render once to change colour, or when you move the camera. Look at the stats to confirm. 30 |
31 |
32 | } -------------------------------------------------------------------------------- /src/core/use-r3m.ts: -------------------------------------------------------------------------------- 1 | import { RootState, _roots, useThree } from "@react-three/fiber"; 2 | import { useState } from "react"; 3 | import { Matrix4, Matrix4Tuple } from "three"; 4 | import { UseBoundStore } from 'zustand'; 5 | import { FromLngLat, MapInstance } from "./generic-map"; 6 | 7 | export interface R3M { 8 | /** Map provider */ 9 | map: T, 10 | /** view projection matrix coming from the map provider */ 11 | viewProjMx: Matrix4Tuple, 12 | fromLngLat: FromLngLat, 13 | } 14 | 15 | export function useR3M () { 16 | const r3m = useThree(s=>(s as any).r3m) as R3M; // eslint-disable-line @typescript-eslint/no-explicit-any 17 | return r3m; 18 | } 19 | 20 | /** init `useR3M` hook */ 21 | export function useInitR3M(props: { 22 | map: T; fromLngLat: FromLngLat; 23 | }) { 24 | const canvas = useThree(s => s.gl.domElement); 25 | // to run only once 26 | useState(()=>{ 27 | const store = _roots.get(canvas)!.store; // eslint-disable-line @typescript-eslint/no-non-null-assertion 28 | initR3M({...props, store}) 29 | }) 30 | } 31 | 32 | export function initR3M({store, ...props}: { 33 | map: T; 34 | fromLngLat: FromLngLat; 35 | store: UseBoundStore; 36 | }) { 37 | const viewProjMx = new Matrix4().identity().toArray(); 38 | const r3m : R3M = { ...props, viewProjMx }; 39 | store.setState({r3m} as any); // eslint-disable-line @typescript-eslint/no-explicit-any 40 | return r3m; 41 | } -------------------------------------------------------------------------------- /src/core/canvas-overlay/init-canvas-fc.tsx: -------------------------------------------------------------------------------- 1 | import { memo, useEffect, useState } from "react"; 2 | import { createPortal } from 'react-dom'; 3 | import { Matrix4Tuple } from "three"; 4 | import { CanvasProps } from "../../api/canvas-props"; 5 | import { FromLngLat, MapInstance } from "../generic-map"; 6 | import { CanvasPortal } from "./canvas-portal"; 7 | 8 | interface InitCanvasFCProps extends CanvasProps { 9 | map: MapInstance, 10 | setOnRender: (callback: () => (mx: Matrix4Tuple) => void) => void, 11 | frameloop?: 'always' | 'demand', 12 | fromLngLat: FromLngLat, 13 | } 14 | export const InitCanvasFC = memo((props) => { 15 | const canvas = props.map.getCanvas() // eslint-disable-line @typescript-eslint/no-non-null-assertion 16 | 17 | const [el] = useState(() => { 18 | const el = document.createElement('div'); 19 | el.style.position = 'absolute'; 20 | el.style.top = '0'; 21 | el.style.bottom = '0'; 22 | el.style.left = '0'; 23 | el.style.right = '0'; 24 | el.style.pointerEvents = 'none'; 25 | return el 26 | }) 27 | 28 | useEffect(() => { 29 | const parent = canvas.parentElement!; // eslint-disable-line @typescript-eslint/no-non-null-assertion 30 | parent.appendChild(el); 31 | return () => { 32 | parent.removeChild(el); 33 | } 34 | }, []) // eslint-disable-line react-hooks/exhaustive-deps 35 | return <> 36 | {createPortal(( 37 | 38 | ), el)} 39 | 40 | }) 41 | InitCanvasFC.displayName = 'InitCanvasFC'; 42 | -------------------------------------------------------------------------------- /stories/src/free-3d-buildings/batched-standard-material/batched-standard-material.ts: -------------------------------------------------------------------------------- 1 | import { MeshStandardMaterial } from "three" 2 | import { BatchedPropertiesTexture } from "./batched-properties-texture" 3 | 4 | // source: https://twitter.com/0xca0a/status/1734157969678278673 5 | 6 | const properties = { 7 | diffuse: 'vec3', 8 | emissive: 'vec3', 9 | metalness: 'float', 10 | roughness: 'float' 11 | } 12 | 13 | export class BatchedStandardMaterial extends MeshStandardMaterial { 14 | 15 | private propertiesTex: BatchedPropertiesTexture 16 | 17 | constructor(geometryCount: number) { 18 | super() 19 | 20 | this.propertiesTex = new BatchedPropertiesTexture(properties, geometryCount) 21 | 22 | this.onBeforeCompile = (parameters) => { 23 | 24 | if (Object.keys(properties).length === 0) return 25 | 26 | parameters.uniforms.propertiesTex = { value: this.propertiesTex } 27 | parameters.vertexShader = parameters.vertexShader.replace( 28 | 'void main() {', 29 | `varying float vBatchId; 30 | void main() { 31 | vBatchId = batchId + 0.5;` 32 | ) 33 | 34 | parameters.fragmentShader = parameters.fragmentShader.replace( 35 | 'void main() {', 36 | `uniform highp sampler2D propertiesTex; 37 | varying float vBatchId; 38 | void main() { 39 | ${this.propertiesTex.getGlsl()}` 40 | ) 41 | 42 | } 43 | } 44 | 45 | setValue(id: number, name: string, ...args: any[]) { // eslint-disable-line @typescript-eslint/no-explicit-any 46 | this.propertiesTex.setValue(id, name, ...args) 47 | } 48 | 49 | dispose() { 50 | super.dispose() 51 | this.propertiesTex?.dispose() 52 | } 53 | } -------------------------------------------------------------------------------- /stories/src/story-map.tsx: -------------------------------------------------------------------------------- 1 | import { MapControls } from '@react-three/drei'; 2 | import { Canvas } from '@react-three/fiber'; 3 | import { useControls } from 'leva'; 4 | import { FC, PropsWithChildren, ReactNode } from "react"; 5 | import { CanvasProps } from 'react-three-map'; 6 | import { StoryMapbox } from './mapbox/story-mapbox'; 7 | import { StoryMaplibre } from './maplibre/story-maplibre'; 8 | 9 | export enum MapProvider { 10 | maplibre = "maplibre", 11 | mapbox = "mapbox", 12 | nomap = "nomap", 13 | } 14 | 15 | export interface StoryMapProps extends PropsWithChildren { 16 | latitude: number, 17 | longitude: number, 18 | zoom?: number, 19 | pitch?: number, 20 | bearing?: number, 21 | canvas?: Partial, 22 | mapChildren?: ReactNode, 23 | mapboxChildren?: ReactNode, 24 | maplibreChildren?: ReactNode, 25 | } 26 | 27 | /** `` styled for stories */ 28 | export const StoryMap: FC = (props) => { 29 | 30 | const { mapProvider, overlay } = useControls({ 31 | mapProvider: { 32 | value: MapProvider.maplibre, 33 | options: MapProvider, 34 | label: 'map provider' 35 | }, 36 | overlay: { 37 | value: false, 38 | } 39 | }); 40 | 41 | const canvas = { overlay, ...props.canvas }; 42 | 43 | return
44 | {mapProvider === MapProvider.maplibre && } 45 | {mapProvider === MapProvider.mapbox && } 46 | {mapProvider === MapProvider.nomap && 50 | 51 | {props.children} 52 | } 53 |
54 | } -------------------------------------------------------------------------------- /src/core/canvas-overlay/canvas-portal.tsx: -------------------------------------------------------------------------------- 1 | import { Canvas } from "@react-three/fiber"; 2 | import { memo, useState } from "react"; 3 | import { Matrix4Tuple } from "three"; 4 | import { CanvasProps } from "../../api/canvas-props"; 5 | import { events } from "../events"; 6 | import { FromLngLat, MapInstance } from "../generic-map"; 7 | import { useFunction } from "../use-function"; 8 | import { InitR3M } from "./init-r3m"; 9 | import { SyncCameraFC } from "./sync-camera-fc"; 10 | 11 | interface CanvasPortalProps extends CanvasProps { 12 | setOnRender: (callback: () => (mx: Matrix4Tuple) => void) => void, 13 | map: MapInstance, 14 | fromLngLat: FromLngLat, 15 | } 16 | 17 | export const CanvasPortal = memo(({ 18 | children, latitude, longitude, altitude, 19 | setOnRender, map, fromLngLat, ...props 20 | }) => { 21 | 22 | const mapCanvas = map.getCanvas(); 23 | const eventSource = mapCanvas.parentElement!; // eslint-disable-line @typescript-eslint/no-non-null-assertion 24 | 25 | const [ready, setReady] = useState(false); 26 | 27 | const onReady = useFunction(() => { 28 | setReady(true); 29 | }) 30 | 31 | return 37 | 44 | 52 | {ready && children} 53 | 54 | }) 55 | CanvasPortal.displayName = 'CanvasPortal'; 56 | -------------------------------------------------------------------------------- /example-mapbox/src/story-map.tsx: -------------------------------------------------------------------------------- 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, PropsWithChildren } from "react"; 6 | import Map from 'react-map-gl/mapbox'; 7 | 8 | export interface StoryMapProps extends PropsWithChildren { 9 | latitude: number, 10 | longitude: number, 11 | zoom?: number, 12 | pitch?: number, 13 | } 14 | 15 | /** `` styled for stories */ 16 | export const StoryMap: FC = ({ 17 | latitude, longitude, zoom = 18, pitch = 60, children 18 | }) => { 19 | 20 | const { mapboxToken } = useControls({ 21 | mapboxToken: { 22 | value: import.meta.env.VITE_MAPBOX_TOKEN || '', 23 | label: 'mapbox token', 24 | } 25 | }) 26 | 27 | const theme = useLadleContext().globalState.theme; 28 | 29 | const mapStyle = theme === ThemeState.Dark 30 | ? "mapbox://styles/mapbox/dark-v11" 31 | : "mapbox://styles/mapbox/streets-v12"; 32 | 33 | Mapbox.accessToken = mapboxToken; 34 | 35 | return
36 | {!mapboxToken &&
Add a mapbox token to load this component
} 37 | {mapboxToken && 48 | {children} 49 | } 50 |
51 | } 52 | 53 | const Center = ({ children }: PropsWithChildren) => ( 54 |
{children}
61 | ) -------------------------------------------------------------------------------- /src/core/sync-camera.ts: -------------------------------------------------------------------------------- 1 | import { Matrix4, Matrix4Tuple, Object3D, PerspectiveCamera, Vector3 } from "three"; 2 | 3 | const originMx = new Matrix4(); 4 | 5 | /** projection * view matrix */ 6 | const projByView = new Matrix4(); 7 | /** projection * view matrix inverted */ 8 | const projByViewInv = new Matrix4(); 9 | 10 | /** forward */ 11 | const fwd = new Vector3(); 12 | 13 | 14 | export function syncCamera(camera: PerspectiveCamera, origin: Matrix4Tuple, mapCamMx: Matrix4Tuple) { 15 | 16 | projByView 17 | .fromArray(mapCamMx) 18 | .multiply(originMx.fromArray(origin)); 19 | projByViewInv 20 | .copy(projByView) 21 | .invert(); 22 | 23 | updateCamera(camera, projByViewInv); 24 | camera.updateMatrix(); 25 | camera.updateMatrixWorld(true); 26 | 27 | camera.projectionMatrix.copy(camera.matrix).premultiply(projByView); 28 | camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert(); 29 | 30 | camera.far = calculateFar( 31 | camera.matrix.elements[10], camera.matrix.elements[14], camera.near 32 | ) 33 | 34 | camera.userData.projByView = projByView.toArray(); 35 | camera.userData.projByViewInv = projByViewInv.toArray(); 36 | 37 | } 38 | 39 | const updateCamera = (target: Object3D, projByViewInv: Matrix4) => { 40 | 41 | target.position 42 | .setScalar(0) 43 | .applyMatrix4(projByViewInv) 44 | 45 | target.up 46 | .set(0, -1, 0) 47 | .applyMatrix4(projByViewInv) 48 | .negate() 49 | .add(target.position) 50 | .normalize() 51 | 52 | fwd 53 | .set(0, 0, 1) 54 | .applyMatrix4(projByViewInv) 55 | 56 | target.lookAt(fwd); 57 | 58 | } 59 | 60 | function calculateFar(c: number, d: number, near: number): number { 61 | const numerator = d * (c - 1); 62 | const denominator = c * near + near; 63 | const far = numerator / denominator; 64 | return far; 65 | } -------------------------------------------------------------------------------- /src/api/coords-to-vector-3.ts: -------------------------------------------------------------------------------- 1 | import { MathUtils, Vector3Tuple } from 'three'; 2 | import { Coords } from './coords'; 3 | import { earthRadius } from "../core/earth-radius"; 4 | 5 | 6 | const mercatorScaleLookup: { [key: number]: number } = {}; 7 | 8 | function getMercatorScale(lat: number): number { 9 | const index = Math.round(lat * 1000); 10 | if (mercatorScaleLookup[index] === undefined) { 11 | mercatorScaleLookup[index] = 1 / Math.cos(lat * MathUtils.DEG2RAD); 12 | } 13 | return mercatorScaleLookup[index]; 14 | } 15 | 16 | export function averageMercatorScale(originLat: number, pointLat: number, steps = 10): number { 17 | let totalScale = 0; 18 | const latStep = (pointLat - originLat) / steps; 19 | for (let i = 0; i <= steps; i++) { 20 | const lat = originLat + latStep * i; 21 | totalScale += getMercatorScale(lat); 22 | } 23 | return totalScale / (steps + 1); 24 | } 25 | 26 | export function coordsToVector3(point: Coords, origin: Coords): Vector3Tuple { 27 | const latitudeDiff = (point.latitude - origin.latitude) * MathUtils.DEG2RAD; 28 | const longitudeDiff = (point.longitude - origin.longitude) * MathUtils.DEG2RAD; 29 | const altitudeDiff = (point.altitude || 0) - (origin.altitude || 0); 30 | 31 | const x = longitudeDiff * earthRadius * Math.cos(origin.latitude * MathUtils.DEG2RAD); 32 | const y = altitudeDiff; 33 | 34 | // dynamic step size based on latitude difference. calculate the mercator unit scale at origin 35 | // and the scale average along the line to the point for better accuracy far from origin 36 | const steps = Math.ceil(Math.abs(point.latitude - origin.latitude)) * 100 + 1; 37 | const avgScale = averageMercatorScale(origin.latitude, point.latitude, steps); 38 | 39 | const z = ((-latitudeDiff * earthRadius) / getMercatorScale(origin.latitude)) * avgScale; 40 | return [x, y, z] as Vector3Tuple; 41 | } -------------------------------------------------------------------------------- /stories/src/postprocessing.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Plane } from "@react-three/drei"; 2 | import { EffectComposer, N8AO } from '@react-three/postprocessing'; 3 | import { levaStore, useControls } from "leva"; 4 | import { useEffect, useState } from "react"; 5 | import { MathUtils } from "three"; 6 | import { ScreenBlend } from "./screen-blend-effect/screen-blend"; 7 | import { StoryMap } from "./story-map"; 8 | import { AdaptiveDpr } from "./adaptive-dpr"; 9 | 10 | export function Default() { 11 | const { ao } = useControls({ ao: { value: true, label: 'Ambient Occlusion' } }); 12 | 13 | // default this story to use overlay 14 | useEffect(() => { 15 | const overlay = levaStore.get('overlay'); 16 | levaStore.setValueAtPath('overlay', true, true); 17 | return () => { 18 | // reset overlay 19 | if (overlay) return; 20 | levaStore.setValueAtPath('overlay', overlay, true); 21 | } 22 | }, []) 23 | 24 | const [toggle, setToggle] = useState(false); 25 | return 32 | 33 | {ao && 34 | 35 | 36 | {/* ScreenBlend forces transparency to work on the canvas overlay */} 37 | } 38 | 39 | setToggle(true)} 44 | onPointerLeave={() => setToggle(false)} 45 | /> 46 | 47 | 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/api/coordinates.tsx: -------------------------------------------------------------------------------- 1 | import { createPortal, useFrame, useThree } from "@react-three/fiber"; 2 | import { PropsWithChildren, memo, useLayoutEffect, useRef, useState } from "react"; 3 | import { Matrix4Tuple, PerspectiveCamera, Scene } from "three"; 4 | import { syncCamera } from "../core/sync-camera"; 5 | import { useCoordsToMatrix } from "../core/use-coords-to-matrix"; 6 | import { R3M, useR3M } from "../core/use-r3m"; 7 | 8 | export interface CoordinatesProps extends PropsWithChildren { 9 | longitude: number, 10 | latitude: number, 11 | altitude?: number, 12 | } 13 | 14 | export const Coordinates = memo(({ 15 | latitude, longitude, altitude = 0, children 16 | }) => { 17 | 18 | const [scene] = useState(() => new Scene()) 19 | 20 | const r3m = useR3M(); 21 | 22 | const origin = useCoordsToMatrix({ 23 | latitude, longitude, altitude, fromLngLat: r3m.fromLngLat, 24 | }); 25 | 26 | 27 | return <>{createPortal(<> 28 | 29 | {children} 30 | , scene, { events: { priority: 2 } })} 31 | }) 32 | 33 | Coordinates.displayName = 'Coordinates'; 34 | 35 | interface RenderAtCoordsProps { 36 | r3m: R3M, 37 | origin: Matrix4Tuple 38 | } 39 | 40 | function RenderAtCoords({ r3m, origin }: RenderAtCoordsProps) { 41 | 42 | const { gl, scene, set } = useThree() 43 | 44 | const cameraRef = useRef(null) 45 | 46 | useFrame(() => { 47 | if (!cameraRef.current) return; 48 | syncCamera(cameraRef.current, origin, r3m.viewProjMx); 49 | gl.render(scene, cameraRef.current); 50 | }) 51 | 52 | useLayoutEffect(() => { 53 | if (!cameraRef.current) return; 54 | set({ 55 | invalidate: () => { 56 | if (!r3m.map) return; 57 | r3m.map.triggerRepaint(); 58 | }, 59 | camera: cameraRef.current, 60 | }); 61 | }, [set, r3m]) 62 | 63 | return 64 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import { resolve } from 'path'; 4 | 5 | /** 0: no lib mode, 1: ES, 2: cjs */ 6 | const libMode = parseInt(process.env.LIB_MODE!) || 0; 7 | 8 | /** 0: MapLibre, 1: MapBox */ 9 | const mapProvider = parseInt(process.env.MAP_MODE!) || 0; 10 | 11 | const isES = libMode === 1; 12 | 13 | const isMaplibre = mapProvider === 0; 14 | 15 | const entry = `src/${isMaplibre ? 'maplibre' : 'mapbox'}.index.ts`; 16 | 17 | let outDir = isMaplibre ? 'dist/maplibre' : 'dist'; 18 | 19 | outDir = `${outDir}/${isES ? 'es' : 'cjs'}`; 20 | 21 | // https://vitejs.dev/config/ 22 | export default defineConfig({ 23 | plugins: [react()], 24 | ...(!libMode 25 | // story mode 26 | ? { 27 | base: '', 28 | resolve: { 29 | alias: { 30 | 'react-three-map/maplibre': resolve(__dirname, './src/maplibre.index.ts'), 31 | 'react-three-map/mapbox': resolve(__dirname, './src/mapbox.index.ts'), 32 | 'react-three-map': resolve(__dirname, './src/mapbox.index.ts'), 33 | } 34 | } 35 | } 36 | // lib mode 37 | : { 38 | publicDir: false, 39 | build: { 40 | minify: false, 41 | lib: { 42 | entry, 43 | name: 'react-three-map', 44 | formats: isES ? ['es'] : ['cjs'], 45 | fileName: 'main', 46 | }, 47 | outDir, 48 | rollupOptions: { 49 | output: !isES ? undefined : { sourcemap: true, preserveModules: true }, 50 | external: [ 51 | "@react-three/fiber", 52 | "maplibre-gl", 53 | "mapbox-gl", 54 | "react", 55 | 'react/jsx-runtime', 56 | "react-dom", 57 | "react-map-gl", 58 | "react-map-gl/mapbox", 59 | "react-map-gl/maplibre", 60 | "three", 61 | ] 62 | }, 63 | }, 64 | 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /src/core/generic-map.ts: -------------------------------------------------------------------------------- 1 | // mock of functions used by `react-three-map` from `Maplibre` or `Mapbox` 2 | 3 | /** Generic interface of Mapbox/Maplibre `LayerProps` */ 4 | export interface LayerProps { 5 | id: string; 6 | type: 'custom'; 7 | renderingMode: '3d'; 8 | onRemove?(map: MapInstance, gl: WebGLRenderingContext): void; 9 | onAdd?(map: MapInstance, gl: WebGLRenderingContext): void; 10 | prerender?(gl: WebGLRenderingContext, matrix: number[]): void; 11 | render(gl: WebGLRenderingContext, matrix: number[]): void; 12 | } 13 | 14 | /** Generic interface of Mapbox/Maplibre `LngLatLike` */ 15 | export type LngLatLike = { 16 | lng: number; 17 | lat: number; 18 | } | { 19 | lon: number; 20 | lat: number; 21 | } | [ 22 | number, 23 | number 24 | ]; 25 | 26 | /** Generic interface of Mapbox/Maplibre `static MercatorCoordinate.fromLngLat` */ 27 | export type FromLngLat = (lngLatLike: LngLatLike, altitude?: number) => MercatorCoordinate; 28 | 29 | /** Generic interface of Mapbox/Maplibre typeof `MercatorCoordinate` */ 30 | export interface MercatorCoordinate { 31 | x: number; 32 | y: number; 33 | z?: number; 34 | meterInMercatorCoordinateUnits(): number; 35 | } 36 | 37 | /** Generic interface of Mapbox/Maplibre `Map` */ 38 | export interface MapInstance { 39 | getCanvas(): HTMLCanvasElement; 40 | /** MapLibre only */ 41 | getPixelRatio?: ()=>number; 42 | triggerRepaint(): void; 43 | // eslint-disable-next-line @typescript-eslint/ban-types 44 | on(type: T, listener: (ev: MapEventType[T] & Object) => void): void; 45 | // eslint-disable-next-line @typescript-eslint/ban-types 46 | off(type: T, listener: (ev: MapEventType[T] & Object) => void): void; 47 | } 48 | 49 | /** Generic interface of Mapbox/Maplibre `MapEventType` */ 50 | export type MapEventType = { 51 | resize: MapEvent; 52 | }; 53 | 54 | /** Generic interface of `MapLibreEvent` or `MapBoxEvent` */ 55 | export interface MapEvent { 56 | type: string; 57 | target: MapInstance; 58 | originalEvent: TOrig; 59 | } -------------------------------------------------------------------------------- /example-maplibre/src/canvas.basic.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame, Vector3 } from "@react-three/fiber"; 2 | import 'maplibre-gl/dist/maplibre-gl.css'; 3 | import { FC, useRef, useState } from "react"; 4 | import Map from 'react-map-gl/maplibre'; 5 | import { Canvas } from "react-three-map/maplibre"; 6 | import { Mesh } from "three"; 7 | 8 | export default { title: 'Canvas' } 9 | 10 | export function BasicExample() { 11 | return
12 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 | } 37 | 38 | const Box: FC<{ position: Vector3 }> = (props) => { 39 | // This reference gives us direct access to the THREE.Mesh object 40 | const ref = useRef(null) 41 | // Hold state for hovered and clicked events 42 | const [hovered, hover] = useState(false) 43 | const [clicked, click] = useState(false) 44 | // Subscribe this component to the render-loop, rotate the mesh every frame 45 | useFrame((_state, delta) => { 46 | if (!ref.current) return; 47 | ref.current.rotation.x += delta; 48 | ref.current.rotation.z -= delta; 49 | }) 50 | // Return the view, these are regular Threejs elements expressed in JSX 51 | return ( 52 | click(!clicked)} 57 | onPointerOver={() => hover(true)} 58 | onPointerOut={() => hover(false)}> 59 | 60 | 61 | 62 | ) 63 | } -------------------------------------------------------------------------------- /stories/src/extrude/extrude-coordinates.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Environment, Extrude, Html } from "@react-three/drei"; 2 | import { useMemo } from "react"; 3 | import { Coords, coordsToVector3 } from "react-three-map"; 4 | import { MathUtils, Shape, Vector2Tuple } from "three"; 5 | import { StoryMap } from "../story-map"; 6 | import { Chaillot } from "./chaillot"; 7 | 8 | const origin: Coords = { 9 | longitude: 2.289449241104535, 10 | latitude: 48.861422672242895, 11 | } 12 | 13 | export function ExtrudeCoordinates() { 14 | 15 | return 21 | {Chaillot.map((points, i) => )} 26 | 31 | 35 | 38 | Palais de Chaillot 39 | 40 | 41 | 42 | 43 | 44 | } 45 | 46 | function ExtrudePoints({ points, origin }: { points: Vector2Tuple[], origin: Coords }) { 47 | const points3D = useMemo(() => { 48 | const points3D = points.map(p => coordsToVector3({ longitude: p[0], latitude: p[1] }, origin)) 49 | return points3D; 50 | }, [origin, points]) 51 | const shape = useMemo(() => { 52 | const shape = new Shape(); 53 | shape.moveTo(points3D[0][2], points3D[0][0]); 54 | for (let i = 1; i < points3D.length; i++) { 55 | shape.lineTo(points3D[i][2], points3D[i][0]); 56 | } 57 | shape.closePath(); 58 | return shape; 59 | }, [points3D]) 60 | 61 | return 64 | 65 | 66 | 67 | } -------------------------------------------------------------------------------- /src/mapbox/canvas.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { extend } from "@react-three/fiber"; 3 | import { MercatorCoordinate } from "mapbox-gl"; 4 | import { memo, useState } from "react"; 5 | import { Layer, useMap } from "react-map-gl/mapbox"; 6 | import * as THREE from "three"; 7 | import { Matrix4Tuple } from "three"; 8 | import { CanvasProps } from "../api/canvas-props"; 9 | import { useCanvasInLayer } from "../core/canvas-in-layer/use-canvas-in-layer"; 10 | import { InitCanvasFC } from "../core/canvas-overlay/init-canvas-fc"; 11 | import { Render } from "../core/canvas-overlay/render"; 12 | import { MapInstance } from "../core/generic-map"; 13 | import { useFunction } from "../core/use-function"; 14 | 15 | extend(THREE); 16 | 17 | const fromLngLat = MercatorCoordinate.fromLngLat 18 | 19 | /** `react-three-fiber` canvas inside `Mapbox` */ 20 | export const Canvas = memo(({ overlay, ...props }) => { 21 | 22 | const map = useMap().current!.getMap(); // eslint-disable-line @typescript-eslint/no-non-null-assertion 23 | 24 | return <> 25 | {overlay && } 26 | {!overlay && } 27 | 28 | }) 29 | Canvas.displayName = 'Canvas' 30 | 31 | interface CanvasPropsAndMap extends CanvasProps { 32 | map: MapInstance; 33 | } 34 | 35 | const CanvasInLayer = memo(({ map, ...props }) => { 36 | const layerProps = useCanvasInLayer(props, fromLngLat, map); 37 | /* @ts-ignore */ // eslint-disable-line @typescript-eslint/ban-ts-comment 38 | return 39 | }) 40 | CanvasInLayer.displayName = 'CanvasInLayer'; 41 | 42 | const CanvasOverlay = memo(({ map, id, beforeId, ...props }) => { 43 | const [onRender, setOnRender] = useState<(mx: Matrix4Tuple) => void>(); 44 | 45 | const render = useFunction((_gl, mx) => { 46 | if (!onRender) return; 47 | onRender(mx as Matrix4Tuple); 48 | }) 49 | 50 | return <> 51 | {/* @ts-ignore */} 52 | 53 | 58 | 59 | }) 60 | CanvasInLayer.displayName = 'CanvasInLayer'; -------------------------------------------------------------------------------- /src/maplibre/canvas.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | import { extend } from "@react-three/fiber"; 3 | import { MercatorCoordinate } from "maplibre-gl"; 4 | import { memo, useState } from "react"; 5 | import { Layer, useMap } from "react-map-gl/maplibre"; 6 | import * as THREE from "three"; 7 | import { Matrix4Tuple } from "three"; 8 | import { CanvasProps } from "../api/canvas-props"; 9 | import { useCanvasInLayer } from "../core/canvas-in-layer/use-canvas-in-layer"; 10 | import { InitCanvasFC } from "../core/canvas-overlay/init-canvas-fc"; 11 | import { Render } from "../core/canvas-overlay/render"; 12 | import { MapInstance } from "../core/generic-map"; 13 | import { useFunction } from "../core/use-function"; 14 | 15 | extend(THREE); 16 | 17 | const fromLngLat = MercatorCoordinate.fromLngLat 18 | 19 | /** `react-three-fiber` canvas inside `MapLibre` */ 20 | export const Canvas = memo(({ overlay, ...props }) => { 21 | 22 | const map = useMap().current!.getMap(); // eslint-disable-line @typescript-eslint/no-non-null-assertion 23 | 24 | return <> 25 | {overlay && } 26 | {!overlay && } 27 | 28 | }) 29 | Canvas.displayName = 'Canvas' 30 | 31 | interface CanvasPropsAndMap extends CanvasProps { 32 | map: MapInstance; 33 | } 34 | 35 | const CanvasInLayer = memo(({ map, ...props }) => { 36 | const layerProps = useCanvasInLayer(props, fromLngLat, map); 37 | /* @ts-ignore */ // eslint-disable-line @typescript-eslint/ban-ts-comment 38 | return 39 | }) 40 | CanvasInLayer.displayName = 'CanvasInLayer'; 41 | 42 | const CanvasOverlay = memo(({ map, id, beforeId, ...props }) => { 43 | const [onRender, setOnRender] = useState<(mx: Matrix4Tuple) => void>(); 44 | 45 | const render = useFunction((_gl, mx) => { 46 | if (!onRender) return; 47 | onRender(mx as Matrix4Tuple); 48 | }) 49 | 50 | return <> 51 | {/* @ts-ignore */} 52 | 53 | 58 | 59 | }) 60 | CanvasInLayer.displayName = 'CanvasInLayer'; -------------------------------------------------------------------------------- /.ladle/components.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalProvider } from "@ladle/react"; 2 | import './style.css'; 3 | import React, { memo, useCallback, useEffect, useState } from "react"; 4 | import { createPortal } from "react-dom"; 5 | import burgerSvg from './burger.svg'; 6 | 7 | export const Provider: GlobalProvider = ({ children }) => { 8 | const [header] = useState(() => document.createElement('div')); 9 | 10 | const toggle = useCallback(() => { 11 | if (!header.parentElement) return; 12 | header.parentElement.classList.toggle('hide') 13 | }, []) 14 | 15 | const hide = useCallback((e: any)=>{ 16 | if (!header.parentElement) return; 17 | if(e.target.nodeName !== 'A') return; 18 | header.parentElement.classList.add('hide') 19 | }, []) 20 | 21 | useEffect(() => { 22 | const container = document.querySelector('.ladle-aside'); 23 | if (!container) return; 24 | container.prepend(header); 25 | container.classList.add('hide'); 26 | container.addEventListener('click', hide) 27 | return () => { 28 | header.remove(); 29 | container.removeEventListener('click', hide) 30 | } 31 | }, []) 32 | return <> 33 | {createPortal(
, header)} 34 | {children} 35 | 36 | } 37 | 38 | interface HeaderProps { 39 | toggle: () => void; 40 | } 41 | 42 | const Header = memo(({ toggle }) => { 43 | return <> 44 | 45 |

react-three-map

46 |
47 |

R3F inside Mapbox & Maplibre

48 | 56 |
57 | 58 | }) 59 | Header.displayName = 'Header'; -------------------------------------------------------------------------------- /stories/.ladle/components.tsx: -------------------------------------------------------------------------------- 1 | import { GlobalProvider } from "@ladle/react"; 2 | import './style.css'; 3 | import React, { memo, useCallback, useEffect, useState } from "react"; 4 | import { createPortal } from "react-dom"; 5 | import burgerSvg from './burger.svg'; 6 | 7 | export const Provider: GlobalProvider = ({ children }) => { 8 | const [header] = useState(() => document.createElement('div')); 9 | 10 | const toggle = useCallback(() => { 11 | if (!header.parentElement) return; 12 | header.parentElement.classList.toggle('hide') 13 | }, []) 14 | 15 | const hide = useCallback((e: any)=>{ 16 | if (!header.parentElement) return; 17 | if(e.target.nodeName !== 'A') return; 18 | header.parentElement.classList.add('hide') 19 | }, []) 20 | 21 | useEffect(() => { 22 | const container = document.querySelector('.ladle-aside'); 23 | if (!container) return; 24 | container.prepend(header); 25 | container.classList.add('hide'); 26 | container.addEventListener('click', hide) 27 | return () => { 28 | header.remove(); 29 | container.removeEventListener('click', hide) 30 | } 31 | }, []) 32 | return <> 33 | {createPortal(
, header)} 34 | {children} 35 | 36 | } 37 | 38 | interface HeaderProps { 39 | toggle: () => void; 40 | } 41 | 42 | const Header = memo(({ toggle }) => { 43 | return <> 44 | 45 |

react-three-map

46 |
47 |

R3F inside Mapbox & Maplibre

48 | 56 |
57 | 58 | }) 59 | Header.displayName = 'Header'; -------------------------------------------------------------------------------- /stories/src/canvas/maplibre.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame, Vector3 } from "@react-three/fiber"; 2 | import 'maplibre-gl/dist/maplibre-gl.css'; 3 | import { FC, useRef, useState } from "react"; 4 | import Map from 'react-map-gl/maplibre'; 5 | import { Mesh } from "three"; 6 | import { Canvas } from "react-three-map/maplibre"; 7 | import { Leva } from "leva"; 8 | 9 | export default { title: 'Canvas' } 10 | 11 | export function Maplibre() { 12 | return <> 13 | 14 |
15 | 27 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | 41 | } 42 | 43 | const Box: FC<{ position: Vector3 }> = (props) => { 44 | // This reference gives us direct access to the THREE.Mesh object 45 | const ref = useRef(null) 46 | // Hold state for hovered and clicked events 47 | const [hovered, hover] = useState(false) 48 | const [clicked, click] = useState(false) 49 | // Subscribe this component to the render-loop, rotate the mesh every frame 50 | useFrame((_state, delta) => { 51 | if (!ref.current) return; 52 | ref.current.rotation.x += delta; 53 | ref.current.rotation.z -= delta; 54 | }) 55 | // Return the view, these are regular Threejs elements expressed in JSX 56 | return ( 57 | click(!clicked)} 62 | onPointerOver={() => hover(true)} 63 | onPointerOut={() => hover(false)}> 64 | 65 | 66 | 67 | ) 68 | } -------------------------------------------------------------------------------- /stories/src/maplibre/story-maplibre.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeState, useLadleContext } from '@ladle/react'; 2 | import MapLibre from "maplibre-gl"; 3 | import 'maplibre-gl/dist/maplibre-gl.css'; 4 | import { FC, memo, useEffect, useRef } from "react"; 5 | import Map, { useMap } from 'react-map-gl/maplibre'; 6 | import { StoryMapProps } from '../story-map'; 7 | import { Canvas } from 'react-three-map/maplibre'; 8 | 9 | /** Maplibre `` styled for stories */ 10 | export const StoryMaplibre: FC> = ({ 11 | latitude, longitude, canvas, mapChildren, maplibreChildren, children, ...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
21 | 30 | 31 | {mapChildren} 32 | {maplibreChildren} 33 | 34 | {children} 35 | 36 | 37 |
38 | } 39 | 40 | interface FlyToProps { 41 | latitude: number, 42 | longitude: number, 43 | zoom?: number, 44 | } 45 | 46 | const FlyTo = memo(({ latitude, longitude, zoom }) => { 47 | 48 | const map = useMap(); 49 | const firstRun = useRef(true); 50 | 51 | useEffect(() => { 52 | if (!map.current) return; 53 | if (firstRun.current) return; 54 | map.current.easeTo({ 55 | center: { lon: longitude, lat: latitude }, 56 | zoom: map.current.getZoom(), 57 | duration: 0, 58 | }) 59 | }, [map, latitude, longitude]) 60 | 61 | useEffect(() => { 62 | if (!map.current) return; 63 | if (firstRun.current) return; 64 | if (zoom === undefined) return; 65 | map.current.easeTo({ 66 | center: map.current.getCenter(), 67 | zoom, 68 | essential: true, 69 | }) 70 | }, [map, zoom]) 71 | 72 | useEffect(() => { 73 | firstRun.current = false; 74 | }, []) 75 | 76 | return <> 77 | }) 78 | FlyTo.displayName = 'FlyTo'; -------------------------------------------------------------------------------- /stories/src/sunlight/scene.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Plane, useHelper } from "@react-three/drei"; 2 | import { MeshProps, useFrame, useThree } from '@react-three/fiber'; 3 | import { useCallback, useRef, useState } from 'react'; 4 | import { CameraHelper, MathUtils, Mesh, OrthographicCamera } from "three"; 5 | 6 | export function MyScene({ showCamHelper }: { showCamHelper?: boolean }) { 7 | return <> 8 | 9 | 10 | 11 | 12 | 13 | } 14 | 15 | 16 | function MyBox(props: MeshProps) { 17 | const [hovered, hover] = useState(false); 18 | const mesh = useRef(null) 19 | const invalidate = useThree(st => st.invalidate); 20 | 21 | const onOver = useCallback(() => { 22 | hover(true); 23 | }, []) 24 | 25 | const onOut = useCallback(() => { 26 | hover(false); 27 | }, []) 28 | 29 | useFrame((_st, dt) => { 30 | if (!mesh.current) return; 31 | mesh.current.rotateY(dt); 32 | invalidate(); 33 | }) 34 | 35 | return ( 36 | 46 | 51 | 52 | ); 53 | } 54 | function Lights({ showCamHelper }: { showCamHelper?: boolean }) { 55 | const cam = useRef(null); 56 | const noCam = useRef(null); 57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 58 | useHelper((showCamHelper ? cam : noCam) as any, CameraHelper) 59 | const camSize = 100; 60 | return <> 61 | 62 | 68 | 73 | 74 | 75 | 76 | 77 | } 78 | 79 | function Floor() { 80 | return 86 | 87 | 88 | } -------------------------------------------------------------------------------- /example-mapbox/src/my-scene.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Plane, useHelper } from "@react-three/drei"; 2 | import { MeshProps, useFrame, useThree } from '@react-three/fiber'; 3 | import { useCallback, useRef, useState } from 'react'; 4 | import { CameraHelper, MathUtils, Mesh, OrthographicCamera } from "three"; 5 | 6 | export function MyScene({ showCamHelper }: { showCamHelper?: boolean }) { 7 | return <> 8 | 9 | 10 | 11 | 12 | 13 | } 14 | 15 | 16 | function MyBox(props: MeshProps) { 17 | const [hovered, hover] = useState(false); 18 | const mesh = useRef(null) 19 | const invalidate = useThree(st => st.invalidate); 20 | 21 | const onOver = useCallback(() => { 22 | hover(true); 23 | }, []) 24 | 25 | const onOut = useCallback(() => { 26 | hover(false); 27 | }, []) 28 | 29 | useFrame((_st, dt) => { 30 | if (!mesh.current) return; 31 | mesh.current.rotateY(dt); 32 | invalidate(); 33 | }) 34 | 35 | return ( 36 | 46 | 51 | 52 | ); 53 | } 54 | function Lights({ showCamHelper }: { showCamHelper?: boolean }) { 55 | const cam = useRef(null); 56 | const noCam = useRef(null); 57 | useHelper((showCamHelper ? cam : noCam) as any, CameraHelper) // eslint-disable-line @typescript-eslint/no-explicit-any 58 | const camSize = 100; 59 | return <> 60 | 61 | 67 | 73 | 74 | 75 | 76 | 77 | } 78 | 79 | function Floor() { 80 | return 86 | 87 | 88 | } -------------------------------------------------------------------------------- /example-maplibre/src/my-scene.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Plane, useHelper } from "@react-three/drei"; 2 | import { MeshProps, useFrame, useThree } from '@react-three/fiber'; 3 | import { useCallback, useRef, useState } from 'react'; 4 | import { CameraHelper, MathUtils, Mesh, OrthographicCamera } from "three"; 5 | 6 | export function MyScene({ showCamHelper }: { showCamHelper?: boolean }) { 7 | return <> 8 | 9 | 10 | 11 | 12 | 13 | } 14 | 15 | 16 | function MyBox(props: MeshProps) { 17 | const [hovered, hover] = useState(false); 18 | const mesh = useRef(null) 19 | const invalidate = useThree(st => st.invalidate); 20 | 21 | const onOver = useCallback(() => { 22 | hover(true); 23 | }, []) 24 | 25 | const onOut = useCallback(() => { 26 | hover(false); 27 | }, []) 28 | 29 | useFrame((_st, dt) => { 30 | if (!mesh.current) return; 31 | mesh.current.rotateY(dt); 32 | invalidate(); 33 | }) 34 | 35 | return ( 36 | 46 | 51 | 52 | ); 53 | } 54 | function Lights({ showCamHelper }: { showCamHelper?: boolean }) { 55 | const cam = useRef(null); 56 | const noCam = useRef(null); 57 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 58 | useHelper((showCamHelper ? cam : noCam) as any, CameraHelper) 59 | const camSize = 100; 60 | return <> 61 | 62 | 68 | 74 | 75 | 76 | 77 | 78 | } 79 | 80 | function Floor() { 81 | return 87 | 88 | 89 | } -------------------------------------------------------------------------------- /example-mapbox/src/canvas.basic.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame, Vector3 } from "@react-three/fiber"; 2 | import { useControls } from "leva"; 3 | import Mapbox from "mapbox-gl"; 4 | import 'mapbox-gl/dist/mapbox-gl.css'; 5 | import { FC, PropsWithChildren, useRef, useState } from "react"; 6 | import Map from 'react-map-gl/mapbox'; 7 | import { Canvas } from "react-three-map"; 8 | import { Mesh } from "three"; 9 | 10 | export default { title: 'Canvas' } 11 | 12 | const Box: FC<{ position: Vector3 }> = (props) => { 13 | // This reference gives us direct access to the THREE.Mesh object 14 | const ref = useRef(null) 15 | // Hold state for hovered and clicked events 16 | const [hovered, hover] = useState(false) 17 | const [clicked, click] = useState(false) 18 | // Subscribe this component to the render-loop, rotate the mesh every frame 19 | useFrame((_state, delta) => { 20 | if (!ref.current) return; 21 | ref.current.rotation.x += delta; 22 | ref.current.rotation.z -= delta; 23 | }) 24 | // Return the view, these are regular Threejs elements expressed in JSX 25 | return ( 26 | click(!clicked)} 31 | onPointerOver={() => hover(true)} 32 | onPointerOut={() => hover(false)}> 33 | 34 | 35 | 36 | ) 37 | } 38 | 39 | export function BasicExample() { 40 | 41 | const { mapboxToken } = useControls({ 42 | mapboxToken: { 43 | value: import.meta.env.VITE_MAPBOX_TOKEN || '', 44 | label: 'mapbox token', 45 | } 46 | }) 47 | 48 | Mapbox.accessToken = mapboxToken; 49 | 50 | return
51 | {!mapboxToken &&
Add a mapbox token to load this component
} 52 | {mapboxToken && 62 | 63 | 67 | 68 | 69 | 70 | 71 | 72 | } 73 |
74 | } 75 | 76 | const Center = ({ children }: PropsWithChildren) => ( 77 |
{children}
84 | ) -------------------------------------------------------------------------------- /stories/src/canvas/mapbox.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame, Vector3 } from "@react-three/fiber"; 2 | import { useControls } from "leva"; 3 | import MapboxGl from "mapbox-gl"; 4 | import 'mapbox-gl/dist/mapbox-gl.css'; 5 | import { FC, PropsWithChildren, useRef, useState } from "react"; 6 | import Map from 'react-map-gl/mapbox'; 7 | import { Canvas } from "react-three-map"; 8 | import { Mesh } from "three"; 9 | 10 | export default { title: 'Canvas' } 11 | 12 | export function Mapbox() { 13 | 14 | const { mapboxToken } = useControls({ 15 | mapboxToken: { 16 | value: import.meta.env.VITE_MAPBOX_TOKEN || '', 17 | label: 'mapbox token', 18 | } 19 | }) 20 | 21 | MapboxGl.accessToken = mapboxToken; 22 | 23 | return
24 | {!mapboxToken &&
Add a mapbox token to load this component
} 25 | {!!mapboxToken && 35 | 36 | 41 | 42 | 43 | 44 | 45 | 46 | } 47 |
48 | } 49 | 50 | const Center = ({ children }: PropsWithChildren) => ( 51 |
{children}
58 | ) 59 | 60 | const Box: FC<{ position: Vector3 }> = (props) => { 61 | // This reference gives us direct access to the THREE.Mesh object 62 | const ref = useRef(null) 63 | // Hold state for hovered and clicked events 64 | const [hovered, hover] = useState(false) 65 | const [clicked, click] = useState(false) 66 | // Subscribe this component to the render-loop, rotate the mesh every frame 67 | useFrame((_state, delta) => { 68 | if (!ref.current) return; 69 | ref.current.rotation.x += delta; 70 | ref.current.rotation.z -= delta; 71 | }) 72 | // Return the view, these are regular Threejs elements expressed in JSX 73 | return ( 74 | click(!clicked)} 79 | onPointerOver={() => hover(true)} 80 | onPointerOut={() => hover(false)}> 81 | 82 | 83 | 84 | ) 85 | } -------------------------------------------------------------------------------- /stories/src/multi-coordinates.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@react-three/drei"; 2 | import { Vector3 } from "@react-three/fiber"; 3 | import { levaStore, useControls } from "leva"; 4 | import { FC, useEffect, useState } from "react"; 5 | import { Coordinates, CoordinatesProps, NearCoordinates } from "react-three-map"; 6 | import { ColorRepresentation } from "three"; 7 | import { StoryMap } from "./story-map"; 8 | 9 | enum CoordinatesType { 10 | NearCoordinates = 'NearCoordinates', 11 | Coordinates = 'Coordinates', 12 | } 13 | 14 | export function Default() { 15 | 16 | const { blue, green, purple, scale } = useControls({ 17 | scale: 1, 18 | blue: { 19 | value: [-0.1261, 51.508775], 20 | pad: 6, 21 | step: 0.000001, 22 | }, 23 | green: { 24 | value: [-0.1261, 51.508775], 25 | pad: 6, 26 | step: 0.000001, 27 | }, 28 | purple: { 29 | value: [-0.1261, 51.508756], 30 | pad: 6, 31 | step: 0.000001, 32 | }, 33 | }) 34 | 35 | useEffect(()=>{ 36 | // default this story to not use overlay 37 | levaStore.setValueAtPath('overlay', false, true); 38 | }, []) 39 | 40 | return 46 | 47 | 51 | 52 | 53 | 57 | 58 | 59 | 60 | } 61 | 62 | const CoordsControl : FC = (props) => { 63 | const {coords} = useControls({ 64 | coords: { 65 | value: CoordinatesType.Coordinates, 66 | options: CoordinatesType 67 | } 68 | }) 69 | 70 | return <> 71 | {coords === CoordinatesType.Coordinates && } 72 | {coords === CoordinatesType.NearCoordinates && } 73 | 74 | } 75 | 76 | const MyBox = ({ position, color, scale }: { 77 | position?: Vector3, 78 | scale: number, 79 | color: ColorRepresentation 80 | }) => { 81 | const [hovered, hover] = useState(false); 82 | 83 | scale *= hovered ? 1.5 : 1; 84 | const height = 7; 85 | 86 | return 87 | hover(!hovered)} 91 | onPointerOver={() => hover(true)} 92 | onPointerOut={() => hover(false)} 93 | scale={scale} 94 | material-color={color} 95 | /> 96 | 97 | } -------------------------------------------------------------------------------- /stories/src/free-3d-buildings/buildings-3d.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ActionType, ThemeState, useLadleContext } from '@ladle/react'; 2 | import { Bloom, EffectComposer } from '@react-three/postprocessing'; 3 | import { levaStore, useControls } from "leva"; 4 | import { Suspense, useEffect } from "react"; 5 | import { Coords } from "react-three-map"; 6 | import { ScreenBlend } from "../screen-blend-effect/screen-blend"; 7 | import { StoryMap } from "../story-map"; 8 | import { BatchedBuildings } from "./batched-buildings"; 9 | import { AdaptiveDpr } from '../adaptive-dpr'; 10 | 11 | const coords: Coords = { latitude: 51.5074, longitude: -0.1278 }; 12 | 13 | export function Default() { 14 | const { dispatch, globalState } = useLadleContext(); 15 | const { bloom } = useControls({ bloom: { value: true } }); 16 | 17 | // disable showBuildings3D control from Mapbox 18 | useControls({ showBuildings3D: { value: false, render: () => false } }); 19 | const { luminanceThreshold, levels, intensity, luminanceSmoothing } = useControls('bloom', { 20 | levels: { value: 3, min: 0, max: 10, step: 0.01 }, 21 | intensity: { value: 1.62, min: 0, max: 2, step: 0.01 }, 22 | luminanceThreshold: { value: .1, min: 0, max: 2, step: 0.01, label: 'threshold' }, 23 | luminanceSmoothing: { value: 2, min: 0, max: 5, step: 0.01, label: 'smoothing' }, 24 | }) 25 | 26 | // use dark theme 27 | useEffect(() => { 28 | const prevTheme = globalState.theme 29 | dispatch({ type: ActionType.UpdateTheme, value: ThemeState.Dark }) 30 | return () => { 31 | // reset theme 32 | dispatch({ type: ActionType.UpdateTheme, value: prevTheme }) 33 | } 34 | }, []) // eslint-disable-line react-hooks/exhaustive-deps 35 | 36 | // default this story to use overlay 37 | useEffect(() => { 38 | const overlay = levaStore.get('overlay'); 39 | levaStore.setValueAtPath('overlay', true, true); 40 | return () => { 41 | // reset overlay 42 | if (overlay) return; 43 | levaStore.setValueAtPath('overlay', overlay, true); 44 | } 45 | }, []) 46 | 47 | return 53 | 54 | {bloom && 55 | 61 | {/* ScreenBlend forces transparency to work on the canvas overlay */} 62 | 63 | } 64 | 65 | 66 | 67 | 68 | 69 | 70 | } -------------------------------------------------------------------------------- /stories/src/mapbox/story-mapbox.tsx: -------------------------------------------------------------------------------- 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, PropsWithChildren, memo } from "react"; 6 | import Map, { Layer } from 'react-map-gl/mapbox'; 7 | import { Canvas } from 'react-three-map'; 8 | import { StoryMapProps } from '../story-map'; 9 | 10 | /** `` styled for stories */ 11 | export const StoryMapbox: FC> = ({ 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
38 | {!mapboxToken &&
Add a mapbox token to load this component
} 39 | {!!mapboxToken && 46 | {mapChildren} 47 | {mapboxChildren} 48 | 49 | {children} 50 | 51 | {showBuildings3D && } 52 | } 53 |
54 | } 55 | 56 | const Center = ({ children }: PropsWithChildren) => ( 57 |
{children}
64 | ) 65 | 66 | const Buildings3D = memo(() => { 67 | return 96 | }) 97 | Buildings3D.displayName = 'Buildings3D' -------------------------------------------------------------------------------- /stories/src/my-scene.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Plane, useHelper } from "@react-three/drei"; 2 | import { MeshProps, useFrame, useThree } from '@react-three/fiber'; 3 | import { useCallback, useRef, useState } from 'react'; 4 | import { CameraHelper, MathUtils, Mesh, OrthographicCamera } from "three"; 5 | 6 | export function MyScene({ showCamHelper, animate }: { 7 | showCamHelper?: boolean, 8 | animate?: boolean, 9 | }) { 10 | return <> 11 | 12 | 13 | 14 | 15 | 16 | } 17 | 18 | 19 | function MyBox({ animate, ...props }: MeshProps & { animate?: boolean }) { 20 | const [hovered, hover] = useState(false); 21 | const mesh = useRef(null) 22 | const invalidate = useThree(st => st.invalidate); 23 | const events = useThree(st => st.events); 24 | 25 | const onOver = useCallback(() => { 26 | hover(true); 27 | }, []) 28 | 29 | const onOut = useCallback(() => { 30 | hover(false); 31 | }, []) 32 | 33 | useFrame((_st, dt) => { 34 | if (!animate) return 35 | if (!mesh.current) return; 36 | mesh.current.rotateY(dt); 37 | invalidate(); 38 | if (events.update) events.update(); 39 | }) 40 | 41 | return ( 42 | 51 | 56 | 57 | ); 58 | } 59 | function Lights({ showCamHelper }: { showCamHelper?: boolean }) { 60 | const cam = useRef(null); 61 | const noCam = useRef(null); 62 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 63 | useHelper((showCamHelper ? cam : noCam) as any, CameraHelper) 64 | const camSize = 100; 65 | return <> 66 | 67 | 73 | 78 | 79 | 80 | 81 | 82 | 83 | } 84 | 85 | function Floor() { 86 | return 92 | 93 | 94 | } -------------------------------------------------------------------------------- /stories/src/pivot-controls.stories.tsx: -------------------------------------------------------------------------------- 1 | import { PivotControls, ScreenSizer, Sphere } from "@react-three/drei"; 2 | import { useControls } from "leva"; 3 | import { FC, useCallback, useEffect, useMemo, useState } from "react"; 4 | import { Marker as MapboxMarker } from "react-map-gl/mapbox"; 5 | import { Marker as MaplibreMarker } from "react-map-gl/maplibre"; 6 | import { useMap, 7 | vector3ToCoords 8 | } from "react-three-map"; 9 | import { Matrix4, Vector3, Vector3Tuple } from "three"; 10 | import { StoryMap } from "./story-map"; 11 | 12 | export function Default() { 13 | const origin = useControls({ 14 | latitude: { value: 51, min: -90, max: 90 }, 15 | longitude: { value: 0, min: -180, max: 180 } 16 | }) 17 | const [position, setPosition] = useState([0, 0, 0]); 18 | const geoPos = useMemo(() => vector3ToCoords(position, origin), [position, origin]) 19 | 20 | // reset on origin change 21 | useEffect(() => setPosition([0, 0, 0]), [origin]) // eslint-disable-line react-hooks/exhaustive-deps 22 | 23 | return
24 | 30 |
lat: {geoPos.latitude}
lon: {geoPos.longitude}
31 | 32 | )} 33 | mapboxChildren={( 34 | 35 |
lat: {geoPos.latitude}
lon: {geoPos.longitude}
36 |
37 | )} 38 | > 39 | 40 | 41 | 46 | 47 | 48 |
49 |
50 | } 51 | 52 | interface MovingBoxProps { 53 | position: Vector3Tuple, 54 | setPosition: (pos: Vector3Tuple) => void 55 | } 56 | 57 | const _v3 = new Vector3() 58 | 59 | const Move: FC = ({ position, setPosition }) => { 60 | const matrix = useMemo(() => new Matrix4().setPosition(...position), [position]); 61 | const map = useMap(); 62 | const onDragStart = useCallback(() => { 63 | map.dragPan.disable(); 64 | map.dragRotate.disable(); 65 | }, [map]); 66 | const onDragEnd = useCallback(() => { 67 | map.dragPan.enable(); 68 | map.dragRotate.enable(); 69 | }, [map]); 70 | const onDrag = useCallback((m4: Matrix4) => { 71 | setPosition(_v3.setFromMatrixPosition(m4).toArray()); 72 | }, [setPosition]) 73 | return ( 74 | 84 | ) 85 | } -------------------------------------------------------------------------------- /stories/src/free-3d-buildings/batched-standard-material/batched-properties-texture.ts: -------------------------------------------------------------------------------- 1 | import { DataTexture, FloatType, RGBAFormat } from "three" 2 | 3 | export class BatchedPropertiesTexture extends DataTexture { 4 | 5 | private fields: { 6 | type: string; 7 | subtype: string; 8 | dim: number; 9 | comp: string | undefined; 10 | name: string 11 | }[] 12 | 13 | private fieldToIndex: Record; 14 | 15 | constructor(params: Record, count: number) { 16 | const fields = Object.entries(params) 17 | .map(([name, type]) => ({ 18 | name, 19 | ...parseToInfo(type) 20 | })) 21 | .sort((a, b) => { 22 | return a.dim - b.dim 23 | }) 24 | const width = fields.length 25 | let size = Math.sqrt(count * width) 26 | size = Math.ceil(size / width) * width 27 | size = Math.max(size, width) 28 | const fieldToIndex : Record = {} 29 | for (let i = 0, l = fields.length; i < l; i++) { 30 | fieldToIndex[fields[i].name] = i 31 | } 32 | super(new Float32Array(size * size * 4), size, size, RGBAFormat, FloatType) 33 | this.fields = fields 34 | this.fieldToIndex = fieldToIndex 35 | } 36 | 37 | setValue(id: number, name: string, ...values: number[]) { 38 | const { fields, fieldToIndex, image } = this 39 | const width = fields.length 40 | if (!(name in fieldToIndex)) return 41 | const fieldId = fieldToIndex[name] 42 | const field = fields[fieldId] 43 | const dim = field.dim 44 | const data = image.data 45 | const offset = id * width * 4 + fieldId * 4 46 | for (let i = 0; i < dim; i++) data[offset + i] = values[i] || 0 47 | this.needsUpdate = true 48 | } 49 | 50 | getGlsl(idField = 'vBatchId', textureName = 'propertiesTex', indent = '') { 51 | const { fields, image } = this 52 | const size = image.width 53 | const width = fields.length 54 | let result = 55 | `${indent}int size = ${size};\n` + 56 | `${indent}int j = int( ${idField} ) * ${width};\n` + 57 | `${indent}int x = j % size;\n` + 58 | `${indent}int y = j / size;\n` 59 | for (let i = 0, l = fields.length; i < l; i++) { 60 | const { name, type, comp } = fields[i] 61 | result += `${indent}${type} ${name} = ${type}( texelFetch( ${textureName}, ivec2( x + ${i}, y ), 0 ).${comp} );\n` 62 | } 63 | return result 64 | } 65 | } 66 | 67 | function parseToInfo(type: string) { 68 | let subtype = type 69 | const dim = parseFloat(type.replace(/[^1-3]/g, '')) || 1 70 | if (/$vec/.test(type)) subtype = 'float' 71 | if (/$uvec/.test(type)) subtype = 'uint' 72 | if (/$ivec/.test(type)) subtype = 'int' 73 | 74 | let comp 75 | switch (dim) { 76 | case 1: 77 | comp = 'r' 78 | break 79 | case 2: 80 | comp = 'rg' 81 | break 82 | case 3: 83 | comp = 'rgb' 84 | break 85 | case 4: 86 | comp = 'rgba' 87 | break 88 | } 89 | 90 | return { type, subtype, dim, comp } 91 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-three-map", 3 | "version": "1.0.0", 4 | "description": "Use react-three-fiber inside MapLibre and Mapbox", 5 | "main": "dist/cjs/main.js", 6 | "module": "dist/es/main.mjs", 7 | "types": "dist/types/mapbox.index.d.ts", 8 | "sideEffects": false, 9 | "repository": "https://github.com/RodrigoHamuy/react-three-map", 10 | "homepage": "https://github.com/RodrigoHamuy/react-three-map", 11 | "bugs": { 12 | "url": "https://github.com/RodrigoHamuy/react-three-map/issues" 13 | }, 14 | "license": "MIT", 15 | "author": "RodrigoHamuy", 16 | "scripts": { 17 | "prebuild": "tsc --project tsconfig.mapbox.json && tsc --project tsconfig.maplibre.json", 18 | "build": "yarn build:maplibre && yarn build:mapbox", 19 | "build:maplibre": "cross-env LIB_MODE=1 MAP_MODE=0 vite build", 20 | "postbuild:maplibre": "cross-env LIB_MODE=2 MAP_MODE=0 vite build", 21 | "build:mapbox": "cross-env LIB_MODE=1 MAP_MODE=1 vite build", 22 | "postbuild:mapbox": "cross-env LIB_MODE=2 MAP_MODE=1 vite build", 23 | "release": "yarn build && yarn changeset publish", 24 | "lint": "eslint src example-maplibre/src example-mapbox/src stories --ext ts,tsx --fix", 25 | "preview": "vite preview", 26 | "ts": "tsc -w", 27 | "ts:check": "tsc", 28 | "dev": "yarn ladle serve", 29 | "build:stories": "yarn ladle build", 30 | "test": "yarn vitest" 31 | }, 32 | "dependencies": {}, 33 | "devDependencies": { 34 | "@changesets/cli": "^2.26.1", 35 | "@ladle/react": "^4.0.2", 36 | "@react-three/drei": "^9.97.6", 37 | "@react-three/fiber": "^8.15.12", 38 | "@react-three/postprocessing": "^2.15.11", 39 | "@types/luxon": "^3.3.1", 40 | "@types/node": "^20.3.1", 41 | "@types/react": "^18.0.37", 42 | "@types/react-dom": "^18.0.11", 43 | "@types/suncalc": "^1.9.0", 44 | "@types/three": "^0.159.0", 45 | "@types/tz-lookup": "^6.1.0", 46 | "@typescript-eslint/eslint-plugin": "^5.59.11", 47 | "@typescript-eslint/parser": "^5.59.11", 48 | "@vitejs/plugin-react": "^4.0.0", 49 | "cross-env": "^7.0.3", 50 | "eslint": "^8.38.0", 51 | "eslint-plugin-react-hooks": "^4.6.0", 52 | "eslint-plugin-react-refresh": "^0.3.4", 53 | "happy-dom": "^10.5.2", 54 | "leva": "^0.9.35", 55 | "luxon": "^3.3.0", 56 | "mapbox-gl": "^3.9.4", 57 | "maplibre-gl": "^5.4.0", 58 | "react": "^18.2.0", 59 | "react-dom": "^18.2.0", 60 | "react-map-gl": "^8.0.1", 61 | "suncalc": "^1.9.0", 62 | "suspend-react": "^0.1.3", 63 | "three": "^0.159.0", 64 | "three-stdlib": "^2.28.7", 65 | "typescript": "~5.0.2", 66 | "tz-lookup": "^6.1.25", 67 | "vite": "^4.4.4", 68 | "vitest": "^0.33.0", 69 | "web-ifc-three": "^0.0.125" 70 | }, 71 | "peerDependencies": { 72 | "@react-three/fiber": ">=8.13", 73 | "mapbox-gl": ">=3.5.0", 74 | "maplibre-gl": ">=4.0.0", 75 | "react": ">=18.0", 76 | "react-map-gl": ">=8.0.0", 77 | "three": ">=0.133" 78 | }, 79 | "peerDependenciesMeta": { 80 | "mapbox-gl": { 81 | "optional": true 82 | }, 83 | "maplibre-gl": { 84 | "optional": true 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/core/canvas-overlay/sync-camera-fc.tsx: -------------------------------------------------------------------------------- 1 | import { useFrame, useThree } from "@react-three/fiber"; 2 | import { memo, useEffect, useLayoutEffect, useMemo, useRef } from "react"; 3 | import { Matrix4Tuple, PerspectiveCamera } from "three"; 4 | import { Coords } from "../../api/coords"; 5 | import { MapInstance } from "../generic-map"; 6 | import { syncCamera } from "../sync-camera"; 7 | import { useCoordsToMatrix } from "../use-coords-to-matrix"; 8 | import { useFunction } from "../use-function"; 9 | import { useR3M } from "../use-r3m"; 10 | 11 | interface SyncCameraFCProps extends Coords { 12 | setOnRender?: (callback: () => (mx: Matrix4Tuple) => void) => void, 13 | /** on `useFrame` it will manually render (used by ``) */ 14 | manualRender?: boolean, 15 | onReady?: () => void, 16 | map: MapInstance, 17 | } 18 | 19 | /** React Component (FC) to sync the Three camera with the map provider */ 20 | export const SyncCameraFC = memo(({ 21 | latitude, longitude, altitude = 0, setOnRender, manualRender, onReady, map 22 | }) => { 23 | 24 | const mapCanvas = map.getCanvas(); 25 | 26 | const r3m = useR3M(); 27 | 28 | const camRef = useRef(null); 29 | 30 | const camera = useThree(s => s.camera) as PerspectiveCamera; 31 | const gl = useThree(s => s.gl); 32 | const threeCanvas = useThree(s => s.gl.domElement); 33 | const scene = useThree(s => s.scene); 34 | const advance = useThree(s => s.advance); 35 | const setSize = useThree(s => s.setSize); 36 | const set = useThree(s => s.set); 37 | 38 | const origin = useCoordsToMatrix({ latitude, longitude, altitude, fromLngLat: r3m.fromLngLat }); 39 | 40 | const ready = useRef(false); 41 | 42 | const triggerRepaint = useMemo(() => map.triggerRepaint, [map]); 43 | const mapPaintRequests = useRef(0); 44 | const triggerRepaintOff = useFunction(() => { 45 | mapPaintRequests.current++; 46 | }) 47 | 48 | useFrame(() => { 49 | syncCamera(camera, origin, r3m.viewProjMx) 50 | 51 | if (manualRender) gl.render(scene, camera); 52 | 53 | map.triggerRepaint = triggerRepaint; 54 | if (mapPaintRequests.current > 0) { 55 | mapPaintRequests.current = 0; 56 | map.triggerRepaint(); 57 | } 58 | }, -Infinity) 59 | 60 | const onRender = useFunction((viewProjMx: Matrix4Tuple | {defaultProjectionData: {mainMatrix: Record}}) => { 61 | map.triggerRepaint = triggerRepaintOff; 62 | 63 | if (threeCanvas.width !== mapCanvas.width || threeCanvas.height !== mapCanvas.height) { 64 | setSize( 65 | mapCanvas.clientWidth, 66 | mapCanvas.clientHeight, 67 | true, 68 | mapCanvas.offsetTop, 69 | mapCanvas.offsetLeft, 70 | ); 71 | } 72 | 73 | const pVMx = 'defaultProjectionData' in viewProjMx ? Object.values(viewProjMx.defaultProjectionData.mainMatrix) : viewProjMx; 74 | r3m.viewProjMx = pVMx as Matrix4Tuple; 75 | if (!ready.current && onReady) { 76 | ready.current = true; 77 | onReady(); 78 | } 79 | advance(Date.now() * .001, true); 80 | }) 81 | 82 | useEffect(() => { 83 | setOnRender && setOnRender(() => onRender) 84 | }, [setOnRender, onRender]) 85 | 86 | useLayoutEffect(() => { 87 | if (!manualRender) return; 88 | set({ camera: camRef.current! }); // eslint-disable-line @typescript-eslint/no-non-null-assertion 89 | }, []) // eslint-disable-line react-hooks/exhaustive-deps 90 | 91 | return <> 92 | {manualRender && } 97 | 98 | }) 99 | 100 | SyncCameraFC.displayName = 'SyncCameraFC'; -------------------------------------------------------------------------------- /src/core/canvas-in-layer/use-root.tsx: -------------------------------------------------------------------------------- 1 | import { _roots, createRoot } from "@react-three/fiber"; 2 | import { useEffect, useState } from "react"; 3 | import { CanvasProps } from "../../api/canvas-props"; 4 | import { events } from "../events"; 5 | import { FromLngLat, MapInstance } from "../generic-map"; 6 | import { setCoords, useSetRootCoords } from "../use-coords"; 7 | import { useFunction } from "../use-function"; 8 | import { initR3M } from "../use-r3m"; 9 | 10 | export function useRoot( 11 | fromLngLat: FromLngLat, 12 | map: MapInstance, 13 | { frameloop, longitude, latitude, altitude, ...props }: CanvasProps 14 | ) { 15 | 16 | const [{ root, useThree, canvas, r3m }] = useState(() => { 17 | const canvas = map.getCanvas(); 18 | const gl = (canvas.getContext('webgl2') || canvas.getContext('webgl')) as WebGLRenderingContext; 19 | 20 | const root = createRoot(canvas); 21 | root.configure({ 22 | dpr: window.devicePixelRatio, 23 | events, 24 | ...props, 25 | frameloop: 'never', 26 | gl: { 27 | context: gl, 28 | autoClear: false, 29 | antialias: true, 30 | ...props?.gl, 31 | }, 32 | onCreated: (state) => { 33 | state.gl.forceContextLoss = () => { }; // eslint-disable-line @typescript-eslint/no-empty-function 34 | }, 35 | camera: { 36 | matrixAutoUpdate: false, 37 | near: 0, 38 | }, 39 | size: { 40 | width: canvas.clientWidth, 41 | height: canvas.clientHeight, 42 | top: canvas.offsetTop, 43 | left: canvas.offsetLeft, 44 | updateStyle: false, 45 | ...props?.size, 46 | }, 47 | }); 48 | 49 | const store = _roots.get(canvas)!.store; // eslint-disable-line @typescript-eslint/no-non-null-assertion 50 | 51 | const r3m = initR3M({ map, fromLngLat, store }); 52 | setCoords(store, {longitude, latitude, altitude}); 53 | 54 | if (frameloop === 'demand') { 55 | store.setState({ 56 | frameloop, 57 | invalidate: () => { 58 | map.triggerRepaint(); 59 | }, 60 | }) 61 | } 62 | 63 | return { root, useThree: store, map, canvas, r3m } 64 | 65 | }) 66 | 67 | const onResize = useFunction(() => { 68 | 69 | const { setDpr, setSize } = useThree.getState(); 70 | 71 | setDpr(window.devicePixelRatio); 72 | 73 | setSize( 74 | canvas.clientWidth, 75 | canvas.clientHeight, 76 | false, 77 | canvas.offsetTop, 78 | canvas.offsetLeft, 79 | ); 80 | 81 | }) 82 | 83 | const onRemove = useFunction(() => { 84 | root.unmount(); 85 | }) 86 | 87 | useSetRootCoords(useThree, {longitude, latitude, altitude}); 88 | 89 | // on `frameloop` change 90 | useEffect(() => { 91 | if (frameloop !== 'demand') return; 92 | const setState = useThree.setState; 93 | const { invalidate } = useThree.getState(); 94 | setState({ 95 | frameloop, 96 | invalidate: () => { 97 | map.triggerRepaint(); 98 | } 99 | }); 100 | return () => { 101 | setState({ frameloop: 'never', invalidate }) 102 | } 103 | }, [frameloop]) // eslint-disable-line react-hooks/exhaustive-deps 104 | 105 | // on mount / unmount 106 | useEffect(() => { 107 | map.on('resize', onResize); 108 | return () => { 109 | map.off('resize', onResize) 110 | } 111 | }, []) // eslint-disable-line react-hooks/exhaustive-deps 112 | 113 | // root.render 114 | useEffect(() => { 115 | root.render(<> 116 | {props.children} 117 | ); 118 | }, [props.children]) // eslint-disable-line react-hooks/exhaustive-deps 119 | 120 | return { onRemove, useThree, r3m }; 121 | } -------------------------------------------------------------------------------- /stories/src/ifc/load-ifc-model.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Plane } from "@react-three/drei"; 2 | import { button, folder, useControls } from "leva"; 3 | import { Suspense, useCallback, useState } from "react"; 4 | import { suspend } from 'suspend-react'; 5 | import { MathUtils } from "three"; 6 | import { IFCModel } from "web-ifc-three/IFC/components/IFCModel"; 7 | import { IFCLoader } from "web-ifc-three/IFCLoader"; 8 | import { StoryMap } from "../story-map"; 9 | import modelUrl from './model.ifc?url'; 10 | 11 | export function Default() { 12 | 13 | const [path, setPath] = useState(modelUrl); 14 | 15 | const loadIfcClick = useCallback(async () => { 16 | try { 17 | setPath(await getLocalFileUrl()); 18 | } catch (error) { 19 | console.warn(error); 20 | } 21 | }, []) 22 | 23 | useControls({ 24 | 'load IFC file': button(() => loadIfcClick()) 25 | }) 26 | 27 | const { latitude, longitude, position, rotation, scale } = useControls({ 28 | coords: folder({ 29 | latitude: { 30 | value: 51.508775, 31 | pad: 6, 32 | }, 33 | longitude: { 34 | value: -0.1261, 35 | pad: 6, 36 | }, 37 | }), 38 | position: { 39 | value: {x: 0, y: .32, z: 0}, 40 | step: 1, 41 | pad: 2, 42 | }, 43 | rotation: { 44 | value: 0, 45 | step: 1, 46 | }, 47 | scale: 1, 48 | }) 49 | 50 | return 51 | 52 | 58 | 59 | 60 | 61 | }> 66 | 67 | 68 | 69 | 70 | } 71 | 72 | function Lights() { 73 | const camSize = 50; 74 | return <> 75 | 76 | 82 | 86 | 87 | 88 | 89 | 90 | } 91 | 92 | interface IfcModelProps { 93 | path: string 94 | } 95 | 96 | function IfcModel({ path }: IfcModelProps) { 97 | const model = useIFC(path); 98 | return <> 99 | 100 | 101 | } 102 | 103 | function useIFC(path: string) { 104 | const m = suspend(() => loadIFC(path), [path]); 105 | return m; 106 | } 107 | 108 | function loadIFC(path: string) { 109 | return new Promise((resolve, reject) => { 110 | const loader = new IFCLoader(); 111 | loader.load(path, e => { 112 | e.castShadow = true; 113 | resolve(e) 114 | }, undefined, reject); 115 | }) 116 | } 117 | 118 | async function getLocalFileUrl() { 119 | return new Promise((resolve) => { 120 | const onChange = (e: Event) => { 121 | if (!(e.target instanceof HTMLInputElement) || !e.target.files) return; 122 | const file = e.target.files[0]; 123 | if (!file) return; 124 | const url = URL.createObjectURL(file); 125 | resolve(url); 126 | }; 127 | const input = document.createElement('input'); 128 | input.type = 'file'; 129 | input.addEventListener('change', onChange); 130 | input.accept = '.ifc'; 131 | input.click(); 132 | }); 133 | } 134 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # react-three-map 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - 9be6c41: Updated to work with `react-map-gl >= 8.0.0`. 8 | 9 | This has the cascade effect of requiring also to upgrade either of the map providers you are using: 10 | 11 | - `mapbox-gl >= 3.5.0` 12 | - `maplibre-gl >= 4.0.0` 13 | 14 | ### Patch Changes 15 | 16 | - e5e937a: Remove `react-map-gl/mapbox` from the build. 17 | - 987d5c7: Fix `overlay` camera calculations bug introduced by `three=>r166`. More info in [#143](https://github.com/RodrigoHamuy/react-three-map/issues/143). 18 | - 55c5392: Support maplibre@v5 19 | 20 | ## 1.0.0-next.2 21 | 22 | ### Patch Changes 23 | 24 | - 987d5c7: Fix `overlay` camera calculations bug introduced by `three=>r166`. More info in [#143](https://github.com/RodrigoHamuy/react-three-map/issues/143). 25 | 26 | ## 1.0.0-next.1 27 | 28 | ### Patch Changes 29 | 30 | - e5e937a: Remove `react-map-gl/mapbox` from the build. 31 | 32 | ## 1.0.0-next.0 33 | 34 | ### Major Changes 35 | 36 | - 9be6c41: Updated to work with `react-map-gl >= 8.0.0`. 37 | 38 | This has the cascade effect of requiring also to upgrade either of the map providers you are using: 39 | 40 | - `mapbox-gl >= 3.5.0` 41 | - `maplibre-gl >= 4.0.0` 42 | 43 | ## 0.8.2 44 | 45 | ### Patch Changes 46 | 47 | - 58c42d9: - Improve accuracy at long distances for `coordsToVector3`. 48 | - Improve translation accuracy for `NearCoordinates`, but scale is still ignored. 49 | - Update `earthRadius` from `6378137` to `6371008.8` to match [Mapbox](https://github.com/maplibre/maplibre-gl-js/blob/8ea76118210dd18fa52fdb83f2cbdd1229807346/src/geo/lng_lat.ts#L8) and [Maplibre](https://github.com/maplibre/maplibre-gl-js/blob/main/src/geo/lng_lat.ts#L8). 50 | 51 | ## 0.8.1 52 | 53 | ### Patch Changes 54 | 55 | - 9dfe4f6: Remove `turf` dependency. 56 | - 9dfe4f6: Map to wait for r3f to finish rendering before the next render in overlay mode. 57 | 58 | ## 0.8.0 59 | 60 | ### Minor Changes 61 | 62 | - 7e1743d: - Peer dependency upgrade: `maplibre-gl`: `>=3.2`, so we don't need to sort `maplibre` old DPR bug anymore. 63 | - Add `overlay?: boolean` to ``, so you can render on a separated canvas if preferred. 64 | - Add `NearCoordinates` component. 65 | - Add `coordsToVector3` function. 66 | 67 | ## 0.7.2 68 | 69 | ### Patch Changes 70 | 71 | - 90a4766: Fix bug on DPR change. 72 | 73 | ## 0.7.1 74 | 75 | ### Patch Changes 76 | 77 | - 2115617: Revert 748d7a7: Fix issues on DPR or browser zoom changes. 78 | 79 | ## 0.7.0 80 | 81 | ### Minor Changes 82 | 83 | - 1c52c0f: `` props accepts `id?: string` and `beforeId?: string` to set the MapLibre/Mapbox layer. 84 | 85 | ### Patch Changes 86 | 87 | - 748d7a7: Fix issues on DPR or browser zoom changes. 88 | 89 | ## 0.6.3 90 | 91 | ### Patch Changes 92 | 93 | - a5b94b6: Fix ThreeJS sync on Mapbox resize. 94 | 95 | ## 0.6.2 96 | 97 | ### Patch Changes 98 | 99 | - 6be61b6: Fix on window DPR changes (different solutions for Mapbox and Maplibre) 100 | 101 | ## 0.6.1 102 | 103 | ### Patch Changes 104 | 105 | - be1efa7: Fix resizing bug when DPR changes. 106 | 107 | ## 0.6.0 108 | 109 | ### Minor Changes 110 | 111 | - 3a9a852: Add `useMap` hook to access the map from react-three-map. 112 | 113 | ### Patch Changes 114 | 115 | - 3a9a852: Better type for `useMap`. 116 | 117 | ## 0.5.0 118 | 119 | ### Minor Changes 120 | 121 | - 7921f43: Add `useMap` hook to access the map from react-three-map. 122 | 123 | ## 0.4.3 124 | 125 | ### Patch Changes 126 | 127 | - 0e37c1c: Fix canvas on resize. 128 | 129 | ## 0.4.2 130 | 131 | ### Patch Changes 132 | 133 | - bd62ef9: Simplify raycaster calculations. 134 | 135 | ## 0.4.1 136 | 137 | ### Patch Changes 138 | 139 | - 41dd225: Improve camera decomposition so the camera matrix and world matrix have more useful values. 140 | 141 | ## 0.4.0 142 | 143 | ### Minor Changes 144 | 145 | - bad8670: Add `` component to render multiple 3D objects at different coordiantes. 146 | 147 | ## 0.3.5 148 | 149 | ### Patch Changes 150 | 151 | - 9b1c068: Fix camera matrix calculations. 152 | 153 | ## 0.3.4 154 | 155 | ### Patch Changes 156 | 157 | - 4dd8a72: Fix types declaration path. 158 | 159 | ## 0.3.3 160 | 161 | ### Patch Changes 162 | 163 | - 7e94458: Generate types declarations. 164 | 165 | ## 0.3.2 166 | 167 | ### Patch Changes 168 | 169 | - b169844: Bug fix related to DPR devices and pointer events. 170 | 171 | ## 0.3.1 172 | 173 | ### Patch Changes 174 | 175 | - 39acb3b: Fix build typo and add more detailed peer dependencies. 176 | 177 | ## 0.3.0 178 | 179 | ### Minor Changes 180 | 181 | - f3155c6: Upgrade to `react-map-gl@7.1.0`, which changes how to use Maplibre. Find more in their [changelog](https://github.com/visgl/react-map-gl/releases/tag/v7.1.0). 182 | 183 | ## 0.2.1 184 | 185 | ### Patch Changes 186 | 187 | - 71d6439: Fix maplibre build 188 | 189 | ## 0.2.0 190 | 191 | ### Minor Changes 192 | 193 | - b210a12: Support to render `react-three-map` on demand via `` 194 | - 557920a: Add Mapbox support. 195 | 196 | - If you use **Mapbox** `import { Canvas } from "react-three-map"` 197 | - If you use **Maplibre** `import { Canvas } from "react-three-map/maplibre"` 198 | 199 | ### Patch Changes 200 | 201 | - 83de85c: Fix camera matrix bug where it may have invalid state on start. 202 | -------------------------------------------------------------------------------- /stories/src/free-3d-buildings/batched-buildings.tsx: -------------------------------------------------------------------------------- 1 | import { Object3DNode, extend, useFrame } from "@react-three/fiber"; 2 | import { memo, useLayoutEffect, useMemo, useRef, useState } from "react"; 3 | import { Coords, coordsToVector3 } from "react-three-map"; 4 | import { suspend } from "suspend-react"; 5 | import { BatchedMesh, Color, ExtrudeGeometry, MathUtils, Shape, Vector3Tuple } from "three"; 6 | import { BatchedStandardMaterial } from "./batched-standard-material/batched-standard-material"; 7 | import { OverpassElement, getBuildingsData } from "./get-buildings-data"; 8 | 9 | extend({ BatchedStandardMaterial }) 10 | 11 | declare module '@react-three/fiber' { 12 | interface ThreeElements { 13 | batchedMesh: Object3DNode, 14 | batchedStandardMaterial: Object3DNode, 15 | } 16 | } 17 | 18 | interface BatchedBuildingsProps { 19 | buildingsCenter: Coords; 20 | origin: Coords; 21 | } 22 | 23 | const _color = new Color(); 24 | 25 | export const BatchedBuildings = memo(({ buildingsCenter, origin }) => { 26 | const buildings = suspend(() => { 27 | const start = { ...buildingsCenter }; 28 | start.latitude -= .01; 29 | start.longitude -= .01; 30 | const end = { ...buildingsCenter }; 31 | end.latitude += .01; 32 | end.longitude += .01; 33 | return getBuildingsData({ start, end }); 34 | }, [buildingsCenter]); 35 | 36 | const [hovered, hover] = useState() 37 | 38 | const { data, vertexCount, indexCount, key } = useMemo(() => { 39 | // lights 40 | const c00 = _color.set('#f0c505').getHSL({ h: 0, s: 0, l: 0 }); 41 | const c01 = _color.set('#f38630').getHSL({ h: 0, s: 0, l: 0 }); 42 | // darks 43 | const c10 = _color.set('#001449').getHSL({ h: 0, s: 0, l: 0 }); 44 | const c11 = _color.set('#49007e').getHSL({ h: 0, s: 0, l: 0 }); 45 | 46 | const data = buildings.map((element, i) => { 47 | const { poly, height, base } = getElementPolygon(element, origin); 48 | const geometry = polygonToExtrudeGeo(poly, height, base); 49 | const c0 = new Color().setHSL(rand(c00.h, c01.h), rand(c00.s, c01.s), rand(c00.l, c01.l)); 50 | const c1 = new Color().setHSL(rand(c10.h, c11.h), rand(c10.s, c11.s), rand(c10.l, c11.l)); 51 | const emissiveIntensity = rand(0, 1) < 0.05 ? 3.5 : 0; 52 | const roughness = rand(0, 0.5); 53 | const metalness = rand(0, 1); 54 | const offset = rand(0, 2 * Math.PI); 55 | const speed = rand(1, 2); 56 | const value = offset; 57 | return { 58 | i, 59 | value, 60 | c0, 61 | c1, 62 | speed, 63 | emissiveIntensity, roughness, metalness, 64 | element, 65 | geometry, 66 | vertexCount: geometry.attributes.position.count, 67 | indexCount: geometry.index?.count || 0, 68 | } 69 | }); 70 | const vertexCount = data.reduce((acc, d) => acc + d.vertexCount, 0); 71 | const indexCount = data.reduce((acc, d) => acc + d.indexCount, 0); 72 | const key = MathUtils.generateUUID(); 73 | return { data, vertexCount, indexCount, key }; 74 | }, [origin, buildings]) 75 | 76 | const meshRef = useRef(null); 77 | const matRef = useRef(null); 78 | 79 | useFrame((_, delta) => { 80 | if (!matRef.current) return; 81 | const material = matRef.current; 82 | for (let i = 0; i < data.length; i++) { 83 | const item = data[i]; 84 | const { c0, c1, emissiveIntensity, roughness, metalness, speed } = item; 85 | item.value += delta * speed; 86 | const sinValue = Math.abs(Math.sin(item.value)) 87 | 88 | const color = _color.lerpColors(c0, c1, sinValue); 89 | material.setValue(item.i, 'diffuse', ...color); 90 | 91 | color.multiplyScalar(hovered === i ? 20 : emissiveIntensity); 92 | material.setValue(item.i, 'emissive', ...color); 93 | 94 | material.setValue(item.i, 'roughness', roughness); 95 | material.setValue(item.i, 'metalness', metalness); 96 | } 97 | }) 98 | 99 | useLayoutEffect(() => { 100 | if (!meshRef.current) return; 101 | const mesh = meshRef.current; 102 | for (let i = 0; i < data.length; i++) { 103 | mesh.addGeometry(data[i].geometry); 104 | } 105 | }, [data]); // eslint-disable-line react-hooks/exhaustive-deps 106 | 107 | return (e.stopPropagation(), hover(e.batchId))} 113 | onPointerOut={() => hover(undefined)} 114 | > 115 | 116 | ; 117 | }) 118 | 119 | BatchedBuildings.displayName = 'BatchedBuildings'; 120 | 121 | function getElementPolygon(element: OverpassElement, origin: Coords) { 122 | const poly = geoPolyToVectorPoly(element.geometry || [], origin); 123 | let height = parseFloat(element.tags?.height || '0'); 124 | if (!height) height = parseFloat(element.tags?.['building:levels'] || '1') * 3; 125 | const base = parseFloat(element.tags?.min_height || '0'); 126 | return { poly, height, base }; 127 | } 128 | 129 | function geoPolyToVectorPoly(poly: { lat: number, lon: number }[], origin: Coords): Vector3Tuple[] { 130 | return poly.map(p => { 131 | const point = { latitude: p.lat, longitude: p.lon }; 132 | return coordsToVector3(point, origin); 133 | }); 134 | } 135 | 136 | function polygonToExtrudeGeo(poly: Vector3Tuple[], height: number, base: number) { 137 | const shape = new Shape(); 138 | shape.moveTo(poly[0][2], poly[0][0]); 139 | for (let i = 1; i < poly.length; i++) { 140 | shape.lineTo(poly[i][2], poly[i][0]); 141 | } 142 | shape.closePath(); 143 | const geo = new ExtrudeGeometry(shape, { depth: height - base, bevelEnabled: false }); 144 | geo.translate(0, 0, base); 145 | return geo; 146 | } 147 | 148 | function rand(min: number, max: number) { 149 | const delta = max - min 150 | return min + Math.random() * delta 151 | } -------------------------------------------------------------------------------- /stories/src/sunlight/sunlight.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Billboard, Line, Plane, Ring, Sphere, useHelper } from "@react-three/drei"; 2 | import { useFrame } from "@react-three/fiber"; 3 | import { useControls } from "leva"; 4 | import { RefObject, memo, useEffect, useMemo, useRef } from "react"; 5 | import { useMap } from "react-three-map"; 6 | import { getPosition } from "suncalc"; 7 | import { BufferAttribute, BufferGeometry, CameraHelper, Color, MathUtils, OrthographicCamera, PCFSoftShadowMap, Vector3Tuple } from "three"; 8 | import { ScreenSizer } from "../screen-sizer"; 9 | import { StoryMap } from "../story-map"; 10 | import tzLookup from "tz-lookup"; 11 | import { DateTime } from "luxon"; 12 | 13 | const RADIUS = 150; 14 | 15 | const night = new Color('#00008B'); 16 | const day = new Color('orange'); 17 | 18 | export function Default() { 19 | 20 | const { longitude, latitude } = useControls({ 21 | longitude: { 22 | value: 0, 23 | min: -179, 24 | max: 180, 25 | pad: 6, 26 | }, 27 | latitude: { 28 | value: 51, 29 | min: -80, 30 | max: 80, 31 | pad: 6, 32 | }, 33 | }) 34 | 35 | return
36 | 46 | 47 | 48 | 49 | 55 | 56 | 57 | 58 | 59 |
60 | } 61 | 62 | function Sun({ latitude, longitude }: { longitude: number, latitude: number }) { 63 | 64 | const { position, sunPath } = useSun({ latitude, longitude }); 65 | 66 | useMapColorsBasedOnSun(position); 67 | 68 | const { showCamHelper, cameraScale } = useControls({ 69 | showCamHelper: false, 70 | cameraScale: { 71 | value: 0.2, 72 | min: 0.1, 73 | max: 2_000_000, 74 | } 75 | }) 76 | 77 | const camera = useRef(null); 78 | 79 | useFrame(() => { 80 | if (!camera.current) return; 81 | camera.current.left = -cameraScale; 82 | camera.current.right = cameraScale; 83 | camera.current.top = -cameraScale; 84 | camera.current.bottom = cameraScale; 85 | }) 86 | 87 | return <> 88 | 89 | 90 | {showCamHelper && } 91 | = 0 ? 1.5 * Math.PI: 0} 95 | shadow-mapSize={1024} 96 | > 97 | = 0} /> 98 | 99 | 100 | 101 | 111 | 112 | 118 | 119 | } 120 | 121 | function CamHelper({ camera }: { camera: RefObject }) { 122 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 123 | useHelper(camera as any, CameraHelper); 124 | 125 | return <> 126 | } 127 | 128 | function SunPath({ path }: { path: Vector3Tuple[] }) { 129 | 130 | const geometry = useMemo(() => { 131 | // Define the geometry 132 | const geometry = new BufferGeometry(); 133 | 134 | // Define the vertices (the end points of the line) 135 | const vertices = new Float32Array(path.flat()); 136 | 137 | // Define the colors 138 | const colors = new Float32Array(path 139 | .map(p => p[1]) 140 | .flatMap(getSunColor) 141 | ); 142 | 143 | // Attach the vertices and colors to the geometry 144 | geometry.setAttribute('position', new BufferAttribute(vertices, 3)); 145 | geometry.setAttribute('color', new BufferAttribute(colors, 3)); 146 | 147 | return geometry; 148 | }, [path]); 149 | 150 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 151 | // @ts-ignore 152 | return 153 | 154 | 155 | } 156 | 157 | const _color = new Color() 158 | 159 | function getSunColor(y: number) { 160 | const nightStart = -RADIUS * .5; 161 | const dayStart = RADIUS * .5; 162 | if (y <= nightStart) return night.toArray(); 163 | if (y >= dayStart) return day.toArray(); 164 | const d = (y - nightStart) / (dayStart - nightStart) 165 | return _color.copy(night).lerp(day, d).toArray(); 166 | } 167 | 168 | function Floor() { 169 | return 175 | 176 | 177 | } 178 | 179 | function useMapColorsBasedOnSun(position: Vector3Tuple) { 180 | const map = useMap(); 181 | 182 | useEffect(() => { 183 | if (!map) return; 184 | const style = position[1] > 0 185 | ? "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json" 186 | : "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"; 187 | map.setStyle(style) 188 | }, [map, position]) 189 | } 190 | 191 | function useSun({ latitude, longitude }: { longitude: number, latitude: number }) { 192 | const { month, hour } = useControls({ 193 | month: { 194 | value: new Date().getMonth() + 1, 195 | min: 1, 196 | max: 12, 197 | step: 0.1, 198 | }, 199 | hour: { value: new Date().getHours(), min: 0, max: 23, step: 0.1 }, 200 | }); 201 | 202 | const date = useMemo(() => { 203 | const timeZone = tzLookup(latitude, longitude); 204 | return DateTime.now().setZone(timeZone).set({ 205 | month: Math.floor(month), 206 | day: Math.floor((month % 1) * 27) + 1, 207 | hour: Math.floor(hour), 208 | minute: (hour % 1) * 60, 209 | second: 0, 210 | millisecond: 0, 211 | }).toJSDate() 212 | }, [latitude, longitude, month, hour]) 213 | 214 | const { position, sunPath } = useMemo(() => { 215 | const position = getSunPosition({ date, latitude, longitude }); 216 | 217 | const tempDate = new Date(date); 218 | const sunPath: Vector3Tuple[] = []; 219 | for (let hour = 0; hour <= 24; hour++) { 220 | tempDate.setHours(hour); 221 | sunPath.push(getSunPosition({ date: tempDate, latitude, longitude })) 222 | } 223 | return { position, sunPath } 224 | }, [date, latitude, longitude]) 225 | 226 | return { position, sunPath }; 227 | } 228 | 229 | interface AnalemmaProps { 230 | latitude: number; 231 | longitude: number; 232 | } 233 | 234 | const Analemma = memo(({ latitude, longitude }) => { 235 | const analemma = useMemo(() => getAnalemma({ latitude, longitude }), [latitude, longitude]); 236 | return <> 237 | {analemma.map((points, i) => )} 246 | 247 | 248 | }) 249 | 250 | function getAnalemma({ latitude, longitude }: AnalemmaProps) { 251 | 252 | const analemma: Vector3Tuple[][] = []; 253 | const timeZone = tzLookup(latitude, longitude); 254 | const dateTime = DateTime.now() 255 | .setZone(timeZone) 256 | .set({ minute: 0, second: 0, millisecond: 0, }); 257 | 258 | for (let hour = 0; hour < 24; hour++) { 259 | analemma.push([]); 260 | for (let day = 0; day < 365; day++) { 261 | 262 | const date = dateTime.set({ day, hour }).toJSDate(); 263 | analemma[hour].push(getSunPosition({ date, latitude, longitude })); 264 | 265 | } 266 | } 267 | 268 | return analemma; 269 | } 270 | 271 | function getSunPosition({ date, latitude, longitude, radius = RADIUS }: { 272 | date: Date; latitude: number; longitude: number; radius?: number; 273 | }): Vector3Tuple { 274 | const sun = getPosition(date, latitude, longitude); 275 | const x = radius * Math.cos(sun.altitude) * - Math.sin(sun.azimuth); 276 | const z = radius * Math.cos(sun.altitude) * Math.cos(sun.azimuth); 277 | const y = radius * Math.sin(sun.altitude); 278 | return [x, y, z]; 279 | } -------------------------------------------------------------------------------- /stories/src/extrude/chaillot.ts: -------------------------------------------------------------------------------- 1 | import { Vector2Tuple } from "three"; 2 | 3 | type Poly2D = Vector2Tuple[]; 4 | 5 | export const Chaillot : Poly2D[] = [ 6 | [[2.2876091301441193,48.86232738746588],[2.287643998861313,48.86230709517818],[2.2876587510108948,48.86231680018636],[2.2876694798469543,48.862309741998786],[2.2876547276973724,48.86230003698921],[2.287694960832596,48.86227356877174],[2.2877097129821777,48.86228415606041],[2.2877217829227448,48.86227709786823],[2.287707030773163,48.86226651057805],[2.287745922803879,48.862241806892314],[2.2877606749534607,48.86225151191317],[2.287774085998535,48.862243571441724],[2.2877593338489532,48.86223298414444],[2.287798225879669,48.86220828044213],[2.287812978029251,48.8622179854695],[2.287825047969818,48.86221004499271],[2.287810295820236,48.86219945768838],[2.2878505289554596,48.862174753969526],[2.2878652811050415,48.86218445900337],[2.2878773510456085,48.86217651852127],[2.2878625988960266,48.86216593120986],[2.28790283203125,48.86214034519796],[2.2879189252853394,48.862150932514794],[2.2879309952259064,48.86214299202737],[2.2879162430763245,48.86213328698548],[2.287956476211548,48.86210681867982],[2.2879712283611298,48.862116523726854],[2.2879832983016968,48.862108583233976],[2.287968546152115,48.86209887818541],[2.288007438182831,48.86207329213923],[2.28802353143692,48.862083879470276],[2.288035601377487,48.86207682124984],[2.2880208492279053,48.86206623391732],[2.288047671318054,48.862047706080006],[2.2879792749881744,48.86200094531725],[2.288035601377487,48.86196388958743],[2.2880007326602936,48.86194095031237],[2.2880208492279053,48.86192683383018],[2.287999391555786,48.86191271734401],[2.287968546152115,48.8619135996245],[2.2879403829574585,48.861910070502404],[2.2879256308078766,48.86190654138005],[2.287900149822235,48.86189595401149],[2.287878692150116,48.861883602078706],[2.287861257791519,48.86186683873646],[2.287849187850952,48.86184831082531],[2.28784516453743,48.86182890062537],[2.2878478467464447,48.8618094904179],[2.2878263890743256,48.861795373898616],[2.2878049314022064,48.86180860813556],[2.287770062685013,48.86178478650655],[2.2877123951911926,48.861820960086845],[2.287643998861313,48.861775081395194],[2.2876332700252533,48.861780375092536],[2.2875353693962097,48.86170538099469],[2.2873100638389587,48.861511278102],[2.2872644662857056,48.861463634549665],[2.2871947288513184,48.861383346238455],[2.28715717792511,48.8613312911107],[2.2871048748493195,48.861241297372345],[2.2870901226997375,48.86121306400929],[2.2870485484600067,48.86112483464717],[2.28703111410141,48.86107189695525],[2.2866368293762207,48.86107189695525],[2.286640852689743,48.86109130744879],[2.2866716980934143,48.86118483063035],[2.28671595454216,48.86128011822049],[2.286752164363861,48.86134981921322],[2.2867950797080994,48.86141687324121],[2.2868473827838898,48.86149451463507],[2.286883592605591,48.861535982148354],[2.2870364785194397,48.86169302901487],[2.2871196269989014,48.86176802313122],[2.2872430086135864,48.861871250142855],[2.287355661392212,48.861957713629806],[2.2872912883758545,48.86199829848033],[2.287275195121765,48.86199565164324],[2.2872671484947205,48.86199565164324],[2.287251055240631,48.86199829848033],[2.287236303091049,48.86200270987513],[2.2872282564640045,48.862007121269556],[2.28722020983696,48.862014179499795],[2.2872135043144226,48.86202388456479],[2.287212163209915,48.862029178235844],[2.2872108221054077,48.86204064785446],[2.2872135043144226,48.862047706080006],[2.2872067987918854,48.862052117470455],[2.287275195121765,48.86209799590816],[2.2872188687324524,48.8621350515387],[2.2872550785541534,48.86215975527713],[2.287241667509079,48.86216857803791],[2.2873395681381226,48.86223563096897],[2.287433445453644,48.86230003698921],[2.2874468564987183,48.86229121425163],[2.2874844074249268,48.86231591791298],[2.287542074918747,48.862280626964434],[2.2876091301441193,48.86232738746588]], 7 | [[2.290222942829132,48.86337287028812],[2.290288656949997,48.86336934126891],[2.290354371070862,48.86336228322975],[2.290399968624115,48.86335610744467],[2.2904670238494873,48.86334375587222],[2.2905313968658447,48.863328757530184],[2.2905756533145905,48.863316405951],[2.290634661912918,48.863297878576475],[2.2906923294067383,48.86327582216933],[2.2906936705112457,48.863274939912856],[2.2906936705112457,48.8629361522716],[2.290666848421097,48.862956444304245],[2.290622591972351,48.86298644120703],[2.290574312210083,48.86301379130862],[2.290540784597397,48.86303143652748],[2.2904710471630096,48.86306231564558],[2.2904106974601746,48.86308437214683],[2.290370464324951,48.863094959263975],[2.2903288900852203,48.86310378185988],[2.290285974740982,48.8631099576761],[2.2902430593967438,48.86311436897293],[2.2901558876037598,48.86311613349156],[2.2901062667369843,48.86311525123227],[2.290057986974716,48.8631108399355],[2.2900083661079407,48.86310378185988],[2.289942651987076,48.86309407700432],[2.289879620075226,48.86308172536721],[2.289816588163376,48.863065844686446],[2.2897562384605408,48.863047317219156],[2.2896382212638855,48.863010262264055],[2.2895389795303345,48.86297497180513],[2.28946253657341,48.8629449748955],[2.2893592715263367,48.86289909723419],[2.2892600297927856,48.862849690475],[2.2891902923583984,48.8628117531089],[2.2890588641166687,48.86273764282248],[2.2889676690101624,48.86268206003561],[2.2889770567417145,48.86267676643365],[2.2889140248298645,48.86263353532996],[2.2889703512191772,48.86259824460541],[2.288934141397476,48.86257265881454],[2.288954257965088,48.86255942477965],[2.2889328002929688,48.86254530847191],[2.288903295993805,48.86254619074123],[2.288873791694641,48.862542661663724],[2.288846969604492,48.86253472123846],[2.288822829723358,48.862522369463306],[2.288804054260254,48.86250737087511],[2.288789302110672,48.86248972547142],[2.2887812554836273,48.86247119779091],[2.2887812554836273,48.862451787832526],[2.2887825965881348,48.86244296512166],[2.2887611389160156,48.86242796650964],[2.2887396812438965,48.8624412005793],[2.288704812526703,48.86241737925141],[2.2886458039283752,48.86245355237449],[2.2885867953300476,48.86241208562143],[2.288578748703003,48.86240679199091],[2.288549244403839,48.86242620196674],[2.2885331511497498,48.86241561470814],[2.28852242231369,48.862422672880825],[2.288537174463272,48.86243326013792],[2.2884996235370636,48.862457963729184],[2.288482189178467,48.86244649420618],[2.2884687781333923,48.862455316916396],[2.2884848713874817,48.86246678643738],[2.2884459793567657,48.862493254552675],[2.2884298861026764,48.86248266730823],[2.288416475057602,48.86249060774176],[2.2884325683116913,48.86250119498453],[2.288392335176468,48.86252766308158],[2.2883762419223785,48.86251707584444],[2.288362830877304,48.8625241340028],[2.288380265235901,48.86253560350801],[2.2883400321006775,48.86256118931783],[2.2883225977420807,48.86255060208774],[2.2883105278015137,48.86255766024138],[2.288326621055603,48.862570012007836],[2.2882890701293945,48.86259383326308],[2.2882716357707977,48.86258324603992],[2.288256883621216,48.86259206872606],[2.2882743179798126,48.862603538215666],[2.288236767053604,48.86262735945499],[2.2882193326950073,48.86261588997081],[2.2882072627544403,48.86262471265118],[2.2882233560085297,48.86263529986556],[2.28814959526062,48.86268206003561],[2.2881415486335754,48.86268735363706],[2.288209944963455,48.86273411375848],[2.2881536185741425,48.86277028665262],[2.288157641887665,48.86277293344875],[2.288191169500351,48.862795872342474],[2.288176417350769,48.86280469499113],[2.288272976875305,48.862870864806325],[2.2883695363998413,48.8629361522716],[2.2883829474449158,48.8629264473835],[2.28840172290802,48.862938799058924],[2.288423180580139,48.86295379751786],[2.2884781658649445,48.862914977967876],[2.288546562194824,48.862961737876645],[2.2885559499263763,48.862955562042146],[2.2885707020759583,48.86295909109052],[2.2885867953300476,48.86295909109052],[2.2885948419570923,48.862957326566345],[2.288609594106674,48.86295379751786],[2.2886230051517487,48.8629458571578],[2.2886310517787933,48.86293791679651],[2.2886377573013306,48.86292291833283],[2.2886377573013306,48.862917624756335],[2.2886337339878082,48.86290615533866],[2.2886981070041656,48.86286380669688],[2.2888724505901337,48.86296703144848],[2.289125919342041,48.86309760604291],[2.2892452776432037,48.86315495288551],[2.2893472015857697,48.86319730128096],[2.289453148841858,48.86323612061193],[2.289561778306961,48.86327052863015],[2.289597988128662,48.863281115707935],[2.28970929980278,48.86331023016024],[2.2898246347904205,48.86333493331861],[2.2899332642555237,48.863354342934514],[2.290021777153015,48.86336404773965],[2.290111631155014,48.863371105778555],[2.290201485157013,48.86337287028812],[2.290222942829132,48.86337287028812]], 8 | [[2.2870512306690216,48.86113012841329],[2.2870485484600067,48.86112483464717],[2.28703111410141,48.86107189695525],[2.2870177030563354,48.86101719461476],[2.287016361951828,48.86098102045355],[2.2870177030563354,48.860944846266165],[2.2870230674743652,48.860915730437824],[2.28703111410141,48.860889261488296],[2.287043184041977,48.860863674823776],[2.2870592772960663,48.86083808814615],[2.2870874404907227,48.86079926695513],[2.2871115803718567,48.86077279794398],[2.2871585190296173,48.86073221209972],[2.2872456908226013,48.86066956866679],[2.287319451570511,48.86062457122293],[2.2873127460479736,48.860620159706656],[2.2873328626155853,48.86060780745899],[2.287263125181198,48.86055928074225],[2.287307381629944,48.860530164689635],[2.287265807390213,48.860500166314665],[2.2872738540172577,48.860494872481894],[2.2871142625808716,48.86038458417244],[2.2871048748493195,48.86038987801686],[2.2870592772960663,48.86035987955779],[2.287013679742813,48.86038987801686],[2.2870056331157684,48.860383701864976],[2.28684201836586,48.86048957864864],[2.2868500649929047,48.86049575478742],[2.286805808544159,48.86052487086005],[2.2868259251117706,48.86053810543294],[2.2867481410503387,48.8605866321702],[2.286796420812607,48.860620159706656],[2.2867441177368164,48.86067574478338],[2.286701202392578,48.860735741304865],[2.2866716980934143,48.860791326253235],[2.2866448760032654,48.86085838102949],[2.2866354882717133,48.86089720217464],[2.286631464958191,48.860927200311636],[2.2866274416446686,48.860975726671654],[2.286628782749176,48.86099778409249],[2.2866328060626984,48.8610498395671],[2.286640852689743,48.86109130744879],[2.2866542637348175,48.86113012841329],[2.2870512306690216,48.86113012841329]], 9 | [[2.2906051576137543,48.86330670113662],[2.290634661912918,48.863297878576475],[2.2906923294067383,48.86327582216933],[2.2907473146915436,48.863251118981765],[2.290790230035782,48.86322994481131],[2.290847897529602,48.863269646373595],[2.290892153978348,48.86324141415477],[2.2909002006053925,48.863246707697016],[2.2910651564598083,48.86313995448688],[2.2910557687282562,48.86313377867435],[2.2910986840724945,48.86310642863836],[2.2911013662815094,48.86310466411942],[2.291058450937271,48.86307554954749],[2.2910678386688232,48.86306937372706],[2.2910302877426147,48.86304378817701],[2.290991395711899,48.863017320352895],[2.290916293859482,48.86296614918652],[2.2909069061279297,48.8629608556146],[2.290898859500885,48.86296614918652],[2.290853261947632,48.8629361522716],[2.2908076643943787,48.86296526692456],[2.2907379269599915,48.862916742493525],[2.290715128183365,48.8629308586965],[2.2907084226608276,48.86292468285819],[2.290666848421097,48.862956444304245],[2.290622591972351,48.86298644120703],[2.2906051576137543,48.8629961460835],[2.2906051576137543,48.86330670113662]] 10 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![logo](public/favicon.svg)React Three Map 2 | 3 | [![Repository](https://img.shields.io/static/v1?&message=github&style=flat&colorA=000000&colorB=000000&label=&logo=github&logoColor=ffffff)](https://github.com/RodrigoHamuy/react-three-map) 4 | [![Version](https://img.shields.io/npm/v/react-three-map?style=flat&colorA=000000&colorB=000000)](https://npmjs.com/package/react-three-map) 5 | [![Build Size](https://img.shields.io/bundlephobia/minzip/react-three-map?label=size&?style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/result?p=react-three-map) 6 | [![Build Status](https://img.shields.io/github/actions/workflow/status/RodrigoHamuy/react-three-map/release.yml?branch=main&style=flat&colorA=000000)](https://github.com/RodrigoHamuy/react-three-map/actions?query=workflow%3Arelease) 7 | [![Examples](https://img.shields.io/badge/stories-stories?colorA=000&colorB=000000&label=📍)](https://rodrigohamuy.github.io/react-three-map/?story=canvas--a-maplibre-example) 8 | [![Sponsor me](https://img.shields.io/github/sponsors/RodrigoHamuy?style=flat&colorA=000000&colorB=000000&label=💛%20sponsor)](https://github.com/sponsors/RodrigoHamuy?frequency=one-time&sponsor=RodrigoHamuy) 9 | 10 | `react-three-map` is a bridge to use [`react-three-fiber`](https://github.com/pmndrs/react-three-fiber) inside [`react-map-gl`](https://github.com/visgl/react-map-gl). 11 | 12 | Until now you had: 13 | 14 | | imperative | declarative (react) | 15 | | --------------- | ------------------- | 16 | | Maplibre/Mapbox | react-map-gl | 17 | | THREE.js | react-three-fiber | 18 | 19 | Now with `react-three-map`, you can use them together :fist_right::star::fist_left:. 20 | 21 | ```sh 22 | npm install react-three-map 23 | ``` 24 | 25 | - [React Three Map](#react-three-map) 26 | - [:book: Examples](#book-examples) 27 | - [:mag: What does it look like?](#mag-what-does-it-look-like) 28 | - [:thinking: Why we build this?](#thinking-why-we-build-this) 29 | - [:gear: API](#gear-api) 30 | - [Canvas](#canvas) 31 | - [Render Props](#render-props) 32 | - [Render Props removed from `@react-three/fiber`](#render-props-removed-from-react-threefiber) 33 | - [Coordinates](#coordinates) 34 | - [NearCoordinates](#nearcoordinates) 35 | - [useMap](#usemap) 36 | - [coordsToVector3](#coordstovector3) 37 | - [vector3ToCoords](#vector3tocoords) 38 | 39 | 40 | ## :book: Examples 41 | 42 | Check out our examples [here](https://rodrigohamuy.github.io/react-three-map) (powered by [Ladle](https://ladle.dev/)). 43 | 44 | ## :mag: What does it look like? 45 | 46 | 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 |
Let's build the same react-three-fiber basic example, but now it can be inside a map. (live demo). 52 | 53 | 54 | 55 |
59 | 60 | 1. Import `Canvas` from `react-three-map` instead of `@react-three/fiber`. 61 | 2. Give it a latitude and longitude so it knows where to position the scene in the map. 62 | 3. Everything else should work just as usual. 63 | 64 | ```jsx 65 | import "maplibre-gl/dist/maplibre-gl.css" 66 | import { createRoot } from 'react-dom/client' 67 | import React, { useRef, useState } from 'react' 68 | import { useFrame } from "@react-three/fiber" 69 | import { useRef, useState } from "react" 70 | import Map from "react-map-gl/maplibre" 71 | import { Canvas } from "react-three-map/maplibre" 72 | // import { Canvas } from "react-three-map" // if you are using MapBox 73 | 74 | function BasicExample() { 75 | return 87 | 88 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | } 99 | ``` 100 | 101 | ## :thinking: Why we build this? 102 | 103 | Look [how complex](https://maplibre.org/maplibre-gl-js-docs/example/add-3d-model/) is to add just one ThreeJS object to a map. 104 | 105 | Look [how complex](https://docs.pmnd.rs/react-three-fiber/api/canvas#createroot) is to create your custom root for R3F. 106 | 107 | You can now replace all that complexity and hundreds of lines of code with the `` component exported by `react-three-map`, which includes a tone of extra features and seamless integration, supporting pointer events, raycasting, and much more, all out of the box. 108 | 109 | ## :gear: API 110 | 111 | ### Canvas 112 | 113 | Same as in `@react-three/fiber`, the `` object is where you start to define your React Three Fiber Scene. 114 | 115 | ```tsx 116 | import "maplibre-gl/dist/maplibre-gl.css" 117 | import Map from "react-map-gl/maplibre" 118 | import { Canvas } from 'react-three-map/maplibre' 119 | // import { Canvas } from "react-three-map" // if you are using MapBox 120 | 121 | const App = () => ( 122 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | ) 134 | ``` 135 | 136 | It shares most of the props from R3F ``, so you can check them directly in the [`@react-three/fiber` docs](https://docs.pmnd.rs/react-three-fiber/api/canvas). There are a few important exceptions though, which are mentioned bellow. 137 | 138 | #### Render Props 139 | 140 | | Prop | Description | Default | 141 | | --------- | ------------------------------------------------ | ---------- | 142 | | latitude | The latitude coordinate where to add the scene. | | 143 | | longitude | The longitude coordinate where to add the scene. | | 144 | | altitude | The altitude coordinate where to add the scene. | `0` | 145 | | frameloop | Render mode: `"always"`, `"demand"`. | `"always"` | 146 | | overlay | Render on a separated canvas. | `false` | 147 | 148 | **About `overlay`** 149 | 150 | You may want to use `overlay` if: 151 | 152 | - You use `react-postprocessing` and have issues clearing the screen. 153 | - Want to avoid unnecesary map renders when only the Three scene changed. 154 | 155 | But it comes with some caveats: 156 | 157 | - ThreeJS will always render on top, as this is now a separated canvas and doesn't have access to the map depth buffer. 158 | - `react-postprocessing` will also not work if you also use `` components. 159 | 160 | #### Render Props removed from `@react-three/fiber` 161 | 162 | Because the scene now lives in a map, we leave a lot of the render and camera control to the map, rather than to R3F. 163 | 164 | Therefore, the following `` props are ignored: 165 | 166 | - gl 167 | - camera 168 | - resize 169 | - orthographic 170 | - dpr 171 | 172 | ### Coordinates 173 | 174 | [![Coordinates example](docs/coordinates.png)](https://rodrigohamuy.github.io/react-three-map/?story=multi-coordinates--default) 175 | 176 | This component allows you to have 3D objects at different coordinates. 177 | 178 | 179 | ```tsx 180 | import { Canvas, Coordinates } from 'react-three-map' 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | ``` 191 | 192 | | Props | Description | Default | 193 | | --------- | ------------------------------------------------ | ------- | 194 | | latitude | The latitude coordinate where to add the scene. | | 195 | | longitude | The longitude coordinate where to add the scene. | | 196 | | altitude | The altitude coordinate where to add the scene. | `0` | 197 | 198 | ### NearCoordinates 199 | 200 | [![](https://img.shields.io/badge/-demo-%23ff69b4)](https://rodrigohamuy.github.io/react-three-map/?story=multi-coordinates--default) 201 | 202 | Same as `Coordinates`, but scale is ignored in exchange of being able to be rendered under the same scene. 203 | 204 | Works well at city level distances, but since scale remains unchanged, is not recommended at country level distances. 205 | 206 | Check the story to see the difference between the two or check #102 for more info. 207 | 208 | ### useMap 209 | 210 | Access the map from inside `react-three-map`. 211 | 212 | ```tsx 213 | import { useMap } from "react-three-map"; 214 | // import { useMap } from "react-three-map/maplibre"; if you use maplibre 215 | const Component = () => { 216 | const map = useMap(); 217 | return <>... 218 | } 219 | 220 | ``` 221 | 222 | ### coordsToVector3 223 | 224 | [![](https://img.shields.io/badge/-demo-%23ff69b4)](https://rodrigohamuy.github.io/react-three-map/?story=extrude-coordinates--extrude-coordinates) 225 | 226 | This utility function converts geographic coordinates into a `Vector3Tuple`, which represents a 3D vector in meters. 227 | 228 | Similar to `NearCoordinates`, remember that this only updates positions (translation) but that scale is not taken into account, which has an important factor at very long distances (country level). 229 | 230 | 231 | | Parameter | Description | 232 | | ---------------- | --------------------------------------------------------------- | 233 | | `point: Coords` | The geographic coordinates of the point to convert. | 234 | | `origin: Coords` | The geographic coordinates used as the origin for calculations. | 235 | 236 | Returns a `Vector3Tuple` representing the 3D position of the point relative to the origin. 237 | 238 | ### vector3ToCoords 239 | 240 | [![](https://img.shields.io/badge/-demo-%23ff69b4)](https://rodrigohamuy.github.io/react-three-map/?story=pivot-controls--default) 241 | 242 | This utility function converts a `Vector3Tuple`, which represents a 3D vector in meters, back into geographic coordinates. 243 | 244 | It is the inverse of `coordsToVector3` but it does not have a good level of precision at long distances since we haven't reverse engineered #102 fix yet. 245 | 246 | Recommended to use at city level distances, but margin errors will be noticeable at country level distances. 247 | 248 | | Parameter | Description | 249 | | ------------------------ | --------------------------------------------------------------- | 250 | | `position: Vector3Tuple` | The 3D vector to convert back into geographic coordinates. | 251 | | `origin: Coords` | The geographic coordinates used as the origin for calculations. | 252 | 253 | Returns a `Coords` object representing the geographic coordinates of the point relative to the origin. --------------------------------------------------------------------------------