@more-than-software/mapkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +306 -0
  2. package/dist/README.md +306 -0
  3. package/dist/components/MapEffectComposer.d.ts +5 -0
  4. package/dist/components/MapEffectComposer.d.ts.map +1 -0
  5. package/dist/components/MapEntity.d.ts +10 -0
  6. package/dist/components/MapEntity.d.ts.map +1 -0
  7. package/dist/components/MapScene.d.ts +15 -0
  8. package/dist/components/MapScene.d.ts.map +1 -0
  9. package/dist/components/MapViewport.d.ts +10 -0
  10. package/dist/components/MapViewport.d.ts.map +1 -0
  11. package/dist/hooks/useMapEvents.d.ts +44 -0
  12. package/dist/hooks/useMapEvents.d.ts.map +1 -0
  13. package/dist/index.d.ts +36 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +43381 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/package.json +48 -0
  18. package/dist/plugins/TileCreasedNormalsPlugin.d.ts +11 -0
  19. package/dist/plugins/TileCreasedNormalsPlugin.d.ts.map +1 -0
  20. package/dist/src/components/MapEffectComposer.tsx +39 -0
  21. package/dist/src/components/MapEntity.tsx +98 -0
  22. package/dist/src/components/MapScene.tsx +138 -0
  23. package/dist/src/components/MapViewport.tsx +82 -0
  24. package/dist/src/hooks/useMapEvents.ts +211 -0
  25. package/dist/src/index.ts +55 -0
  26. package/dist/src/plugins/TileCreasedNormalsPlugin.ts +64 -0
  27. package/dist/src/utils/camera.ts +60 -0
  28. package/dist/src/utils/flyTo.ts +118 -0
  29. package/dist/src/utils/presets.ts +63 -0
  30. package/dist/utils/camera.d.ts +12 -0
  31. package/dist/utils/camera.d.ts.map +1 -0
  32. package/dist/utils/flyTo.d.ts +13 -0
  33. package/dist/utils/flyTo.d.ts.map +1 -0
  34. package/dist/utils/presets.d.ts +14 -0
  35. package/dist/utils/presets.d.ts.map +1 -0
  36. package/package.json +59 -0
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@more-than-software/mapkit",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "files": ["dist", "README.md"],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "https://github.com/more-than-software/citykit.git",
19
+ "directory": "packages/mapkit"
20
+ },
21
+ "keywords": ["react", "three.js", "3d-tiles", "google-maps", "geospatial", "globe", "map", "react-three-fiber"],
22
+ "scripts": {
23
+ "build": "vite build",
24
+ "prepack": "pnpm run build"
25
+ },
26
+ "dependencies": {
27
+ "3d-tiles-renderer": "0.4.21",
28
+ "@takram/three-atmosphere": "^0.16.0",
29
+ "@takram/three-clouds": "^0.16.0",
30
+ "@takram/three-geospatial": "^0.6.0",
31
+ "@takram/three-geospatial-effects": "^0.6.0",
32
+ "motion": "^12.34.2",
33
+ "postprocessing": "^6.38.2",
34
+ "react-merge-refs": "^3.0.2",
35
+ "tiny-invariant": "^1.3.3"
36
+ },
37
+ "peerDependencies": {
38
+ "@react-three/drei": "^10.0.0",
39
+ "@react-three/fiber": "^9.0.0",
40
+ "@react-three/postprocessing": "^3.0.0",
41
+ "react": "^18.0.0 || ^19.0.0",
42
+ "react-dom": "^18.0.0 || ^19.0.0",
43
+ "three": "^0.180.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
@@ -0,0 +1,11 @@
1
+ import { Tile } from '3d-tiles-renderer';
2
+ import { Object3D } from 'three';
3
+ export interface TileCreasedNormalsPluginOptions {
4
+ creaseAngle?: number;
5
+ }
6
+ export declare class TileCreasedNormalsPlugin {
7
+ readonly options: TileCreasedNormalsPluginOptions;
8
+ constructor(options?: TileCreasedNormalsPluginOptions);
9
+ processTileModel(scene: Object3D, tile: Tile): Promise<void>;
10
+ }
11
+ //# sourceMappingURL=TileCreasedNormalsPlugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TileCreasedNormalsPlugin.d.ts","sourceRoot":"","sources":["../../src/plugins/TileCreasedNormalsPlugin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAA;AAC7C,OAAO,EAAwB,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE3D,MAAM,WAAW,+BAA+B;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAgCD,qBAAa,wBAAwB;IACnC,QAAQ,CAAC,OAAO,EAAE,+BAA+B,CAAA;gBAErC,OAAO,CAAC,EAAE,+BAA+B;IAK/C,gBAAgB,CAAC,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;CAgBnE"}
@@ -0,0 +1,39 @@
1
+ import {
2
+ EffectComposer as WrappedEffectComposer,
3
+ type EffectComposerProps
4
+ } from '@react-three/postprocessing'
5
+ import {
6
+ NormalPass,
7
+ type EffectComposer as EffectComposerImpl
8
+ } from 'postprocessing'
9
+ import { useLayoutEffect, useRef, type FC, type RefAttributes } from 'react'
10
+ import { mergeRefs } from 'react-merge-refs'
11
+ import { HalfFloatType, type WebGLRenderTarget } from 'three'
12
+ import invariant from 'tiny-invariant'
13
+
14
+ import { reinterpretType } from '@takram/three-geospatial'
15
+
16
+ // Provided for half-float normal buffer.
17
+ export const MapEffectComposer: FC<
18
+ EffectComposerProps & RefAttributes<EffectComposerImpl>
19
+ > = ({ ref: forwardedRef, enableNormalPass = true, ...props }) => {
20
+ const ref = useRef<EffectComposerImpl>(null)
21
+ useLayoutEffect(() => {
22
+ const composer = ref.current
23
+ invariant(ref.current != null)
24
+ const normalPass = composer?.passes.find(pass => pass instanceof NormalPass)
25
+ invariant(normalPass != null)
26
+ reinterpretType<NormalPass & { renderTarget: WebGLRenderTarget }>(
27
+ normalPass
28
+ )
29
+ normalPass.renderTarget.texture.type = HalfFloatType
30
+ }, [])
31
+
32
+ return (
33
+ <WrappedEffectComposer
34
+ ref={mergeRefs([ref, forwardedRef])}
35
+ {...props}
36
+ enableNormalPass={enableNormalPass}
37
+ />
38
+ )
39
+ }
@@ -0,0 +1,98 @@
1
+ import { useGLTF } from '@react-three/drei'
2
+ import { useFrame } from '@react-three/fiber'
3
+ import { useMemo, type FC, type ReactNode } from 'react'
4
+ import { Sprite, SpriteMaterial, TextureLoader } from 'three'
5
+ import { EastNorthUpFrame } from '@takram/three-geospatial/r3f'
6
+
7
+ import type { MapEntity as MapEntityType } from '../index'
8
+
9
+ export interface MapEntityProps extends MapEntityType {
10
+ children?: ReactNode
11
+ }
12
+
13
+ const textureLoader = new TextureLoader()
14
+
15
+ /**
16
+ * Render a GLB model at a geodetic location
17
+ */
18
+ const ModelEntity: FC<{
19
+ url: string
20
+ position: [number, number, number]
21
+ rotation?: [number, number, number]
22
+ scale?: number
23
+ }> = ({ url, position, rotation, scale = 1 }) => {
24
+ const gltf = useGLTF(url)
25
+ const model = useMemo(() => {
26
+ const cloned = gltf.scene.clone()
27
+ cloned.scale.setScalar(scale)
28
+ if (rotation) {
29
+ cloned.rotation.set(rotation[0], rotation[1], rotation[2])
30
+ }
31
+ return cloned
32
+ }, [gltf.scene, rotation, scale])
33
+
34
+ return <primitive object={model} position={position} />
35
+ }
36
+
37
+ /**
38
+ * Render a billboard sprite at a geodetic location
39
+ */
40
+ const BillboardEntity: FC<{
41
+ url?: string
42
+ position: [number, number, number]
43
+ scale?: number
44
+ }> = ({ url, position, scale = 1 }) => {
45
+ const sprite = useMemo(() => {
46
+ const material = new SpriteMaterial({
47
+ map: url ? textureLoader.load(url) : undefined,
48
+ color: url ? undefined : 0xffffff,
49
+ transparent: true
50
+ })
51
+ const sprite = new Sprite(material)
52
+ sprite.scale.setScalar(scale * 10) // Default billboard size
53
+ return sprite
54
+ }, [url, scale])
55
+
56
+ return <primitive object={sprite} position={position} />
57
+ }
58
+
59
+ /**
60
+ * MapEntity component - renders entities at geodetic coordinates
61
+ */
62
+ export const MapEntity: FC<MapEntityProps> = ({
63
+ id,
64
+ type,
65
+ latitude,
66
+ longitude,
67
+ altitude = 0,
68
+ rotation,
69
+ scale = 1,
70
+ url,
71
+ children
72
+ }) => {
73
+ const position: [number, number, number] = [longitude, latitude, altitude]
74
+
75
+ if (type === 'model') {
76
+ if (!url) {
77
+ console.warn(`MapEntity ${id}: model type requires url`)
78
+ return null
79
+ }
80
+ return (
81
+ <EastNorthUpFrame longitude={longitude} latitude={latitude} height={altitude}>
82
+ <ModelEntity url={url} position={[0, 0, 0]} rotation={rotation} scale={scale} />
83
+ {children}
84
+ </EastNorthUpFrame>
85
+ )
86
+ }
87
+
88
+ if (type === 'billboard') {
89
+ return (
90
+ <EastNorthUpFrame longitude={longitude} latitude={latitude} height={altitude}>
91
+ <BillboardEntity url={url} position={[0, 0, 0]} scale={scale} />
92
+ {children}
93
+ </EastNorthUpFrame>
94
+ )
95
+ }
96
+
97
+ return null
98
+ }
@@ -0,0 +1,138 @@
1
+ import { useFrame, useThree } from '@react-three/fiber'
2
+ import { SMAA, ToneMapping } from '@react-three/postprocessing'
3
+ import type { GlobeControls as GlobeControlsImpl } from '3d-tiles-renderer'
4
+ import { GlobeControls } from '3d-tiles-renderer/r3f'
5
+ import {
6
+ EffectMaterial,
7
+ type EffectComposer as EffectComposerImpl
8
+ } from 'postprocessing'
9
+ import { Fragment, useEffect, useRef, useState, type FC, type ReactNode } from 'react'
10
+
11
+ import {
12
+ AerialPerspective,
13
+ Atmosphere,
14
+ type AtmosphereApi
15
+ } from '@takram/three-atmosphere/r3f'
16
+ import type { CloudsEffect } from '@takram/three-clouds'
17
+ import { Clouds } from '@takram/three-clouds/r3f'
18
+
19
+ import { MapEffectComposer } from './MapEffectComposer'
20
+ import { MapViewport } from './MapViewport'
21
+
22
+ export interface MapSceneProps {
23
+ apiKey: string
24
+ children?: ReactNode
25
+ enableClouds?: boolean
26
+ enableAerialPerspective?: boolean
27
+ enableToneMapping?: boolean
28
+ cloudsCoverage?: number
29
+ exposure?: number
30
+ correctAltitude?: boolean
31
+ correctGeometricError?: boolean
32
+ onLoadError?: () => void
33
+ }
34
+
35
+ export const MapScene: FC<MapSceneProps> = ({
36
+ apiKey,
37
+ children,
38
+ enableClouds = false,
39
+ enableAerialPerspective = true,
40
+ enableToneMapping = true,
41
+ cloudsCoverage = 0.3,
42
+ exposure = 10,
43
+ correctAltitude = true,
44
+ correctGeometricError = true,
45
+ onLoadError
46
+ }) => {
47
+ const camera = useThree(({ camera }) => camera)
48
+ const controls = useThree(
49
+ ({ controls }) => controls as GlobeControlsImpl | null
50
+ )
51
+
52
+ // Enable adjustHeight when user first interacts
53
+ useEffect(() => {
54
+ if (controls != null) {
55
+ const callback = (): void => {
56
+ controls.adjustHeight = true
57
+ controls.removeEventListener('start', callback)
58
+ }
59
+ controls.addEventListener('start', callback)
60
+ return () => {
61
+ controls.removeEventListener('start', callback)
62
+ }
63
+ }
64
+ }, [controls])
65
+
66
+ // Effects must know the camera near/far changed by GlobeControls.
67
+ const composerRef = useRef<EffectComposerImpl>(null)
68
+ useFrame(() => {
69
+ const composer = composerRef.current
70
+ if (composer != null) {
71
+ composer.passes.forEach(pass => {
72
+ if (pass.fullscreenMaterial instanceof EffectMaterial) {
73
+ pass.fullscreenMaterial.adoptCameraSettings(camera)
74
+ }
75
+ })
76
+ }
77
+ })
78
+
79
+ const atmosphereRef = useRef<AtmosphereApi>(null)
80
+ useFrame(() => {
81
+ atmosphereRef.current?.updateByDate(new Date())
82
+ })
83
+
84
+ const [clouds, setClouds] = useState<CloudsEffect | null>(null)
85
+
86
+ return (
87
+ <Atmosphere ref={atmosphereRef} correctAltitude={correctAltitude}>
88
+ <MapViewport apiKey={apiKey} onLoadError={onLoadError}>
89
+ <GlobeControls
90
+ enableDamping
91
+ // Globe controls adjust the camera height based on very low LoD tiles
92
+ // during the initial load, causing the camera to unexpectedly jump to
93
+ // the sky when set to a low altitude.
94
+ // Re-enable it when the user first drags.
95
+ adjustHeight={false}
96
+ maxAltitude={Math.PI * 0.55} // Permit grazing angles
97
+ />
98
+ </MapViewport>
99
+ {children}
100
+ <MapEffectComposer ref={composerRef} multisampling={0}>
101
+ <Fragment
102
+ // Effects are order-dependant; we need to reconstruct the nodes.
103
+ key={JSON.stringify([
104
+ correctGeometricError,
105
+ enableClouds,
106
+ enableAerialPerspective,
107
+ enableToneMapping,
108
+ exposure
109
+ ])}
110
+ >
111
+ {enableClouds && (
112
+ <Clouds
113
+ ref={setClouds}
114
+ shadow-farScale={0.25}
115
+ coverage={cloudsCoverage}
116
+ animate
117
+ />
118
+ )}
119
+ {enableAerialPerspective && (
120
+ <AerialPerspective
121
+ sky
122
+ sunLight
123
+ skyLight
124
+ correctGeometricError={correctGeometricError}
125
+ albedoScale={2 / Math.PI}
126
+ />
127
+ )}
128
+ {enableToneMapping && (
129
+ <>
130
+ <ToneMapping mode={exposure > 5 ? 'reinhard' : 'aces'} />
131
+ <SMAA />
132
+ </>
133
+ )}
134
+ </Fragment>
135
+ </MapEffectComposer>
136
+ </Atmosphere>
137
+ )
138
+ }
@@ -0,0 +1,82 @@
1
+ import type { TilesRenderer as TilesRendererImpl } from '3d-tiles-renderer'
2
+ import {
3
+ GLTFExtensionsPlugin,
4
+ GoogleCloudAuthPlugin,
5
+ TileCompressionPlugin,
6
+ TilesFadePlugin,
7
+ UpdateOnChangePlugin
8
+ } from '3d-tiles-renderer/plugins'
9
+ import {
10
+ TilesAttributionOverlay,
11
+ TilesPlugin,
12
+ TilesRenderer
13
+ } from '3d-tiles-renderer/r3f'
14
+ import { useEffect, useState, type FC, type ReactNode, type Ref } from 'react'
15
+ import { mergeRefs } from 'react-merge-refs'
16
+ import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js'
17
+
18
+ import { radians } from '@takram/three-geospatial'
19
+
20
+ import { TileCreasedNormalsPlugin } from '../plugins/TileCreasedNormalsPlugin'
21
+
22
+ const dracoLoader = new DRACOLoader()
23
+ dracoLoader.setDecoderPath('https://www.gstatic.com/draco/v1/decoders/')
24
+
25
+ export interface MapViewportProps {
26
+ apiKey: string
27
+ ref?: Ref<TilesRendererImpl>
28
+ children?: ReactNode
29
+ onLoadError?: () => void
30
+ }
31
+
32
+ export const MapViewport: FC<MapViewportProps> = ({
33
+ apiKey,
34
+ ref,
35
+ children,
36
+ onLoadError
37
+ }) => {
38
+ const [tiles, setTiles] = useState<TilesRendererImpl | null>(null)
39
+
40
+ useEffect(() => {
41
+ if (tiles == null) {
42
+ return
43
+ }
44
+ if (onLoadError) {
45
+ const callback = (): void => {
46
+ onLoadError()
47
+ }
48
+ tiles.addEventListener('load-error', callback)
49
+ return () => {
50
+ tiles.removeEventListener('load-error', callback)
51
+ }
52
+ }
53
+ }, [tiles, onLoadError])
54
+
55
+ return (
56
+ <TilesRenderer
57
+ ref={mergeRefs([ref, setTiles])}
58
+ // Reconstruct tiles when API key changes.
59
+ key={apiKey}
60
+ // The root URL sometimes becomes null without specifying the URL.
61
+ url={`https://tile.googleapis.com/v1/3dtiles/root.json?key=${apiKey}`}
62
+ >
63
+ <TilesPlugin
64
+ plugin={GoogleCloudAuthPlugin}
65
+ args={{
66
+ apiToken: apiKey,
67
+ autoRefreshToken: true
68
+ }}
69
+ />
70
+ <TilesPlugin plugin={GLTFExtensionsPlugin} dracoLoader={dracoLoader} />
71
+ <TilesPlugin plugin={TileCompressionPlugin} />
72
+ <TilesPlugin plugin={UpdateOnChangePlugin} />
73
+ <TilesPlugin plugin={TilesFadePlugin} />
74
+ <TilesPlugin
75
+ plugin={TileCreasedNormalsPlugin}
76
+ args={{ creaseAngle: radians(30) }}
77
+ />
78
+ {children}
79
+ <TilesAttributionOverlay />
80
+ </TilesRenderer>
81
+ )
82
+ }
@@ -0,0 +1,211 @@
1
+ import { useThree } from '@react-three/fiber'
2
+ import { useEffect, useRef, useState, type RefObject } from 'react'
3
+ import type { TilesRenderer } from '3d-tiles-renderer'
4
+ import { Ray, Vector2, Vector3, type Camera } from 'three'
5
+ import { useFrame } from '@react-three/fiber'
6
+
7
+ import { Ellipsoid, Geodetic, degrees } from '@takram/three-geospatial'
8
+
9
+ import { getCameraView } from '../utils/camera'
10
+ import type { CameraView, LatLng } from '../index'
11
+
12
+ export interface MapClickEvent {
13
+ latlng: LatLng
14
+ point: { x: number; y: number }
15
+ }
16
+
17
+ export interface MapHoverEvent {
18
+ latlng: LatLng | null
19
+ point: { x: number; y: number } | null
20
+ }
21
+
22
+ /**
23
+ * Convert mouse event to geodetic coordinates by raycasting to ellipsoid
24
+ */
25
+ function mouseEventToLatLng(
26
+ event: MouseEvent,
27
+ camera: Camera,
28
+ gl: { domElement: HTMLElement }
29
+ ): LatLng | null {
30
+ const rect = gl.domElement.getBoundingClientRect()
31
+ const x = ((event.clientX - rect.left) / rect.width) * 2 - 1
32
+ const y = -((event.clientY - rect.top) / rect.height) * 2 + 1
33
+
34
+ // Create ray from camera through mouse position
35
+ const pointer = new Vector2(x, y)
36
+
37
+ // Get camera position and direction
38
+ const origin = new Vector3().setFromMatrixPosition(camera.matrixWorld)
39
+
40
+ // Unproject mouse coordinates to get direction vector
41
+ // For perspective camera, use near plane (z = -1) and far plane (z = 1)
42
+ const nearPoint = new Vector3(pointer.x, pointer.y, -1)
43
+ const farPoint = new Vector3(pointer.x, pointer.y, 1)
44
+
45
+ // Transform to world space
46
+ nearPoint.unproject(camera)
47
+ farPoint.unproject(camera)
48
+
49
+ // Calculate direction vector
50
+ const direction = farPoint.sub(nearPoint).normalize()
51
+
52
+ // Create ray
53
+ const ray = new Ray(origin, direction)
54
+
55
+ // Find intersection with ellipsoid
56
+ const ellipsoid = Ellipsoid.WGS84
57
+ const intersection = ellipsoid.getIntersection(ray)
58
+
59
+ if (intersection == null) {
60
+ return null
61
+ }
62
+
63
+ // Convert ECEF to geodetic coordinates
64
+ const geodetic = new Geodetic()
65
+ geodetic.setFromECEF(intersection, { ellipsoid })
66
+
67
+ return {
68
+ latitude: degrees(geodetic.latitude),
69
+ longitude: degrees(geodetic.longitude),
70
+ altitude: geodetic.height
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Hook for handling map click events
76
+ */
77
+ export function useMapClick(
78
+ tilesRenderer: RefObject<TilesRenderer | null>,
79
+ onClick?: (event: MapClickEvent) => void
80
+ ): {
81
+ handleClick: (event: MouseEvent) => void
82
+ } {
83
+ const { camera, gl } = useThree()
84
+ const onClickRef = useRef(onClick)
85
+ onClickRef.current = onClick
86
+
87
+ const handleClick = (event: MouseEvent) => {
88
+ if (!onClickRef.current || !tilesRenderer.current) {
89
+ return
90
+ }
91
+
92
+ const latlng = mouseEventToLatLng(event, camera, gl)
93
+
94
+ if (latlng == null) {
95
+ return
96
+ }
97
+
98
+ onClickRef.current({
99
+ latlng,
100
+ point: { x: event.clientX, y: event.clientY }
101
+ })
102
+ }
103
+
104
+ return { handleClick }
105
+ }
106
+
107
+ /**
108
+ * Hook for handling map hover events
109
+ */
110
+ export function useMapHover(
111
+ tilesRenderer: RefObject<TilesRenderer | null>,
112
+ onHover?: (event: MapHoverEvent) => void
113
+ ): {
114
+ handleMouseMove: (event: MouseEvent) => void
115
+ handleMouseLeave: () => void
116
+ hoveredLatLng: LatLng | null
117
+ } {
118
+ const { camera, gl } = useThree()
119
+ const onHoverRef = useRef(onHover)
120
+ onHoverRef.current = onHover
121
+ const [hoveredLatLng, setHoveredLatLng] = useState<LatLng | null>(null)
122
+
123
+ const handleMouseMove = (event: MouseEvent) => {
124
+ if (!tilesRenderer.current) {
125
+ return
126
+ }
127
+
128
+ const latlng = mouseEventToLatLng(event, camera, gl)
129
+
130
+ setHoveredLatLng(latlng)
131
+ if (onHoverRef.current) {
132
+ onHoverRef.current({
133
+ latlng,
134
+ point: { x: event.clientX, y: event.clientY }
135
+ })
136
+ }
137
+ }
138
+
139
+ const handleMouseLeave = () => {
140
+ setHoveredLatLng(null)
141
+ if (onHoverRef.current) {
142
+ onHoverRef.current({
143
+ latlng: null,
144
+ point: null
145
+ })
146
+ }
147
+ }
148
+
149
+ return { handleMouseMove, handleMouseLeave, hoveredLatLng }
150
+ }
151
+
152
+ /**
153
+ * Hook for listening to camera position changes
154
+ */
155
+ export function useMapCameraChange(
156
+ onCameraChange?: (view: CameraView) => void
157
+ ): {
158
+ currentView: CameraView | null
159
+ } {
160
+ const camera = useThree(({ camera }) => camera)
161
+ const onCameraChangeRef = useRef(onCameraChange)
162
+ onCameraChangeRef.current = onCameraChange
163
+ const [currentView, setCurrentView] = useState<CameraView | null>(null)
164
+
165
+ useFrame(() => {
166
+ const view = getCameraView(camera)
167
+ if (view) {
168
+ setCurrentView(view)
169
+ if (onCameraChangeRef.current) {
170
+ onCameraChangeRef.current(view)
171
+ }
172
+ }
173
+ })
174
+
175
+ return { currentView }
176
+ }
177
+
178
+ /**
179
+ * Hook for listening to tile loading events
180
+ */
181
+ export function useMapTileLoad(
182
+ tilesRenderer: RefObject<TilesRenderer | null>,
183
+ onTileLoad?: () => void
184
+ ): {
185
+ tilesLoaded: boolean
186
+ } {
187
+ const [tilesLoaded, setTilesLoaded] = useState(false)
188
+ const onTileLoadRef = useRef(onTileLoad)
189
+ onTileLoadRef.current = onTileLoad
190
+
191
+ useEffect(() => {
192
+ const tiles = tilesRenderer.current
193
+ if (!tiles) {
194
+ return
195
+ }
196
+
197
+ const handleLoadTileset = () => {
198
+ setTilesLoaded(true)
199
+ if (onTileLoadRef.current) {
200
+ onTileLoadRef.current()
201
+ }
202
+ }
203
+
204
+ tiles.addEventListener('load-tileset', handleLoadTileset)
205
+ return () => {
206
+ tiles.removeEventListener('load-tileset', handleLoadTileset)
207
+ }
208
+ }, [tilesRenderer])
209
+
210
+ return { tilesLoaded }
211
+ }
@@ -0,0 +1,55 @@
1
+ // MapKit Public API Surface (v0.1)
2
+
3
+ export type LatLng = {
4
+ latitude: number
5
+ longitude: number
6
+ altitude?: number
7
+ }
8
+
9
+ export type CameraView = {
10
+ latitude: number
11
+ longitude: number
12
+ heading: number
13
+ pitch: number
14
+ distance: number
15
+ fov?: number
16
+ }
17
+
18
+ export type MapEntity = {
19
+ id: string
20
+ type: 'model' | 'billboard'
21
+ latitude: number
22
+ longitude: number
23
+ altitude?: number
24
+ rotation?: [number, number, number]
25
+ scale?: number
26
+ url?: string
27
+ metadata?: Record<string, unknown>
28
+ }
29
+
30
+ // Components
31
+ export { MapViewport } from './components/MapViewport'
32
+ export type { MapViewportProps } from './components/MapViewport'
33
+
34
+ export { MapScene } from './components/MapScene'
35
+ export type { MapSceneProps } from './components/MapScene'
36
+
37
+ export { MapEntity } from './components/MapEntity'
38
+ export type { MapEntityProps } from './components/MapEntity'
39
+
40
+ // Utilities
41
+ export { flyTo } from './utils/flyTo'
42
+ export { setCameraView, getCameraView } from './utils/camera'
43
+ export { createPreset, getPreset, commonPresets } from './utils/presets'
44
+
45
+ // Hooks
46
+ export {
47
+ useMapClick,
48
+ useMapHover,
49
+ useMapCameraChange,
50
+ useMapTileLoad
51
+ } from './hooks/useMapEvents'
52
+ export type {
53
+ MapClickEvent,
54
+ MapHoverEvent
55
+ } from './hooks/useMapEvents'