js.foresight 0.0.8 → 0.0.9

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.
@@ -33,7 +33,7 @@ export class ForesightDebugger {
33
33
  transition: opacity 0.2s ease, border-color 0.2s ease, background-color 0.2s ease, box-shadow 0.2s ease;
34
34
  }
35
35
  .jsforesight-link-overlay.active {
36
- border-color: red; background-color: rgba(255, 0, 0, 0.1);
36
+ border-color: orange; background-color: rgba(255, 0, 0, 0.1);
37
37
  }
38
38
  .jsforesight-link-overlay.trajectory-hit {
39
39
  border-color: lime; background-color: rgba(0, 255, 0, 0.3);
@@ -1,6 +1,6 @@
1
1
  import type { ForesightCallback, ForesightManagerProps, ForesightElement, Rect } from "../types/types";
2
2
  export declare class ForesightManager {
3
- private static instance;
3
+ private static manager;
4
4
  private links;
5
5
  private isSetup;
6
6
  private debugMode;
@@ -13,7 +13,7 @@ export declare class ForesightManager {
13
13
  private resizeScrollThrottleTimeoutId;
14
14
  private constructor();
15
15
  static initialize(props?: Partial<ForesightManagerProps>): ForesightManager;
16
- static getInstance(): ForesightManager;
16
+ static get instance(): ForesightManager;
17
17
  private checkTrajectoryHitExpiration;
18
18
  private normalizeHitSlop;
19
19
  register(element: ForesightElement, callback: ForesightCallback, hitSlop?: number | Rect): () => void;
@@ -24,7 +24,15 @@ export declare class ForesightManager {
24
24
  private updateExpandedRect;
25
25
  private updateAllRects;
26
26
  private predictMousePosition;
27
- private pointIntersectsRect;
27
+ /**
28
+ * Checks if a line segment intersects with an axis-aligned rectangle.
29
+ * Uses the Liang-Barsky line clipping algorithm.
30
+ * @param p1 Start point of the line segment.
31
+ * @param p2 End point of the line segment.
32
+ * @param rect The rectangle to check against.
33
+ * @returns True if the line segment intersects the rectangle, false otherwise.
34
+ */
35
+ private lineSegmentIntersectsRect;
28
36
  private isMouseInExpandedArea;
29
37
  private handleMouseMove;
30
38
  private handleResizeOrScroll;
@@ -1,7 +1,7 @@
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
@@ -9,8 +9,8 @@ export class ForesightManager {
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
  });
@@ -68,13 +68,13 @@ export type ForesightManagerProps = {
68
68
  /**
69
69
  * Number of mouse positions to keep in history for trajectory calculation.
70
70
  * A higher number might lead to smoother but slightly delayed predictions.
71
- * @default 6
71
+ * @default 8
72
72
  */
73
73
  positionHistorySize: number;
74
74
  /**
75
75
  * How far ahead (in milliseconds) to predict the mouse trajectory.
76
76
  * A larger value means the prediction extends further into the future. (meaning it will trigger callbacks sooner)
77
- * @default 50
77
+ * @default 80
78
78
  */
79
79
  trajectoryPredictionTime: number;
80
80
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js.foresight",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",