@@ -36,6 +36,7 @@ type Props = {
3636 onHover : ( node : SurfNode | null ) => void
3737 onPointerLockChange : ( locked : boolean ) => void
3838 onZoneChange : ( section : LibrarySection ) => void
39+ onWayfindingCueChange : ( cue : string | null ) => void
3940 currentFloor : number
4041 floorRequest : FloorRequest
4142 onFloorChange : ( floor : number ) => void
@@ -148,6 +149,16 @@ const SECTION_ACCENTS: Record<LibrarySection, number> = {
148149 archive : 0x8b96a8 ,
149150}
150151
152+ const WAYFINDING_DESTINATIONS : Record < LibrarySection , string > = {
153+ atrium : 'Central Atrium' ,
154+ featured : 'Featured Shelves' ,
155+ latest : 'New Arrivals' ,
156+ topics : 'Topic Wings' ,
157+ creators : 'Creator Studies' ,
158+ search : 'Card Catalog' ,
159+ archive : 'Deep Archive' ,
160+ }
161+
151162const KIND_GEOMETRY : Record < SurfNodeKind , ( ) => THREE . BufferGeometry > = {
152163 home : ( ) => new THREE . CylinderGeometry ( .8 , 1.05 , .72 , 8 ) ,
153164 section : ( ) => new THREE . CylinderGeometry ( .09 , .13 , 1.05 , 8 ) ,
@@ -542,6 +553,7 @@ export default function DevWebSurf3D({
542553 onHover,
543554 onPointerLockChange,
544555 onZoneChange,
556+ onWayfindingCueChange,
545557 currentFloor,
546558 floorRequest,
547559 onFloorChange,
@@ -560,6 +572,7 @@ export default function DevWebSurf3D({
560572 const hoverRef = useRef ( onHover )
561573 const lockRef = useRef ( onPointerLockChange )
562574 const zoneRef = useRef ( onZoneChange )
575+ const wayfindingCueRef = useRef ( onWayfindingCueChange )
563576 const currentFloorRef = useRef ( currentFloor )
564577 const floorRequestRef = useRef ( floorRequest )
565578 const floorChangeRef = useRef ( onFloorChange )
@@ -577,6 +590,7 @@ export default function DevWebSurf3D({
577590 hoverRef . current = onHover
578591 lockRef . current = onPointerLockChange
579592 zoneRef . current = onZoneChange
593+ wayfindingCueRef . current = onWayfindingCueChange
580594 currentFloorRef . current = currentFloor
581595 floorRequestRef . current = floorRequest
582596 floorChangeRef . current = onFloorChange
@@ -4201,6 +4215,44 @@ export default function DevWebSurf3D({
42014215 } )
42024216 architecturalGeometries . push ( floorStripGeometry )
42034217
4218+ // A single transparent gradient gives otherwise-neutral transit runs a
4219+ // gentle pull toward the next threshold without adding lights or routes.
4220+ const corridorPullCanvas = document . createElement ( 'canvas' )
4221+ corridorPullCanvas . width = 16
4222+ corridorPullCanvas . height = 256
4223+ const corridorPullContext = corridorPullCanvas . getContext ( '2d' )
4224+ if ( corridorPullContext ) {
4225+ const gradient = corridorPullContext . createLinearGradient ( 0 , 256 , 0 , 0 )
4226+ gradient . addColorStop ( 0 , 'rgba(255,255,255,0)' )
4227+ gradient . addColorStop ( .45 , 'rgba(255,255,255,.025)' )
4228+ gradient . addColorStop ( 1 , 'rgba(255,255,255,.22)' )
4229+ corridorPullContext . fillStyle = gradient
4230+ corridorPullContext . fillRect ( 0 , 0 , 16 , 256 )
4231+ }
4232+ const corridorPullTexture = new THREE . CanvasTexture ( corridorPullCanvas )
4233+ corridorPullTexture . minFilter = THREE . LinearFilter
4234+ corridorPullTexture . magFilter = THREE . LinearFilter
4235+ corridorPullTexture . generateMipmaps = false
4236+ labelsToDispose . push ( corridorPullTexture )
4237+ const corridorPullGeometry = new THREE . PlaneGeometry ( .82 , 1 )
4238+ const corridorPullMaterial = new THREE . MeshBasicMaterial ( {
4239+ color : SECTION_ACCENTS . featured ,
4240+ map : corridorPullTexture ,
4241+ transparent : true ,
4242+ opacity : .46 ,
4243+ blending : THREE . AdditiveBlending ,
4244+ depthWrite : false ,
4245+ } )
4246+ const corridorPull = new THREE . Mesh (
4247+ corridorPullGeometry ,
4248+ corridorPullMaterial ,
4249+ )
4250+ corridorPull . rotation . x = - Math . PI / 2
4251+ corridorPull . visible = false
4252+ scene . add ( corridorPull )
4253+ architecturalGeometries . push ( corridorPullGeometry )
4254+ architecturalMaterials . push ( corridorPullMaterial )
4255+
42044256 const arrowShape = new THREE . Shape ( )
42054257 arrowShape . moveTo ( 0 , .28 )
42064258 arrowShape . lineTo ( .16 , .02 )
@@ -4259,6 +4311,7 @@ export default function DevWebSurf3D({
42594311 const sceneRevealStartedAt = performance . now ( )
42604312 let debugFrameCount = 0
42614313 let debugWindowStartedAt = performance . now ( )
4314+ let displayedWayfindingCue : string | null = null
42624315
42634316 function updateArticleLods ( now : number ) {
42644317 if ( now - lastLodUpdate < .22 ) return
@@ -5259,6 +5312,66 @@ export default function DevWebSurf3D({
52595312 zoneRef . current ( nearestSection )
52605313 }
52615314
5315+ const floorBase = currentFloorIndex * LIBRARY_FLOOR_HEIGHT
5316+ const nearbyHubs : Array < {
5317+ section : LibrarySection
5318+ position : THREE . Vector3
5319+ } > =
5320+ currentFloorIndex === 0
5321+ ? ( Object . keys ( SECTION_DOORWAYS ) as LibrarySection [ ] )
5322+ . filter ( ( section ) => section !== currentSection )
5323+ . map ( ( section ) => ( {
5324+ section,
5325+ position : SECTION_DOORWAYS [ section ] ,
5326+ } ) )
5327+ : [
5328+ {
5329+ section : 'atrium' ,
5330+ position : new THREE . Vector3 ( 0 , floorBase + .09 , 7 ) ,
5331+ } ,
5332+ ]
5333+ camera . getWorldDirection ( tempDirection )
5334+ let nextHubIndex = - 1
5335+ let nextHubDistance = Number . POSITIVE_INFINITY
5336+ nearbyHubs . forEach ( ( hub , index ) => {
5337+ const dx = hub . position . x - camera . position . x
5338+ const dz = hub . position . z - camera . position . z
5339+ const distance = Math . hypot ( dx , dz )
5340+ if ( distance < 3 || distance > 18 ) return
5341+ const ahead =
5342+ ( dx * tempDirection . x + dz * tempDirection . z ) /
5343+ Math . max ( .001 , distance )
5344+ if ( ahead < .58 || distance >= nextHubDistance ) return
5345+ nextHubIndex = index
5346+ nextHubDistance = distance
5347+ } )
5348+ const nextHub =
5349+ nextHubIndex >= 0 ? nearbyHubs [ nextHubIndex ] : null
5350+
5351+ const nextCue = nextHub
5352+ ? 'AHEAD: ' + WAYFINDING_DESTINATIONS [ nextHub . section ]
5353+ : null
5354+ if ( nextCue !== displayedWayfindingCue ) {
5355+ displayedWayfindingCue = nextCue
5356+ wayfindingCueRef . current ( nextCue )
5357+ }
5358+
5359+ if ( nextHub ) {
5360+ const dx = nextHub . position . x - camera . position . x
5361+ const dz = nextHub . position . z - camera . position . z
5362+ corridorPull . visible = true
5363+ corridorPull . position . set (
5364+ camera . position . x + dx / 2 ,
5365+ floorBase + .043 ,
5366+ camera . position . z + dz / 2 ,
5367+ )
5368+ corridorPull . rotation . set ( - Math . PI / 2 , Math . atan2 ( dx , dz ) , 0 )
5369+ corridorPull . scale . set ( 1 , nextHubDistance , 1 )
5370+ corridorPullMaterial . color . setHex ( SECTION_ACCENTS [ nextHub . section ] )
5371+ } else {
5372+ corridorPull . visible = false
5373+ }
5374+
52625375 deskGlow . rotation . z += delta * .12
52635376 deskGlowMaterial . opacity =
52645377 .28 + Math . max ( 0 , Math . sin ( now * .7 ) ) * .13
0 commit comments