@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.
@@ -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,8 +224,17 @@ export function Player({
198
224
  <PointerLockControls
199
225
  ref={controlsRef}
200
226
  makeDefault
227
+ selector=".stage canvas"
201
228
  enabled={steering && lockEnabled}
202
- onLock={() => onLockedChange(true)}
229
+ onLock={() => {
230
+ if (!lockEnabled) {
231
+ controlsRef.current?.unlock()
232
+ document.exitPointerLock()
233
+ onLockedChange(false)
234
+ return
235
+ }
236
+ onLockedChange(true)
237
+ }}
203
238
  onUnlock={() => onLockedChange(false)}
204
239
  />
205
240
  </>
@@ -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
 
@@ -8,6 +8,8 @@ type SelectionControllerProps = {
8
8
  locked: boolean
9
9
  files: Record<string, PlacedFile>
10
10
  onSelect: (fileId: string | null) => void
11
+ onInspect?: (fileId: string) => void
12
+ onAimFile?: (fileId: string | null) => void
11
13
  onAimRelation?: (aim: AimedRelation | null) => void
12
14
  onTravelTo: (fromId: string, toId: string) => void
13
15
  }
@@ -36,54 +38,93 @@ function aimKey(aim: AimedRelation | null) {
36
38
  return aim ? `${aim.from}->${aim.to}:${aim.flyTo}` : ''
37
39
  }
38
40
 
41
+ function aimedFileId(hits: THREE.Intersection[]): string | null {
42
+ const file = hits.find((item) => item.object.userData.fileId)
43
+ return file ? (file.object.userData.fileId as string) : null
44
+ }
45
+
39
46
  export function SelectionController({
40
47
  locked,
41
48
  files,
42
49
  onSelect,
50
+ onInspect,
51
+ onAimFile,
43
52
  onAimRelation,
44
53
  onTravelTo,
45
54
  }: SelectionControllerProps) {
46
55
  const { camera, scene } = useThree()
47
56
  const lastAim = useRef('')
57
+ const lastAimedFile = useRef<string | null>(null)
48
58
 
49
59
  useFrame(() => {
50
- if (!onAimRelation) return
60
+ if (!onAimRelation && !onAimFile) return
51
61
  if (!locked) {
52
62
  if (lastAim.current) {
53
63
  lastAim.current = ''
54
- onAimRelation(null)
64
+ onAimRelation?.(null)
65
+ }
66
+ if (lastAimedFile.current) {
67
+ lastAimedFile.current = null
68
+ onAimFile?.(null)
55
69
  }
56
70
  return
57
71
  }
58
72
  raycaster.setFromCamera(ndc, camera)
59
- const next = aimedRelation(
60
- raycaster.intersectObjects(scene.children, true),
61
- camera.position,
62
- files,
63
- )
73
+ const hits = raycaster.intersectObjects(scene.children, true)
74
+ const next = aimedRelation(hits, camera.position, files)
64
75
  const key = aimKey(next)
65
- if (key === lastAim.current) return
66
- lastAim.current = key
67
- onAimRelation(next)
76
+ if (onAimRelation && key !== lastAim.current) {
77
+ lastAim.current = key
78
+ onAimRelation(next)
79
+ }
80
+ const fileId = aimedFileId(hits)
81
+ if (onAimFile && fileId !== lastAimedFile.current) {
82
+ lastAimedFile.current = fileId
83
+ onAimFile(fileId)
84
+ }
68
85
  })
69
86
 
70
87
  useEffect(() => {
71
- const onClick = () => {
88
+ let travelTimer: number | undefined
89
+
90
+ const onClick = (event: MouseEvent) => {
72
91
  if (!locked) return
92
+ if (event.detail > 1) {
93
+ if (travelTimer) window.clearTimeout(travelTimer)
94
+ return
95
+ }
73
96
  raycaster.setFromCamera(ndc, camera)
74
97
  const hits = raycaster.intersectObjects(scene.children, true)
75
98
  const aim = aimedRelation(hits, camera.position, files)
76
99
  if (aim) {
77
- onTravelTo(aim.from, aim.to)
100
+ travelTimer = window.setTimeout(() => {
101
+ travelTimer = undefined
102
+ onTravelTo(aim.from, aim.to)
103
+ }, 280)
78
104
  return
79
105
  }
80
- const file = hits.find((item) => item.object.userData.fileId)
81
- onSelect(file ? (file.object.userData.fileId as string) : null)
106
+ const fileId = aimedFileId(hits)
107
+ onSelect(fileId)
108
+ }
109
+
110
+ const onDblClick = () => {
111
+ if (travelTimer) window.clearTimeout(travelTimer)
112
+ if (!locked) return
113
+ raycaster.setFromCamera(ndc, camera)
114
+ const fileId = aimedFileId(raycaster.intersectObjects(scene.children, true))
115
+ if (!fileId) return
116
+ onSelect(fileId)
117
+ onInspect?.(fileId)
82
118
  }
83
119
 
84
120
  window.addEventListener('click', onClick)
85
- return () => window.removeEventListener('click', onClick)
86
- }, [camera, files, locked, onSelect, onTravelTo, scene])
121
+ window.addEventListener('dblclick', onDblClick)
122
+ return () => {
123
+ if (travelTimer) window.clearTimeout(travelTimer)
124
+ window.removeEventListener('click', onClick)
125
+ window.removeEventListener('dblclick', onDblClick)
126
+ }
127
+ }, [camera, files, locked, onInspect, onSelect, onTravelTo, scene])
87
128
 
88
129
  return null
89
130
  }