@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,64 @@
1
+ import { Vector3 } from 'three'
2
+ import { toCreasedNormals as toCreasedNormalsImpl } from 'three/addons/utils/BufferGeometryUtils.js'
3
+ import type { Tile } from '3d-tiles-renderer'
4
+ import { BufferGeometry, Mesh, type Object3D } from 'three'
5
+
6
+ export interface TileCreasedNormalsPluginOptions {
7
+ creaseAngle?: number
8
+ }
9
+
10
+ async function toCreasedNormalsAsync(
11
+ geometry: BufferGeometry,
12
+ creaseAngle?: number
13
+ ): Promise<BufferGeometry> {
14
+ const result = toCreasedNormalsImpl(geometry, creaseAngle)
15
+
16
+ // Triangles can be degenerate lines, producing zero normals and eventually
17
+ // causing NaN values under SH. Fix this by replacing them with a non-zero
18
+ // normal (they are hardly visible because they are degenerate).
19
+ // See: https://github.com/takram-design-engineering/three-geospatial/issues/13
20
+ const normal = result.getAttribute('normal')
21
+ if (normal) {
22
+ const v0 = new Vector3()
23
+ const v1 = new Vector3()
24
+ const v2 = new Vector3()
25
+ for (let i = 0; i < normal.count; i += 3) {
26
+ v0.fromBufferAttribute(normal, i + 0)
27
+ v1.fromBufferAttribute(normal, i + 1)
28
+ v2.fromBufferAttribute(normal, i + 2)
29
+ if (v0.length() < 0.5 || v1.length() < 0.5 || v2.length() < 0.5) {
30
+ normal.setXYZ(i + 0, 0, 0, 1)
31
+ normal.setXYZ(i + 1, 0, 0, 1)
32
+ normal.setXYZ(i + 2, 0, 0, 1)
33
+ }
34
+ }
35
+ }
36
+
37
+ return result
38
+ }
39
+
40
+ export class TileCreasedNormalsPlugin {
41
+ readonly options: TileCreasedNormalsPluginOptions
42
+
43
+ constructor(options?: TileCreasedNormalsPluginOptions) {
44
+ this.options = { ...options }
45
+ }
46
+
47
+ // Plugin method
48
+ async processTileModel(scene: Object3D, tile: Tile): Promise<void> {
49
+ const meshes: Mesh[] = []
50
+ scene.traverse(object => {
51
+ if (object instanceof Mesh && object.geometry instanceof BufferGeometry) {
52
+ meshes.push(object)
53
+ }
54
+ })
55
+ await Promise.all(
56
+ meshes.map(async mesh => {
57
+ mesh.geometry = await toCreasedNormalsAsync(
58
+ mesh.geometry,
59
+ this.options.creaseAngle
60
+ )
61
+ })
62
+ )
63
+ }
64
+ }
@@ -0,0 +1,60 @@
1
+ import { type Camera } from 'three'
2
+ import { Geodetic, PointOfView, radians, degrees, Ellipsoid } from '@takram/three-geospatial'
3
+
4
+ import type { CameraView } from '../index'
5
+
6
+ /**
7
+ * Set camera position and orientation from a CameraView
8
+ */
9
+ export function setCameraView(
10
+ camera: Camera,
11
+ view: CameraView,
12
+ ellipsoid: Ellipsoid = Ellipsoid.WGS84
13
+ ): void {
14
+ new PointOfView(
15
+ view.distance,
16
+ radians(view.heading),
17
+ radians(view.pitch)
18
+ ).decompose(
19
+ new Geodetic(radians(view.longitude), radians(view.latitude)).toECEF(
20
+ undefined,
21
+ { ellipsoid }
22
+ ),
23
+ camera.position,
24
+ camera.quaternion,
25
+ camera.up
26
+ )
27
+
28
+ if (view.fov != null && 'fov' in camera && typeof camera.fov === 'number') {
29
+ camera.fov = view.fov
30
+ if ('updateProjectionMatrix' in camera && typeof camera.updateProjectionMatrix === 'function') {
31
+ camera.updateProjectionMatrix()
32
+ }
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get current camera view
38
+ */
39
+ export function getCameraView(
40
+ camera: Camera,
41
+ ellipsoid: Ellipsoid = Ellipsoid.WGS84
42
+ ): CameraView | null {
43
+ const pov = new PointOfView()
44
+ const result = pov.setFromCamera(camera, ellipsoid)
45
+ if (result == null) {
46
+ return null
47
+ }
48
+
49
+ const geodetic = new Geodetic()
50
+ geodetic.setFromECEF(camera.position, { ellipsoid })
51
+
52
+ return {
53
+ latitude: degrees(geodetic.latitude),
54
+ longitude: degrees(geodetic.longitude),
55
+ heading: degrees(pov.heading),
56
+ pitch: degrees(pov.pitch),
57
+ distance: pov.distance,
58
+ fov: 'fov' in camera && typeof camera.fov === 'number' ? camera.fov : undefined
59
+ }
60
+ }
@@ -0,0 +1,118 @@
1
+ import type { Camera } from 'three'
2
+ import type { GlobeControls } from '3d-tiles-renderer'
3
+
4
+ import { setCameraView, getCameraView } from './camera'
5
+ import type { CameraView } from '../index'
6
+
7
+ export interface FlyToOptions {
8
+ duration?: number
9
+ easing?: (t: number) => number
10
+ }
11
+
12
+ /**
13
+ * Interpolate angle using shortest path (handles wrapping at 360°)
14
+ * Returns the interpolated angle in degrees
15
+ */
16
+ function interpolateAngle(start: number, end: number, t: number): number {
17
+ // Normalize angles to [0, 360)
18
+ const normalize = (angle: number) => {
19
+ angle = angle % 360
20
+ return angle < 0 ? angle + 360 : angle
21
+ }
22
+
23
+ const startNorm = normalize(start)
24
+ const endNorm = normalize(end)
25
+
26
+ // Calculate the difference
27
+ let diff = endNorm - startNorm
28
+
29
+ // Find shortest path: if difference > 180°, go the other way
30
+ if (diff > 180) {
31
+ diff -= 360
32
+ } else if (diff < -180) {
33
+ diff += 360
34
+ }
35
+
36
+ // Interpolate and normalize result
37
+ const result = normalize(startNorm + diff * t)
38
+ return result
39
+ }
40
+
41
+ /**
42
+ * Interpolate longitude using shortest path (handles wrapping at ±180°)
43
+ * Returns the interpolated longitude in degrees
44
+ */
45
+ function interpolateLongitude(start: number, end: number, t: number): number {
46
+ // Normalize longitudes to [-180, 180)
47
+ const normalize = (lon: number) => {
48
+ lon = lon % 360
49
+ if (lon > 180) lon -= 360
50
+ if (lon < -180) lon += 360
51
+ return lon
52
+ }
53
+
54
+ const startNorm = normalize(start)
55
+ const endNorm = normalize(end)
56
+
57
+ // Calculate the difference
58
+ let diff = endNorm - startNorm
59
+
60
+ // Find shortest path: if difference > 180°, go the other way
61
+ if (diff > 180) {
62
+ diff -= 360
63
+ } else if (diff < -180) {
64
+ diff += 360
65
+ }
66
+
67
+ // Interpolate and normalize result
68
+ return normalize(startNorm + diff * t)
69
+ }
70
+
71
+ /**
72
+ * Imperative flyTo function for animating camera to a target view
73
+ * Returns a promise that resolves when animation completes
74
+ */
75
+ export async function flyTo(
76
+ camera: Camera,
77
+ controls: GlobeControls | null,
78
+ targetView: CameraView,
79
+ options?: FlyToOptions
80
+ ): Promise<void> {
81
+ const duration = options?.duration ?? 2000
82
+ const easing = options?.easing ?? ((t: number) => t * (2 - t)) // ease-out
83
+ const startTime = Date.now()
84
+
85
+ const startView = getCameraView(camera)
86
+ if (startView == null) {
87
+ setCameraView(camera, targetView)
88
+ return Promise.resolve()
89
+ }
90
+
91
+ return new Promise(resolve => {
92
+ const animate = () => {
93
+ const elapsed = Date.now() - startTime
94
+ const progress = Math.min(elapsed / duration, 1)
95
+ const eased = easing(progress)
96
+
97
+ const currentView: CameraView = {
98
+ latitude: startView.latitude + (targetView.latitude - startView.latitude) * eased,
99
+ longitude: interpolateLongitude(startView.longitude, targetView.longitude, eased),
100
+ heading: interpolateAngle(startView.heading, targetView.heading, eased),
101
+ pitch: startView.pitch + (targetView.pitch - startView.pitch) * eased,
102
+ distance: startView.distance + (targetView.distance - startView.distance) * eased,
103
+ fov: targetView.fov != null
104
+ ? (startView.fov ?? 50) + (targetView.fov - (startView.fov ?? 50)) * eased
105
+ : undefined
106
+ }
107
+
108
+ setCameraView(camera, currentView)
109
+
110
+ if (progress < 1) {
111
+ requestAnimationFrame(animate)
112
+ } else {
113
+ resolve()
114
+ }
115
+ }
116
+ animate()
117
+ })
118
+ }
@@ -0,0 +1,63 @@
1
+ import type { CameraView } from '../index'
2
+
3
+ const presets = new Map<string, CameraView>()
4
+
5
+ /**
6
+ * Create a named camera preset
7
+ */
8
+ export function createPreset(name: string, view: CameraView): void {
9
+ presets.set(name, { ...view })
10
+ }
11
+
12
+ /**
13
+ * Get a preset by name
14
+ */
15
+ export function getPreset(name: string): CameraView | undefined {
16
+ return presets.get(name)
17
+ }
18
+
19
+ /**
20
+ * Common location presets
21
+ */
22
+ export const commonPresets: Record<string, CameraView> = {
23
+ tokyo: {
24
+ latitude: 35.6812,
25
+ longitude: 139.7671,
26
+ heading: 180,
27
+ pitch: -30,
28
+ distance: 4500
29
+ },
30
+ london: {
31
+ latitude: 51.5074,
32
+ longitude: -0.1278,
33
+ heading: 0,
34
+ pitch: -30,
35
+ distance: 5000
36
+ },
37
+ 'new-york': {
38
+ latitude: 40.7128,
39
+ longitude: -74.0060,
40
+ heading: 0,
41
+ pitch: -30,
42
+ distance: 5000
43
+ },
44
+ paris: {
45
+ latitude: 48.8566,
46
+ longitude: 2.3522,
47
+ heading: 0,
48
+ pitch: -30,
49
+ distance: 5000
50
+ },
51
+ sydney: {
52
+ latitude: -33.8688,
53
+ longitude: 151.2093,
54
+ heading: 0,
55
+ pitch: -30,
56
+ distance: 5000
57
+ }
58
+ }
59
+
60
+ // Initialize common presets
61
+ Object.entries(commonPresets).forEach(([name, view]) => {
62
+ createPreset(name, view)
63
+ })
@@ -0,0 +1,12 @@
1
+ import { Camera } from 'three';
2
+ import { Ellipsoid } from '@takram/three-geospatial';
3
+ import { CameraView } from '../index';
4
+ /**
5
+ * Set camera position and orientation from a CameraView
6
+ */
7
+ export declare function setCameraView(camera: Camera, view: CameraView, ellipsoid?: Ellipsoid): void;
8
+ /**
9
+ * Get current camera view
10
+ */
11
+ export declare function getCameraView(camera: Camera, ellipsoid?: Ellipsoid): CameraView | null;
12
+ //# sourceMappingURL=camera.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera.d.ts","sourceRoot":"","sources":["../../src/utils/camera.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,EAA2C,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAE7F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C;;GAEG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,UAAU,EAChB,SAAS,GAAE,SAA2B,GACrC,IAAI,CAqBN;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,SAA2B,GACrC,UAAU,GAAG,IAAI,CAkBnB"}
@@ -0,0 +1,13 @@
1
+ import { Camera } from 'three';
2
+ import { GlobeControls } from '3d-tiles-renderer';
3
+ import { CameraView } from '../index';
4
+ export interface FlyToOptions {
5
+ duration?: number;
6
+ easing?: (t: number) => number;
7
+ }
8
+ /**
9
+ * Imperative flyTo function for animating camera to a target view
10
+ * Returns a promise that resolves when animation completes
11
+ */
12
+ export declare function flyTo(camera: Camera, controls: GlobeControls | null, targetView: CameraView, options?: FlyToOptions): Promise<void>;
13
+ //# sourceMappingURL=flyTo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flyTo.d.ts","sourceRoot":"","sources":["../../src/utils/flyTo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AACnC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAGtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAE1C,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;CAC/B;AA6DD;;;GAGG;AACH,wBAAsB,KAAK,CACzB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,aAAa,GAAG,IAAI,EAC9B,UAAU,EAAE,UAAU,EACtB,OAAO,CAAC,EAAE,YAAY,GACrB,OAAO,CAAC,IAAI,CAAC,CAsCf"}
@@ -0,0 +1,14 @@
1
+ import { CameraView } from '../index';
2
+ /**
3
+ * Create a named camera preset
4
+ */
5
+ export declare function createPreset(name: string, view: CameraView): void;
6
+ /**
7
+ * Get a preset by name
8
+ */
9
+ export declare function getPreset(name: string): CameraView | undefined;
10
+ /**
11
+ * Common location presets
12
+ */
13
+ export declare const commonPresets: Record<string, CameraView>;
14
+ //# sourceMappingURL=presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presets.d.ts","sourceRoot":"","sources":["../../src/utils/presets.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAI1C;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,IAAI,CAEjE;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAE9D;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAoCpD,CAAA"}
package/package.json ADDED
@@ -0,0 +1,59 @@
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": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/more-than-software/citykit.git",
22
+ "directory": "packages/mapkit"
23
+ },
24
+ "keywords": [
25
+ "react",
26
+ "three.js",
27
+ "3d-tiles",
28
+ "google-maps",
29
+ "geospatial",
30
+ "globe",
31
+ "map",
32
+ "react-three-fiber"
33
+ ],
34
+ "dependencies": {
35
+ "3d-tiles-renderer": "0.4.21",
36
+ "@takram/three-atmosphere": "^0.16.0",
37
+ "@takram/three-clouds": "^0.16.0",
38
+ "@takram/three-geospatial": "^0.6.0",
39
+ "@takram/three-geospatial-effects": "^0.6.0",
40
+ "motion": "^12.34.2",
41
+ "postprocessing": "^6.38.2",
42
+ "react-merge-refs": "^3.0.2",
43
+ "tiny-invariant": "^1.3.3"
44
+ },
45
+ "peerDependencies": {
46
+ "@react-three/drei": "^10.0.0",
47
+ "@react-three/fiber": "^9.0.0",
48
+ "@react-three/postprocessing": "^3.0.0",
49
+ "react": "^18.0.0 || ^19.0.0",
50
+ "react-dom": "^18.0.0 || ^19.0.0",
51
+ "three": "^0.180.0"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "scripts": {
57
+ "build": "vite build"
58
+ }
59
+ }