@jkwd/inbase 0.1.3 → 0.1.5

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,5 +1,5 @@
1
1
  import { Suspense } from 'react'
2
- import { Text } from '@react-three/drei'
2
+ import { Html, Text } from '@react-three/drei'
3
3
  import { CHANGE_HIGHLIGHT, folderFloorColor, MAP_SELECTION, type ChangeKind } from '../theme'
4
4
  import type { PlacedFolder } from '../types'
5
5
  import { MapSelectBorder } from './MapSelectBorder'
@@ -10,6 +10,7 @@ type FolderAreaProps = {
10
10
  selected?: boolean
11
11
  mapMode?: boolean
12
12
  highlightKind?: ChangeKind | null
13
+ previewLabels?: boolean
13
14
  }
14
15
 
15
16
  export function FolderArea({
@@ -18,10 +19,10 @@ export function FolderArea({
18
19
  selected = false,
19
20
  mapMode = false,
20
21
  highlightKind = null,
22
+ previewLabels = false,
21
23
  }: FolderAreaProps) {
22
24
  const added = Boolean(folder.added)
23
- const highlight =
24
- mapMode && highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
25
+ const highlight = highlightKind ? CHANGE_HIGHLIGHT[highlightKind] : null
25
26
  const addedOnly = added && !highlight
26
27
  const outline = highlight ? highlight.color : addedOnly ? '#7ec8e8' : null
27
28
  const color = highlight
@@ -57,6 +58,7 @@ export function FolderArea({
57
58
  depth={folder.depth}
58
59
  y={0.06}
59
60
  stroke={MAP_SELECTION.islandPad}
61
+ color={MAP_SELECTION.island}
60
62
  />
61
63
  )}
62
64
  <mesh position={[0, 0.02, 0]} rotation={[-Math.PI / 2, 0, 0]}>
@@ -79,7 +81,18 @@ export function FolderArea({
79
81
  </mesh>
80
82
  )}
81
83
  <Suspense fallback={null}>
82
- {!naming && (
84
+ {!naming && previewLabels && (
85
+ <Html
86
+ position={[0, 1.35, -folder.depth / 2 + 1.6]}
87
+ center
88
+ occlude={false}
89
+ style={{ pointerEvents: 'none' }}
90
+ zIndexRange={[20, 0]}
91
+ >
92
+ <div className="thumbnail-folder-label">{label}</div>
93
+ </Html>
94
+ )}
95
+ {!naming && !previewLabels && !mapMode && (
83
96
  <Text
84
97
  position={[0, 0.05, -folder.depth / 2 + 1.6]}
85
98
  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}
@@ -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 target = useMemo(() => [bounds.cx, 0, cz] as [number, number, number], [bounds.cx, cz])
52
53
 
53
54
  useLayoutEffect(() => {
54
55
  if (!enabled || !(camera instanceof THREE.OrthographicCamera)) return
@@ -66,8 +67,13 @@ export function MapView({
66
67
  const element = gl.domElement
67
68
  element.style.cursor = 'grab'
68
69
 
70
+ const isWalkClick = (event: PointerEvent | MouseEvent) => event.ctrlKey
71
+
72
+ const isWalkButton = (event: PointerEvent) =>
73
+ event.button === 0 || (event.ctrlKey && event.button === 2)
74
+
69
75
  const onDown = (event: PointerEvent) => {
70
- if (event.button !== 0) return
76
+ if (!isWalkButton(event)) return
71
77
  drag.current = { x: event.clientX, y: event.clientY, moved: false, active: true }
72
78
  element.style.cursor = 'grabbing'
73
79
  }
@@ -93,20 +99,32 @@ export function MapView({
93
99
  return { raycaster, relationHit, fileHit }
94
100
  }
95
101
 
96
- const landAtPointer = (clientX: number, clientY: number) => {
102
+ const landAt = (x: number, z: number) => {
103
+ const lock = element.requestPointerLock()
104
+ if (lock && typeof lock.catch === 'function') {
105
+ void lock.catch(() => {})
106
+ }
107
+ onLand(x, z)
108
+ }
109
+
110
+ const landAtPointer = (clientX: number, clientY: number, allowIsland: boolean) => {
97
111
  const pick = pickAt(clientX, clientY)
98
112
  if (!pick) return false
99
113
  const { raycaster, relationHit, fileHit } = pick
100
- if (relationHit || fileHit) return false
114
+ if (fileHit) return false
101
115
 
102
116
  const hit = new THREE.Vector3()
103
117
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
104
118
  if (!raycaster.ray.intersectPlane(plane, hit)) return false
105
- const lock = element.requestPointerLock()
106
- if (lock && typeof lock.catch === 'function') {
107
- void lock.catch(() => {})
119
+
120
+ const folder = folderAt(hit.x, hit.z, layout)
121
+ if (allowIsland && folder) {
122
+ landAt(hit.x, hit.z)
123
+ return true
108
124
  }
109
- onLand(hit.x, hit.z)
125
+ if (relationHit) return false
126
+
127
+ landAt(hit.x, hit.z)
110
128
  return true
111
129
  }
112
130
 
@@ -114,27 +132,26 @@ export function MapView({
114
132
  element.style.cursor = 'grab'
115
133
  const startedOnCanvas = drag.current.active
116
134
  drag.current.active = false
117
- if (!startedOnCanvas || event.button !== 0 || drag.current.moved) return
135
+ if (!startedOnCanvas || !isWalkButton(event) || drag.current.moved) return
118
136
 
119
- if (event.shiftKey && landAtPointer(event.clientX, event.clientY)) return
137
+ if (isWalkClick(event) && landAtPointer(event.clientX, event.clientY, true)) {
138
+ return
139
+ }
120
140
 
121
141
  const pick = pickAt(event.clientX, event.clientY)
122
142
  if (!pick) return
123
143
  const { relationHit, fileHit } = pick
124
- if (
125
- relationHit &&
126
- (!fileHit || relationHit.distance <= fileHit.distance + 0.4)
127
- ) {
144
+ if (fileHit) {
145
+ onSelect(fileHit.object.userData.fileId as string)
146
+ return
147
+ }
148
+ if (relationHit) {
128
149
  onTravelTo(
129
150
  relationHit.object.userData.relationFrom as string,
130
151
  relationHit.object.userData.relationTo as string,
131
152
  )
132
153
  return
133
154
  }
134
- if (fileHit) {
135
- onSelect(fileHit.object.userData.fileId as string)
136
- return
137
- }
138
155
 
139
156
  const hit = new THREE.Vector3()
140
157
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
@@ -149,14 +166,20 @@ export function MapView({
149
166
  onSelectFolder(null)
150
167
  }
151
168
 
169
+ const onContextMenu = (event: MouseEvent) => {
170
+ if (event.ctrlKey) event.preventDefault()
171
+ }
172
+
152
173
  element.addEventListener('pointerdown', onDown)
153
174
  window.addEventListener('pointermove', onMove)
154
175
  window.addEventListener('pointerup', onUp)
176
+ element.addEventListener('contextmenu', onContextMenu)
155
177
  return () => {
156
178
  element.style.cursor = ''
157
179
  element.removeEventListener('pointerdown', onDown)
158
180
  window.removeEventListener('pointermove', onMove)
159
181
  window.removeEventListener('pointerup', onUp)
182
+ element.removeEventListener('contextmenu', onContextMenu)
160
183
  }
161
184
  }, [
162
185
  camera,
@@ -191,7 +214,7 @@ export function MapView({
191
214
  zoomSpeed={1.15}
192
215
  minZoom={Math.max(fitZoom * 0.35, 0.05)}
193
216
  maxZoom={Math.max(fitZoom * 10, 20)}
194
- target={[bounds.cx, 0, cz]}
217
+ target={target}
195
218
  />
196
219
  )}
197
220
  {enabled &&
@@ -49,6 +49,7 @@ export function Player({
49
49
  const [steering, setSteering] = useState(true)
50
50
  const controlsRef = useRef<PointerLockControlsImpl>(null)
51
51
  const capturedLook = useRef(false)
52
+ const lookHeld = useRef(false)
52
53
  const flying = useRef<{
53
54
  startPos: THREE.Vector3
54
55
  endPos: THREE.Vector3
@@ -133,6 +134,31 @@ export function Player({
133
134
  controls.lock()
134
135
  }, [lockEnabled, onLockedChange, steering, walking])
135
136
 
137
+ useEffect(() => {
138
+ if (!walking) return
139
+
140
+ const looking = () =>
141
+ Boolean(controlsRef.current?.isLocked || document.pointerLockElement)
142
+
143
+ const onClick = (event: MouseEvent) => {
144
+ if (event.button !== 0 || event.detail !== 1) return
145
+ lookHeld.current = looking()
146
+ }
147
+
148
+ const onDblClick = () => {
149
+ if (!lookHeld.current) return
150
+ controlsRef.current?.unlock()
151
+ document.exitPointerLock()
152
+ }
153
+
154
+ document.addEventListener('click', onClick, true)
155
+ document.addEventListener('dblclick', onDblClick)
156
+ return () => {
157
+ document.removeEventListener('click', onClick, true)
158
+ document.removeEventListener('dblclick', onDblClick)
159
+ }
160
+ }, [walking])
161
+
136
162
  useFrame((_, delta) => {
137
163
  try {
138
164
  if (walking && flying.current) {
@@ -198,6 +224,7 @@ export function Player({
198
224
  <PointerLockControls
199
225
  ref={controlsRef}
200
226
  makeDefault
227
+ selector=".stage canvas"
201
228
  enabled={steering && lockEnabled}
202
229
  onLock={() => onLockedChange(true)}
203
230
  onUnlock={() => onLockedChange(false)}
@@ -125,49 +125,53 @@ export function RelationLines({
125
125
  const selectedRadius = fromAbove ? 0.2 : 0.07
126
126
  const plannedRadius = fromAbove ? 0.26 : 0.1
127
127
  const lines: LineMesh[] = []
128
- const plannedKeys = new Set<string>()
128
+ const drawn = new Set<string>()
129
+
130
+ const addLine = (
131
+ fromId: string,
132
+ toId: string,
133
+ planned: boolean,
134
+ radius: number,
135
+ ) => {
136
+ const key = `${fromId}->${toId}`
137
+ if (drawn.has(key)) return
138
+ const from = placed[fromId]
139
+ const to = placed[toId]
140
+ if (!from || !to) return
141
+ drawn.add(key)
142
+ lines.push(edgeMesh(from, to, toId, planned, radius, fromAbove, false))
143
+ }
129
144
 
130
145
  for (const edge of plannedEdges) {
131
- const from = placed[edge.from]
132
- const to = placed[edge.to]
133
- if (!from || !to) continue
134
- const key = `${edge.from}->${edge.to}`
135
- if (plannedKeys.has(key)) continue
136
- plannedKeys.add(key)
137
- lines.push(edgeMesh(from, to, edge.to, true, plannedRadius, fromAbove, false))
146
+ addLine(edge.from, edge.to, true, plannedRadius)
138
147
  }
139
148
 
140
149
  if (selectedId) {
141
- const selected = placed[selectedId]
142
- if (selected) {
143
- if (importedBy) {
144
- for (const file of files) {
145
- if (file.id === selectedId || !file.imports.includes(selectedId)) continue
146
- if (plannedKeys.has(`${file.id}->${selectedId}`)) continue
147
- const importer = placed[file.id]
148
- if (!importer) continue
149
- lines.push(
150
- edgeMesh(importer, selected, selectedId, false, selectedRadius, fromAbove, false),
151
- )
152
- }
153
- } else {
154
- const file = files.find((item) => item.id === selectedId)
155
- if (file) {
156
- for (const importId of file.imports) {
157
- if (plannedKeys.has(`${file.id}->${importId}`)) continue
158
- const target = placed[importId]
159
- if (!target) continue
160
- lines.push(
161
- edgeMesh(selected, target, importId, false, selectedRadius, fromAbove, false),
162
- )
163
- }
150
+ if (importedBy) {
151
+ for (const file of files) {
152
+ if (file.id === selectedId || !file.imports.includes(selectedId)) continue
153
+ addLine(file.id, selectedId, false, selectedRadius)
154
+ }
155
+ } else {
156
+ const file = files.find((item) => item.id === selectedId)
157
+ if (file) {
158
+ for (const importId of file.imports) {
159
+ addLine(file.id, importId, false, selectedRadius)
164
160
  }
165
161
  }
166
162
  }
167
163
  }
168
164
 
169
165
  return lines
170
- }, [extras, files, fromAbove, importedBy, layout.files, plannedEdges, selectedId])
166
+ }, [
167
+ extras,
168
+ files,
169
+ fromAbove,
170
+ importedBy,
171
+ layout.files,
172
+ plannedEdges,
173
+ selectedId,
174
+ ])
171
175
 
172
176
  if (meshes.length === 0) return null
173
177
 
@@ -68,21 +68,39 @@ export function SelectionController({
68
68
  })
69
69
 
70
70
  useEffect(() => {
71
- const onClick = () => {
71
+ let travelTimer: number | undefined
72
+
73
+ const onClick = (event: MouseEvent) => {
72
74
  if (!locked) return
75
+ if (event.detail > 1) {
76
+ if (travelTimer) window.clearTimeout(travelTimer)
77
+ return
78
+ }
73
79
  raycaster.setFromCamera(ndc, camera)
74
80
  const hits = raycaster.intersectObjects(scene.children, true)
75
81
  const aim = aimedRelation(hits, camera.position, files)
76
82
  if (aim) {
77
- onTravelTo(aim.from, aim.to)
83
+ travelTimer = window.setTimeout(() => {
84
+ travelTimer = undefined
85
+ onTravelTo(aim.from, aim.to)
86
+ }, 280)
78
87
  return
79
88
  }
80
89
  const file = hits.find((item) => item.object.userData.fileId)
81
90
  onSelect(file ? (file.object.userData.fileId as string) : null)
82
91
  }
83
92
 
93
+ const onDblClick = () => {
94
+ if (travelTimer) window.clearTimeout(travelTimer)
95
+ }
96
+
84
97
  window.addEventListener('click', onClick)
85
- return () => window.removeEventListener('click', onClick)
98
+ window.addEventListener('dblclick', onDblClick)
99
+ return () => {
100
+ if (travelTimer) window.clearTimeout(travelTimer)
101
+ window.removeEventListener('click', onClick)
102
+ window.removeEventListener('dblclick', onDblClick)
103
+ }
86
104
  }, [camera, files, locked, onSelect, onTravelTo, scene])
87
105
 
88
106
  return null