@jkwd/inbase 0.1.4 → 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.
@@ -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,7 @@ export function MapView({
66
67
  const element = gl.domElement
67
68
  element.style.cursor = 'grab'
68
69
 
69
- const isWalkClick = (event: PointerEvent | MouseEvent) =>
70
- event.shiftKey || event.ctrlKey || event.metaKey
70
+ const isWalkClick = (event: PointerEvent | MouseEvent) => event.ctrlKey
71
71
 
72
72
  const isWalkButton = (event: PointerEvent) =>
73
73
  event.button === 0 || (event.ctrlKey && event.button === 2)
@@ -111,6 +111,7 @@ export function MapView({
111
111
  const pick = pickAt(clientX, clientY)
112
112
  if (!pick) return false
113
113
  const { raycaster, relationHit, fileHit } = pick
114
+ if (fileHit) return false
114
115
 
115
116
  const hit = new THREE.Vector3()
116
117
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
@@ -121,7 +122,7 @@ export function MapView({
121
122
  landAt(hit.x, hit.z)
122
123
  return true
123
124
  }
124
- if (relationHit || fileHit) return false
125
+ if (relationHit) return false
125
126
 
126
127
  landAt(hit.x, hit.z)
127
128
  return true
@@ -140,20 +141,17 @@ export function MapView({
140
141
  const pick = pickAt(event.clientX, event.clientY)
141
142
  if (!pick) return
142
143
  const { relationHit, fileHit } = pick
143
- if (
144
- relationHit &&
145
- (!fileHit || relationHit.distance <= fileHit.distance + 0.4)
146
- ) {
144
+ if (fileHit) {
145
+ onSelect(fileHit.object.userData.fileId as string)
146
+ return
147
+ }
148
+ if (relationHit) {
147
149
  onTravelTo(
148
150
  relationHit.object.userData.relationFrom as string,
149
151
  relationHit.object.userData.relationTo as string,
150
152
  )
151
153
  return
152
154
  }
153
- if (fileHit) {
154
- onSelect(fileHit.object.userData.fileId as string)
155
- return
156
- }
157
155
 
158
156
  const hit = new THREE.Vector3()
159
157
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
@@ -216,7 +214,7 @@ export function MapView({
216
214
  zoomSpeed={1.15}
217
215
  minZoom={Math.max(fitZoom * 0.35, 0.05)}
218
216
  maxZoom={Math.max(fitZoom * 10, 20)}
219
- target={[bounds.cx, 0, cz]}
217
+ target={target}
220
218
  />
221
219
  )}
222
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