1
1
"use client" ;
2
2
import { ForesightDebugger } from "./ForesightDebugger" ;
3
3
export class ForesightManager {
4
- static instance ;
4
+ static manager ;
5
5
links = new Map ( ) ;
6
6
isSetup = false ;
7
7
debugMode = false ; // Synced with globalSettings.debug
8
8
debugger = null ;
9
9
globalSettings = {
10
10
debug : false ,
11
11
enableMouseTrajectory : true ,
12
- positionHistorySize : 6 ,
13
- trajectoryPredictionTime : 50 ,
12
+ positionHistorySize : 8 ,
13
+ trajectoryPredictionTime : 80 ,
14
14
defaultHitSlop : { top : 0 , left : 0 , right : 0 , bottom : 0 } ,
15
15
resizeScrollThrottleDelay : 50 ,
16
16
} ;
@@ -20,44 +20,40 @@ export class ForesightManager {
20
20
lastResizeScrollCallTimestamp = 0 ;
21
21
resizeScrollThrottleTimeoutId = null ;
22
22
constructor ( ) {
23
- // Ensure defaultHitSlop is normalized if it's a number initially
24
23
this . globalSettings . defaultHitSlop = this . normalizeHitSlop ( this . globalSettings . defaultHitSlop ) ;
25
24
setInterval ( this . checkTrajectoryHitExpiration . bind ( this ) , 100 ) ;
26
25
}
27
26
static initialize ( props ) {
28
- if ( ! ForesightManager . instance ) {
29
- ForesightManager . instance = new ForesightManager ( ) ;
30
- // Apply initial props, which also handles initial debugger setup
27
+ if ( ! ForesightManager . manager ) {
28
+ ForesightManager . manager = new ForesightManager ( ) ;
31
29
if ( props ) {
32
- ForesightManager . instance . alterGlobalSettings ( props ) ;
30
+ ForesightManager . manager . alterGlobalSettings ( props ) ;
33
31
}
34
32
else {
35
- // If no props, but default globalSettings.debug is true, ensure debugger is on
36
- if ( ForesightManager . instance . globalSettings . debug ) {
37
- ForesightManager . instance . turnOnDebugMode ( ) ;
33
+ if ( ForesightManager . manager . globalSettings . debug ) {
34
+ ForesightManager . manager . turnOnDebugMode ( ) ;
38
35
}
39
36
}
40
37
}
41
38
else if ( props ) {
42
- // Instance exists, apply new props (handles debugger lifecycle and UI updates)
43
- ForesightManager . instance . alterGlobalSettings ( props ) ;
39
+ console . error ( "ForesightManager is already initialized. Use alterGlobalSettings to update settings. Make sure to not put the ForesightManager.initialize() in a place that rerenders often." ) ;
40
+ ForesightManager . manager . alterGlobalSettings ( props ) ;
44
41
}
45
- // Ensure internal debugMode flag is synced
46
- ForesightManager . instance . debugMode = ForesightManager . instance . globalSettings . debug ;
47
- return ForesightManager . instance ;
42
+ ForesightManager . manager . debugMode = ForesightManager . manager . globalSettings . debug ;
43
+ return ForesightManager . manager ;
48
44
}
49
- static getInstance ( ) {
50
- if ( ! ForesightManager . instance ) {
51
- return this . initialize ( ) ; // Initialize with defaults
45
+ static get instance ( ) {
46
+ if ( ! ForesightManager . manager ) {
47
+ return this . initialize ( ) ;
52
48
}
53
- return ForesightManager . instance ;
49
+ return ForesightManager . manager ;
54
50
}
55
51
checkTrajectoryHitExpiration ( ) {
56
52
const now = performance . now ( ) ;
57
53
let needsVisualUpdate = false ;
58
54
const updatedForesightElements = [ ] ;
59
55
this . links . forEach ( ( elementData , element ) => {
60
- if ( elementData . isTrajectoryHit && now - elementData . trajectoryHitTime > 300 ) {
56
+ if ( elementData . isTrajectoryHit && now - elementData . trajectoryHitTime > 100 ) {
61
57
this . links . set ( element , {
62
58
...elementData ,
63
59
isTrajectoryHit : false ,
@@ -89,7 +85,7 @@ export class ForesightManager {
89
85
register ( element , callback , hitSlop ) {
90
86
const normalizedHitSlop = hitSlop
91
87
? this . normalizeHitSlop ( hitSlop )
92
- : this . globalSettings . defaultHitSlop ; // Already normalized in constructor/alterGlobalSettings
88
+ : this . globalSettings . defaultHitSlop ; // Already normalized in constructor
93
89
const originalRect = element . getBoundingClientRect ( ) ;
94
90
const newElementData = {
95
91
callback,
@@ -145,7 +141,6 @@ export class ForesightManager {
145
141
if ( JSON . stringify ( this . globalSettings . defaultHitSlop ) !== JSON . stringify ( newSlop ) ) {
146
142
this . globalSettings . defaultHitSlop = newSlop ;
147
143
settingsActuallyChanged = true ;
148
- // Note: This won't update existing elements' hitSlop unless they are re-registered.
149
144
}
150
145
}
151
146
if ( props ?. resizeScrollThrottleDelay !== undefined &&
@@ -172,14 +167,12 @@ export class ForesightManager {
172
167
}
173
168
}
174
169
turnOnDebugMode ( ) {
175
- this . debugMode = true ; // Ensure this is true when method is called
170
+ this . debugMode = true ;
176
171
if ( ! this . debugger ) {
177
172
this . debugger = new ForesightDebugger ( this ) ;
178
173
this . debugger . initialize ( this . links , this . globalSettings , this . currentPoint , this . predictedPoint ) ;
179
174
}
180
175
else {
181
- // If debugger exists, ensure its controls are up-to-date with current globalSettings
182
- // This could happen if debug was false, then true again, or settings changed.
183
176
this . debugger . updateControlsState ( this . globalSettings ) ;
184
177
this . debugger . updateTrajectoryVisuals ( this . currentPoint , this . predictedPoint , this . globalSettings . enableMouseTrajectory ) ;
185
178
}
@@ -243,9 +236,67 @@ export class ForesightManager {
243
236
const predictedY = y + vy * trajectoryPredictionTimeInSeconds ;
244
237
return { x : predictedX , y : predictedY } ;
245
238
} ;
246
- pointIntersectsRect = ( x , y , rect ) => {
247
- return x >= rect . left && x <= rect . right && y >= rect . top && y <= rect . bottom ;
248
- } ;
239
+ /**
240
+ * Checks if a line segment intersects with an axis-aligned rectangle.
241
+ * Uses the Liang-Barsky line clipping algorithm.
242
+ * @param p1 Start point of the line segment.
243
+ * @param p2 End point of the line segment.
244
+ * @param rect The rectangle to check against.
245
+ * @returns True if the line segment intersects the rectangle, false otherwise.
246
+ */
247
+ lineSegmentIntersectsRect ( p1 , p2 , rect ) {
248
+ let t0 = 0.0 ;
249
+ let t1 = 1.0 ;
250
+ const dx = p2 . x - p1 . x ;
251
+ const dy = p2 . y - p1 . y ;
252
+ // Helper function for Liang-Barsky algorithm
253
+ // p: parameter related to edge normal and line direction
254
+ // q: parameter related to distance from p1 to edge
255
+ const clipTest = ( p , q ) => {
256
+ if ( p === 0 ) {
257
+ // Line is parallel to the clip edge
258
+ if ( q < 0 ) {
259
+ // Line is outside the clip edge (p1 is on the "wrong" side)
260
+ return false ;
261
+ }
262
+ }
263
+ else {
264
+ const r = q / p ;
265
+ if ( p < 0 ) {
266
+ // Line proceeds from outside to inside (potential entry)
267
+ if ( r > t1 )
268
+ return false ; // Enters after already exited
269
+ if ( r > t0 )
270
+ t0 = r ; // Update latest entry time
271
+ }
272
+ else {
273
+ // Line proceeds from inside to outside (potential exit) (p > 0)
274
+ if ( r < t0 )
275
+ return false ; // Exits before already entered
276
+ if ( r < t1 )
277
+ t1 = r ; // Update earliest exit time
278
+ }
279
+ }
280
+ return true ;
281
+ } ;
282
+ // Left edge: rect.left
283
+ if ( ! clipTest ( - dx , p1 . x - rect . left ) )
284
+ return false ;
285
+ // Right edge: rect.right
286
+ if ( ! clipTest ( dx , rect . right - p1 . x ) )
287
+ return false ;
288
+ // Top edge: rect.top
289
+ if ( ! clipTest ( - dy , p1 . y - rect . top ) )
290
+ return false ;
291
+ // Bottom edge: rect.bottom
292
+ if ( ! clipTest ( dy , rect . bottom - p1 . y ) )
293
+ return false ;
294
+ // If t0 > t1, the segment is completely outside or misses the clip window.
295
+ // Also, the valid intersection must be within the segment [0,1].
296
+ // Since t0 and t1 are initialized to 0 and 1, and clamped,
297
+ // this also ensures the intersection lies on the segment.
298
+ return t0 <= t1 ;
299
+ }
249
300
isMouseInExpandedArea = ( area , clientPoint , isAlreadyHovering ) => {
250
301
const isInExpandedArea = clientPoint . x >= area . left &&
251
302
clientPoint . x <= area . right &&
@@ -268,24 +319,29 @@ export class ForesightManager {
268
319
const { isHoveringInArea, shouldRunCallback } = this . isMouseInExpandedArea ( elementData . elementBounds . expandedRect , this . currentPoint , elementData . isHovering ) ;
269
320
let linkStateChanged = false ;
270
321
if ( this . globalSettings . enableMouseTrajectory && ! isHoveringInArea ) {
271
- if ( this . pointIntersectsRect ( this . predictedPoint . x , this . predictedPoint . y , elementData . elementBounds . expandedRect ) ) {
322
+ // Check if the trajectory line segment intersects the expanded rect
323
+ if ( this . lineSegmentIntersectsRect ( this . currentPoint , this . predictedPoint , elementData . elementBounds . expandedRect ) ) {
272
324
if ( ! elementData . isTrajectoryHit ) {
273
325
elementData . callback ( ) ;
274
326
this . links . set ( element , {
275
327
...elementData ,
276
328
isTrajectoryHit : true ,
277
329
trajectoryHitTime : performance . now ( ) ,
278
- isHovering : isHoveringInArea ,
330
+ isHovering : isHoveringInArea , // isHoveringInArea is false here
279
331
} ) ;
280
332
linkStateChanged = true ;
281
333
}
282
334
}
335
+ // Note: No 'else' here to turn off isTrajectoryHit immediately.
336
+ // It's managed by checkTrajectoryHitExpiration or when actual hover occurs.
283
337
}
284
338
if ( elementData . isHovering !== isHoveringInArea ) {
285
339
this . links . set ( element , {
286
340
...elementData ,
287
341
isHovering : isHoveringInArea ,
288
- // Preserve trajectory hit state if it was already hit
342
+ // Preserve trajectory hit state if it was already hit,
343
+ // unless actual hover is now false and trajectory also doesn't hit
344
+ // (though trajectory hit is primarily for non-hovering states)
289
345
isTrajectoryHit : this . links . get ( element ) . isTrajectoryHit ,
290
346
trajectoryHitTime : this . links . get ( element ) . trajectoryHitTime ,
291
347
} ) ;
0 commit comments