@jkwd/inbase 0.1.4 → 0.1.6

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.
@@ -1,12 +1,11 @@
1
1
  import { Suspense } from 'react'
2
2
  import { Billboard, Edges, Html, Text } from '@react-three/drei'
3
- import { CHANGE_HIGHLIGHT, CONFIG, dimColor, fileColor, type ChangeKind } from '../theme'
3
+ import { CHANGE_HIGHLIGHT, CONFIG, dimColor, fileColor, FILE_SELECTION, MAP_SELECTION, type ChangeKind } from '../theme'
4
4
  import { NameInput } from '../ui/NameInput'
5
5
  import { MapSelectBorder } from './MapSelectBorder'
6
6
  import type { FileNode } from '../types'
7
7
  import type { PlacedFile } from '../types'
8
8
 
9
- const BAR = 0.22
10
9
  const SIDE_LABEL_PAD = 0.05
11
10
  const SIDE_LABEL_MIN_HEIGHT = CONFIG.eyeHeight * 2.5
12
11
  const SIDE_LABELS: { rotationY: number; axis: 'x' | 'z'; sign: 1 | -1 }[] = [
@@ -28,51 +27,13 @@ type FileBlockProps = {
28
27
  dimmed: boolean
29
28
  naming?: boolean
30
29
  mapMode?: boolean
30
+ highlightMapChange?: boolean
31
+ previewLabels?: boolean
32
+ labelVisible?: boolean
31
33
  onCommitName?: (name: string) => void
32
34
  onCancelName?: () => void
33
35
  }
34
36
 
35
- function PlannedFrame({
36
- width,
37
- height,
38
- depth,
39
- color,
40
- }: {
41
- width: number
42
- height: number
43
- depth: number
44
- color: string
45
- }) {
46
- const ox = width / 2 + BAR / 2
47
- const oy = height / 2 + BAR / 2
48
- const oz = depth / 2 + BAR / 2
49
- const bars: [number, number, number, number, number, number][] = [
50
- [ox, 0, oz, BAR, height + BAR * 2, BAR],
51
- [-ox, 0, oz, BAR, height + BAR * 2, BAR],
52
- [ox, 0, -oz, BAR, height + BAR * 2, BAR],
53
- [-ox, 0, -oz, BAR, height + BAR * 2, BAR],
54
- [0, oy, oz, width + BAR * 2, BAR, BAR],
55
- [0, oy, -oz, width + BAR * 2, BAR, BAR],
56
- [ox, oy, 0, BAR, BAR, depth],
57
- [-ox, oy, 0, BAR, BAR, depth],
58
- [0, -oy, oz, width + BAR * 2, BAR, BAR],
59
- [0, -oy, -oz, width + BAR * 2, BAR, BAR],
60
- [ox, -oy, 0, BAR, BAR, depth],
61
- [-ox, -oy, 0, BAR, BAR, depth],
62
- ]
63
-
64
- return (
65
- <group>
66
- {bars.map(([x, y, z, w, h, d], index) => (
67
- <mesh key={index} position={[x, y, z]}>
68
- <boxGeometry args={[w, h, d]} />
69
- <meshBasicMaterial color={color} toneMapped={false} />
70
- </mesh>
71
- ))}
72
- </group>
73
- )
74
- }
75
-
76
37
  function fileLabel(name: string, changeKind: ChangeKind | null, added: boolean) {
77
38
  if (changeKind === 'remove') return `- ${name}`
78
39
  if (changeKind === 'add' || added) return `+ ${name}`
@@ -91,15 +52,19 @@ export function FileBlock({
91
52
  dimmed,
92
53
  naming = false,
93
54
  mapMode = false,
55
+ highlightMapChange = mapMode,
56
+ previewLabels = false,
57
+ labelVisible = true,
94
58
  onCommitName,
95
59
  onCancelName,
96
60
  }: FileBlockProps) {
97
61
  const [width, height, depth] = placed.size
98
- const highlight = changeKind && mapMode ? CHANGE_HIGHLIGHT[changeKind] : null
62
+ const highlight =
63
+ changeKind && highlightMapChange && !selected
64
+ ? CHANGE_HIGHLIGHT[changeKind]
65
+ : null
99
66
  const color = added || file.userCreated ? '#7ec8e8' : fileColor(file.language)
100
67
  const muted = dimColor(color, 0.32)
101
- const walkSelected = selected && !mapMode
102
- const mapSelected = selected && mapMode
103
68
  const label = fileLabel(file.name, changeKind, added || Boolean(file.userCreated))
104
69
  const labelColor = aimed
105
70
  ? '#9ad8ff'
@@ -112,62 +77,58 @@ export function FileBlock({
112
77
  ? '#9ad8ff'
113
78
  : highlight
114
79
  ? highlight.color
115
- : walkSelected
116
- ? '#5d9ec4'
80
+ : selected
81
+ ? FILE_SELECTION.color
117
82
  : related
118
83
  ? '#4e7f72'
119
84
  : dimmed
120
85
  ? muted
121
86
  : color
122
87
 
88
+ const showLabels = !naming && !mapMode
89
+
123
90
  return (
124
91
  <group position={placed.position}>
125
- {highlight && !mapSelected && (
126
- <mesh
127
- position={[0, height / 2 + 0.04, 0]}
128
- rotation={[-Math.PI / 2, 0, 0]}
129
- userData={{ fileId: file.id }}
130
- >
131
- <planeGeometry args={[width + 0.7, depth + 0.7]} />
132
- <meshBasicMaterial color={highlight.color} toneMapped={false} />
133
- </mesh>
134
- )}
135
- <mesh userData={{ fileId: file.id }}>
136
- <boxGeometry args={[width, height, depth]} />
137
- <meshLambertMaterial
138
- color={meshColor}
139
- emissive={
140
- aimed
141
- ? '#3a6a80'
142
- : highlight
143
- ? highlight.emissive
144
- : walkSelected
145
- ? '#2a4a5c'
146
- : related
147
- ? '#1f4a44'
148
- : added || file.userCreated
149
- ? '#2a5064'
150
- : dimmed
151
- ? muted
152
- : color
153
- }
154
- emissiveIntensity={
155
- aimed
156
- ? 0.45
157
- : highlight
158
- ? 0.95
159
- : walkSelected
160
- ? 0.28
161
- : related
162
- ? 0.18
163
- : added || file.userCreated
164
- ? 0.22
165
- : dimmed
166
- ? 0.08
167
- : 0.12
168
- }
169
- />
170
- {walkSelected && (
92
+ <mesh userData={{ fileId: file.id }} scale={[width, height, depth]}>
93
+ <boxGeometry args={[1, 1, 1]} />
94
+ {mapMode ? (
95
+ <meshBasicMaterial color={meshColor} toneMapped={false} />
96
+ ) : (
97
+ <meshLambertMaterial
98
+ color={meshColor}
99
+ emissive={
100
+ aimed
101
+ ? '#3a6a80'
102
+ : highlight
103
+ ? highlight.emissive
104
+ : selected
105
+ ? FILE_SELECTION.emissive
106
+ : related
107
+ ? '#1f4a44'
108
+ : added || file.userCreated
109
+ ? '#2a5064'
110
+ : dimmed
111
+ ? muted
112
+ : color
113
+ }
114
+ emissiveIntensity={
115
+ aimed
116
+ ? 0.45
117
+ : highlight
118
+ ? 0.95
119
+ : selected
120
+ ? 0.55
121
+ : related
122
+ ? 0.18
123
+ : added || file.userCreated
124
+ ? 0.22
125
+ : dimmed
126
+ ? 0.08
127
+ : 0.12
128
+ }
129
+ />
130
+ )}
131
+ {selected && !mapMode && (
171
132
  <Edges
172
133
  color="#000000"
173
134
  dashed
@@ -175,26 +136,18 @@ export function FileBlock({
175
136
  gapSize={0.1}
176
137
  lineWidth={2.5}
177
138
  toneMapped={false}
178
- scale={1.002}
179
139
  />
180
140
  )}
181
141
  </mesh>
182
- {mapSelected && (
142
+ {selected && mapMode && (
183
143
  <MapSelectBorder
184
144
  width={width}
185
145
  depth={depth}
186
- y={height / 2 + 0.05}
146
+ y={height / 2 + 0.03}
147
+ stroke={MAP_SELECTION.blockPad}
187
148
  userData={{ fileId: file.id }}
188
149
  />
189
150
  )}
190
- {highlight && !mapSelected && (
191
- <PlannedFrame
192
- width={width}
193
- height={height}
194
- depth={depth}
195
- color={highlight.color}
196
- />
197
- )}
198
151
  {naming && onCommitName && onCancelName && (
199
152
  <Html
200
153
  position={[0, height / 2 + 0.42, 0]}
@@ -210,47 +163,63 @@ export function FileBlock({
210
163
  />
211
164
  </Html>
212
165
  )}
213
- <Suspense fallback={null}>
214
- {!naming && (
215
- <Billboard position={[0, height / 2 + (planned || highlight ? 0.55 : 0.38), 0]}>
216
- <Text
217
- fontSize={0.28}
218
- color={labelColor}
219
- anchorX="center"
220
- anchorY="bottom"
221
- maxWidth={3.4}
222
- >
223
- {label}
224
- </Text>
225
- </Billboard>
226
- )}
227
- {height >= SIDE_LABEL_MIN_HEIGHT &&
228
- SIDE_LABELS.map((side) => {
229
- const face = side.axis === 'x' ? width : depth
230
- const x =
231
- side.axis === 'x' ? side.sign * (width / 2 + SIDE_LABEL_PAD) : 0
232
- const z =
233
- side.axis === 'z' ? side.sign * (depth / 2 + SIDE_LABEL_PAD) : 0
234
- return (
235
- <Text
236
- key={`${side.axis}:${side.sign}`}
237
- position={[x, -height / 2 + 0.22, z]}
238
- rotation={[0, side.rotationY, 0]}
239
- fontSize={0.22}
240
- color={labelColor}
241
- anchorX="center"
242
- anchorY="middle"
243
- maxWidth={face - 0.2}
244
- overflowWrap="break-word"
245
- outlineWidth={0.012}
246
- outlineColor="#11151c"
247
- depthOffset={-1}
166
+ {showLabels && (
167
+ <Suspense fallback={null}>
168
+ {previewLabels ? (
169
+ labelVisible && (
170
+ <Html
171
+ position={[0, height / 2 + 0.4, 0]}
172
+ center
173
+ occlude={false}
174
+ style={{ pointerEvents: 'none' }}
175
+ zIndexRange={[20, 0]}
248
176
  >
249
- {label}
250
- </Text>
177
+ <div className="thumbnail-block-label">{label}</div>
178
+ </Html>
251
179
  )
252
- })}
253
- </Suspense>
180
+ ) : (
181
+ <>
182
+ <Billboard position={[0, height / 2 + (planned || highlight ? 0.55 : 0.38), 0]}>
183
+ <Text
184
+ fontSize={0.28}
185
+ color={labelColor}
186
+ anchorX="center"
187
+ anchorY="bottom"
188
+ maxWidth={3.4}
189
+ >
190
+ {label}
191
+ </Text>
192
+ </Billboard>
193
+ {height >= SIDE_LABEL_MIN_HEIGHT &&
194
+ SIDE_LABELS.map((side) => {
195
+ const face = side.axis === 'x' ? width : depth
196
+ const x =
197
+ side.axis === 'x' ? side.sign * (width / 2 + SIDE_LABEL_PAD) : 0
198
+ const z =
199
+ side.axis === 'z' ? side.sign * (depth / 2 + SIDE_LABEL_PAD) : 0
200
+ return (
201
+ <Text
202
+ key={`${side.axis}:${side.sign}`}
203
+ position={[x, -height / 2 + 0.22, z]}
204
+ rotation={[0, side.rotationY, 0]}
205
+ fontSize={0.22}
206
+ color={labelColor}
207
+ anchorX="center"
208
+ anchorY="middle"
209
+ maxWidth={face - 0.2}
210
+ overflowWrap="break-word"
211
+ outlineWidth={0.012}
212
+ outlineColor="#11151c"
213
+ depthOffset={-1}
214
+ >
215
+ {label}
216
+ </Text>
217
+ )
218
+ })}
219
+ </>
220
+ )}
221
+ </Suspense>
222
+ )}
254
223
  </group>
255
224
  )
256
225
  }
@@ -1,6 +1,13 @@
1
1
  import { Suspense } from 'react'
2
- import { Text } from '@react-three/drei'
3
- import { CHANGE_HIGHLIGHT, folderFloorColor, MAP_SELECTION, type ChangeKind } from '../theme'
2
+ import { Html, Text } from '@react-three/drei'
3
+ import {
4
+ CHANGE_HIGHLIGHT,
5
+ CONFIG,
6
+ folderAisleColor,
7
+ folderFloorColor,
8
+ MAP_SELECTION,
9
+ type ChangeKind,
10
+ } from '../theme'
4
11
  import type { PlacedFolder } from '../types'
5
12
  import { MapSelectBorder } from './MapSelectBorder'
6
13
 
@@ -10,6 +17,7 @@ type FolderAreaProps = {
10
17
  selected?: boolean
11
18
  mapMode?: boolean
12
19
  highlightKind?: ChangeKind | null
20
+ previewLabels?: boolean
13
21
  }
14
22
 
15
23
  export function FolderArea({
@@ -18,10 +26,10 @@ export function FolderArea({
18
26
  selected = false,
19
27
  mapMode = false,
20
28
  highlightKind = null,
29
+ previewLabels = false,
21
30
  }: FolderAreaProps) {
22
31
  const added = Boolean(folder.added)
23
- const highlight =
24
- mapMode && highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
32
+ const highlight = highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
25
33
  const addedOnly = added && !highlight
26
34
  const outline = highlight ? highlight.color : addedOnly ? '#7ec8e8' : null
27
35
  const color = highlight
@@ -29,6 +37,11 @@ export function FolderArea({
29
37
  : addedOnly
30
38
  ? '#16323f'
31
39
  : folderFloorColor(folder.path)
40
+ const aisle = highlight
41
+ ? highlight.aisle
42
+ : addedOnly
43
+ ? '#23485a'
44
+ : folderAisleColor(folder.path)
32
45
  const label =
33
46
  highlightKind === 'remove'
34
47
  ? `- ${folder.name}`
@@ -57,29 +70,35 @@ export function FolderArea({
57
70
  depth={folder.depth}
58
71
  y={0.06}
59
72
  stroke={MAP_SELECTION.islandPad}
73
+ color={MAP_SELECTION.island}
60
74
  />
61
75
  )}
62
76
  <mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
63
- <planeGeometry args={[2.2, folder.depth - 1.5]} />
64
- <meshBasicMaterial
65
- color={highlight ? highlight.aisle : addedOnly ? '#23485a' : '#2a3340'}
66
- />
77
+ <planeGeometry args={[CONFIG.bridgeWidth, folder.depth]} />
78
+ <meshBasicMaterial color={aisle} />
67
79
  </mesh>
68
80
  {folder.width > 28 && (
69
81
  <mesh
70
- position={[0, 0.02, folder.depth / 2 - 1.3]}
82
+ position={[0, 0.02, folder.depth / 2 - CONFIG.bridgeWidth / 2]}
71
83
  rotation={[-Math.PI / 2, 0, 0]}
72
84
  >
73
- <planeGeometry args={[folder.width - 2.4, 2.4]} />
74
- <meshBasicMaterial
75
- color={
76
- highlight ? highlight.aisle : addedOnly ? '#23485a' : '#2a3340'
77
- }
78
- />
85
+ <planeGeometry args={[folder.width, CONFIG.bridgeWidth]} />
86
+ <meshBasicMaterial color={aisle} />
79
87
  </mesh>
80
88
  )}
81
89
  <Suspense fallback={null}>
82
- {!naming && (
90
+ {!naming && previewLabels && (
91
+ <Html
92
+ position={[0, 1.35, -folder.depth / 2 + 1.6]}
93
+ center
94
+ occlude={false}
95
+ style={{ pointerEvents: 'none' }}
96
+ zIndexRange={[20, 0]}
97
+ >
98
+ <div className="thumbnail-folder-label">{label}</div>
99
+ </Html>
100
+ )}
101
+ {!naming && !previewLabels && !mapMode && (
83
102
  <Text
84
103
  position={[0, 0.05, -folder.depth / 2 + 1.6]}
85
104
  rotation={[-Math.PI / 2, 0, 0]}
@@ -7,6 +7,7 @@ type MapSelectBorderProps = {
7
7
  depth: number
8
8
  y?: number
9
9
  stroke?: number
10
+ color?: string
10
11
  userData?: Record<string, unknown>
11
12
  }
12
13
 
@@ -15,6 +16,7 @@ export function MapSelectBorder({
15
16
  depth,
16
17
  y = 0.04,
17
18
  stroke = MAP_SELECTION.blockPad,
19
+ color = MAP_SELECTION.color,
18
20
  userData,
19
21
  }: MapSelectBorderProps) {
20
22
  const geometry = useMemo(() => {
@@ -47,7 +49,7 @@ export function MapSelectBorder({
47
49
  userData={userData}
48
50
  >
49
51
  <meshBasicMaterial
50
- color={MAP_SELECTION.color}
52
+ color={color}
51
53
  toneMapped={false}
52
54
  side={THREE.DoubleSide}
53
55
  depthWrite={false}
@@ -30,7 +30,7 @@ export function MapView({
30
30
  onTravelTo,
31
31
  }: MapViewProps) {
32
32
  const size = useThree((state) => state.size)
33
- const { camera, gl, scene } = useThree()
33
+ const { camera, gl, invalidate, scene } = useThree()
34
34
  const bounds = useMemo(() => worldBounds(layout), [layout])
35
35
  const drag = useRef({ x: 0, y: 0, moved: false, active: false })
36
36
  const sized = size.width > 16 && size.height > 16
@@ -49,6 +49,7 @@ export function MapView({
49
49
  // Ortho up is -Z, so +Z is down the screen. Shift the view so the map sits
50
50
  // above the bottom HUD instead of centering under it.
51
51
  const cz = bounds.cz + hudReserve / 2 / zoom
52
+ const controlsRef = useRef<{ target: THREE.Vector3; update: () => void }>(null)
52
53
 
53
54
  useLayoutEffect(() => {
54
55
  if (!enabled || !(camera instanceof THREE.OrthographicCamera)) return
@@ -59,15 +60,19 @@ export function MapView({
59
60
  camera.far = 2000
60
61
  camera.zoom = zoom
61
62
  camera.updateProjectionMatrix()
62
- }, [bounds.cx, camera, cz, enabled, zoom])
63
+ const controls = controlsRef.current
64
+ if (controls) {
65
+ controls.target.set(bounds.cx, 0, cz)
66
+ controls.update()
67
+ }
68
+ }, [bounds.cx, camera, cz, enabled, sized, zoom])
63
69
 
64
70
  useEffect(() => {
65
71
  if (!enabled) return
66
72
  const element = gl.domElement
67
73
  element.style.cursor = 'grab'
68
74
 
69
- const isWalkClick = (event: PointerEvent | MouseEvent) =>
70
- event.shiftKey || event.ctrlKey || event.metaKey
75
+ const isWalkClick = (event: PointerEvent | MouseEvent) => event.ctrlKey
71
76
 
72
77
  const isWalkButton = (event: PointerEvent) =>
73
78
  event.button === 0 || (event.ctrlKey && event.button === 2)
@@ -111,6 +116,7 @@ export function MapView({
111
116
  const pick = pickAt(clientX, clientY)
112
117
  if (!pick) return false
113
118
  const { raycaster, relationHit, fileHit } = pick
119
+ if (fileHit) return false
114
120
 
115
121
  const hit = new THREE.Vector3()
116
122
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
@@ -121,7 +127,7 @@ export function MapView({
121
127
  landAt(hit.x, hit.z)
122
128
  return true
123
129
  }
124
- if (relationHit || fileHit) return false
130
+ if (relationHit) return false
125
131
 
126
132
  landAt(hit.x, hit.z)
127
133
  return true
@@ -140,20 +146,17 @@ export function MapView({
140
146
  const pick = pickAt(event.clientX, event.clientY)
141
147
  if (!pick) return
142
148
  const { relationHit, fileHit } = pick
143
- if (
144
- relationHit &&
145
- (!fileHit || relationHit.distance <= fileHit.distance + 0.4)
146
- ) {
149
+ if (fileHit) {
150
+ onSelect(fileHit.object.userData.fileId as string)
151
+ return
152
+ }
153
+ if (relationHit) {
147
154
  onTravelTo(
148
155
  relationHit.object.userData.relationFrom as string,
149
156
  relationHit.object.userData.relationTo as string,
150
157
  )
151
158
  return
152
159
  }
153
- if (fileHit) {
154
- onSelect(fileHit.object.userData.fileId as string)
155
- return
156
- }
157
160
 
158
161
  const hit = new THREE.Vector3()
159
162
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
@@ -172,21 +175,79 @@ export function MapView({
172
175
  if (event.ctrlKey) event.preventDefault()
173
176
  }
174
177
 
178
+ const ground = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
179
+ const ndc = new THREE.Vector2()
180
+ const before = new THREE.Vector3()
181
+ const after = new THREE.Vector3()
182
+ const raycaster = new THREE.Raycaster()
183
+ const minZoom = Math.max(fitZoom * 0.35, 0.05)
184
+ const maxZoom = Math.max(fitZoom * 10, 20)
185
+ const zoomScale = Math.pow(0.95, 1.15)
186
+
187
+ const worldUnderCursor = (clientX: number, clientY: number, target: THREE.Vector3) => {
188
+ const rect = element.getBoundingClientRect()
189
+ if (rect.width < 2 || rect.height < 2) return false
190
+ ndc.set(
191
+ ((clientX - rect.left) / rect.width) * 2 - 1,
192
+ -((clientY - rect.top) / rect.height) * 2 + 1,
193
+ )
194
+ raycaster.setFromCamera(ndc, camera)
195
+ return Boolean(raycaster.ray.intersectPlane(ground, target))
196
+ }
197
+
198
+ const zoomAtCursor = (clientX: number, clientY: number, dollyScale: number) => {
199
+ if (!(camera instanceof THREE.OrthographicCamera)) return
200
+ if (!worldUnderCursor(clientX, clientY, before)) return
201
+ const nextZoom = Math.min(maxZoom, Math.max(minZoom, camera.zoom / dollyScale))
202
+ if (nextZoom === camera.zoom) return
203
+ camera.zoom = nextZoom
204
+ camera.updateProjectionMatrix()
205
+ if (!worldUnderCursor(clientX, clientY, after)) return
206
+ const dx = before.x - after.x
207
+ const dz = before.z - after.z
208
+ camera.position.x += dx
209
+ camera.position.z += dz
210
+ const controls = controlsRef.current
211
+ if (controls) {
212
+ controls.target.x += dx
213
+ controls.target.z += dz
214
+ controls.update()
215
+ }
216
+ invalidate()
217
+ }
218
+
219
+ const onWheel = (event: WheelEvent) => {
220
+ const overlay = document.elementFromPoint(event.clientX, event.clientY)
221
+ if (overlay?.closest('.hud-thumbnail')) {
222
+ event.preventDefault()
223
+ event.stopImmediatePropagation()
224
+ return
225
+ }
226
+ event.preventDefault()
227
+ event.stopImmediatePropagation()
228
+ if (event.deltaY < 0) zoomAtCursor(event.clientX, event.clientY, zoomScale)
229
+ else if (event.deltaY > 0) zoomAtCursor(event.clientX, event.clientY, 1 / zoomScale)
230
+ }
231
+
175
232
  element.addEventListener('pointerdown', onDown)
176
233
  window.addEventListener('pointermove', onMove)
177
234
  window.addEventListener('pointerup', onUp)
178
235
  element.addEventListener('contextmenu', onContextMenu)
236
+ element.addEventListener('wheel', onWheel, { capture: true, passive: false })
179
237
  return () => {
180
238
  element.style.cursor = ''
181
239
  element.removeEventListener('pointerdown', onDown)
182
240
  window.removeEventListener('pointermove', onMove)
183
241
  window.removeEventListener('pointerup', onUp)
184
242
  element.removeEventListener('contextmenu', onContextMenu)
243
+ element.removeEventListener('wheel', onWheel, { capture: true })
185
244
  }
186
245
  }, [
187
246
  camera,
188
247
  enabled,
248
+ fitZoom,
189
249
  gl.domElement,
250
+ invalidate,
190
251
  layout,
191
252
  onLand,
192
253
  onSelect,
@@ -198,25 +259,19 @@ export function MapView({
198
259
  return (
199
260
  <>
200
261
  {enabled && (
201
- <OrthographicCamera
202
- makeDefault
203
- position={[bounds.cx, 120, cz]}
204
- zoom={zoom}
205
- near={1}
206
- far={2000}
207
- up={[0, 0, -1]}
208
- />
262
+ <OrthographicCamera makeDefault near={1} far={2000} up={[0, 0, -1]} />
209
263
  )}
210
264
  {enabled && sized && (
211
265
  <MapControls
266
+ ref={controlsRef}
212
267
  enableRotate={false}
213
268
  enableDamping
214
269
  dampingFactor={0.12}
215
270
  screenSpacePanning
271
+ zoomToCursor
216
272
  zoomSpeed={1.15}
217
273
  minZoom={Math.max(fitZoom * 0.35, 0.05)}
218
274
  maxZoom={Math.max(fitZoom * 10, 20)}
219
- target={[bounds.cx, 0, cz]}
220
275
  />
221
276
  )}
222
277
  {enabled &&