@blorkfield/overlay-core 0.8.8 → 0.8.10

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.
package/README.md CHANGED
@@ -22,13 +22,24 @@ pnpm add @blorkfield/overlay-core
22
22
 
23
23
  ### Tag Based Behavior
24
24
 
25
- Objects don't have fixed types. Instead, their behavior is determined by string tags:
25
+ Objects don't have fixed types. Instead, their behavior is determined by string tags. Import the tag constants to avoid magic strings:
26
26
 
27
- | Tag | Behavior |
28
- |-----|----------|
29
- | `falling` | Object is dynamic and affected by gravity |
30
- | `window_follow` | Object follows mouse position when grounded |
31
- | `grabable` | Object can be dragged via mouse constraint |
27
+ ```typescript
28
+ import { TAGS, TAG_FALLING, TAG_GRABABLE, TAG_FOLLOW_WINDOW } from '@blorkfield/overlay-core';
29
+
30
+ // Use individual constants
31
+ scene.spawnObject({ tags: [TAG_FALLING, TAG_GRABABLE], ... });
32
+
33
+ // Or destructure from TAGS object
34
+ const { FALLING, GRABABLE } = TAGS;
35
+ scene.spawnObject({ tags: [FALLING, GRABABLE], ... });
36
+ ```
37
+
38
+ | Constant | Value | Behavior |
39
+ |----------|-------|----------|
40
+ | `TAG_FALLING` / `TAGS.FALLING` | `'falling'` | Object is dynamic and affected by gravity |
41
+ | `TAG_FOLLOW_WINDOW` / `TAGS.FOLLOW_WINDOW` | `'follow_window'` | Object follows mouse position when grounded |
42
+ | `TAG_GRABABLE` / `TAGS.GRABABLE` | `'grabable'` | Object can be grabbed and moved with mouse |
32
43
 
33
44
  Without the `falling` tag, objects are static and won't move.
34
45
 
@@ -378,6 +389,8 @@ The offset calculation is your responsibility - overlay-core uses whatever posit
378
389
 
379
390
  Grab uses delta-based movement: when grabbed, the entity and mouse become linked. The entity moves BY the same amount as the mouse moves, not TO the mouse position. This ensures the entity stays at its original position on grab and follows mouse movement naturally.
380
391
 
392
+ Grab detection uses a two-pass approach to handle fast-moving bodies. The first pass does an exact point query at the click position. If that misses (the body tunneled through the cursor between frames), a second pass sweeps the body's recent position history (last 5 frames, 20px radius) to catch it. This means you can grab entities even when they are moving quickly.
393
+
381
394
  ```typescript
382
395
  // Grab object at current mouse position (only 'grabable' tagged objects)
383
396
  const grabbedId = scene.startGrab();
@@ -712,6 +725,12 @@ import type {
712
725
  UpdateCallbackData,
713
726
 
714
727
  // Logging
715
- LogLevel
728
+ LogLevel,
729
+
730
+ // Tags
731
+ Tag
716
732
  } from '@blorkfield/overlay-core';
733
+
734
+ // Tag constants (values, not types)
735
+ import { TAGS, TAG_FALLING, TAG_GRABABLE, TAG_FOLLOW_WINDOW } from '@blorkfield/overlay-core';
717
736
  ```
package/dist/index.cjs CHANGED
@@ -32,6 +32,10 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BackgroundManager: () => BackgroundManager,
34
34
  OverlayScene: () => OverlayScene,
35
+ TAGS: () => TAGS,
36
+ TAG_FALLING: () => TAG_FALLING,
37
+ TAG_FOLLOW_WINDOW: () => TAG_FOLLOW_WINDOW,
38
+ TAG_GRABABLE: () => TAG_GRABABLE,
35
39
  clearFontCache: () => clearFontCache,
36
40
  getGlyphData: () => getGlyphData,
37
41
  getKerning: () => getKerning,
@@ -1447,6 +1451,12 @@ var OverlayScene = class {
1447
1451
  this.lastGrabMousePosition = null;
1448
1452
  this.grabbedWasDynamic = false;
1449
1453
  this.grabVelocity = { x: 0, y: 0 };
1454
+ // Position history for sweep-based grab detection (catches fast-moving bodies)
1455
+ this.bodyPositionHistory = /* @__PURE__ */ new Map();
1456
+ this.grabHistoryFrames = 5;
1457
+ this.grabHistoryRadius = 20;
1458
+ // Number of physics substeps per frame — more substeps = better collision at high speeds, more CPU
1459
+ this.substeps = 2;
1450
1460
  /** Handle mouse down - start grab via programmatic API */
1451
1461
  this.handleMouseDown = (event) => {
1452
1462
  const rect = this.canvas.getBoundingClientRect();
@@ -1523,6 +1533,10 @@ var OverlayScene = class {
1523
1533
  };
1524
1534
  // ==================== PRIVATE ====================
1525
1535
  this.loop = () => {
1536
+ const substepDelta = 1e3 / 60 / this.substeps;
1537
+ for (let i = 0; i < this.substeps; i++) {
1538
+ import_matter_js5.default.Engine.update(this.engine, substepDelta);
1539
+ }
1526
1540
  this.effectManager.update();
1527
1541
  this.checkTTLExpiration();
1528
1542
  this.checkDespawnBelowFloor();
@@ -1547,7 +1561,7 @@ var OverlayScene = class {
1547
1561
  const isDragging = this.grabbedObjectId === entry.id;
1548
1562
  if (!isDragging) {
1549
1563
  for (const tag of entry.tags) {
1550
- const key = tag === "window_follow" ? "mouse" : tag.startsWith("follow-") ? tag.slice(7) : null;
1564
+ const key = tag === "follow_window" ? "mouse" : tag.startsWith("follow-") ? tag.slice(7) : null;
1551
1565
  if (key) {
1552
1566
  const target = this.followTargets.get(key);
1553
1567
  if (target) {
@@ -1566,6 +1580,14 @@ var OverlayScene = class {
1566
1580
  if (entry.domElement && entry.tags.includes("falling")) {
1567
1581
  this.updateDOMElementTransform(entry);
1568
1582
  }
1583
+ if (entry.tags.includes("grabable") && !entry.body.isStatic) {
1584
+ const history = this.bodyPositionHistory.get(entry.body.id) ?? [];
1585
+ history.push({ x: entry.body.position.x, y: entry.body.position.y });
1586
+ if (history.length > this.grabHistoryFrames) {
1587
+ history.shift();
1588
+ }
1589
+ this.bodyPositionHistory.set(entry.body.id, history);
1590
+ }
1569
1591
  }
1570
1592
  if (!this.config.debug) {
1571
1593
  this.drawTTFGlyphs();
@@ -1827,7 +1849,7 @@ var OverlayScene = class {
1827
1849
  }
1828
1850
  }
1829
1851
  if (parts.length > 0) {
1830
- console.log("[Pressure]", parts.join(" "));
1852
+ logger.debug("[Pressure]", parts.join(" "));
1831
1853
  }
1832
1854
  }
1833
1855
  /** Calculate weighted pressure from a set of object IDs */
@@ -2004,7 +2026,6 @@ var OverlayScene = class {
2004
2026
  }
2005
2027
  start() {
2006
2028
  import_matter_js5.default.Render.run(this.render);
2007
- import_matter_js5.default.Runner.run(this.runner, this.engine);
2008
2029
  this.loop();
2009
2030
  }
2010
2031
  stop() {
@@ -2082,7 +2103,7 @@ var OverlayScene = class {
2082
2103
  * Spawn an object synchronously.
2083
2104
  * Object behavior is determined by tags:
2084
2105
  * - 'falling': Object is dynamic (affected by gravity)
2085
- * - 'window_follow': Object follows mouse when grounded (walks toward mouse)
2106
+ * - 'follow_window': Object follows mouse when grounded (walks toward mouse)
2086
2107
  * - 'grabable': Object can be grabbed and moved with mouse
2087
2108
  * Without 'falling' tag, object is static.
2088
2109
  */
@@ -2378,6 +2399,7 @@ var OverlayScene = class {
2378
2399
  import_matter_js5.default.Composite.allBodies(this.engine.world),
2379
2400
  position
2380
2401
  );
2402
+ logger.debug("OverlayScene", "Grabbed position " + position + ', had "' + bodies.length + '"');
2381
2403
  for (const body of bodies) {
2382
2404
  const entry = this.findObjectByBody(body);
2383
2405
  if (entry && entry.tags.includes("grabable")) {
@@ -2389,6 +2411,24 @@ var OverlayScene = class {
2389
2411
  return entry.id;
2390
2412
  }
2391
2413
  }
2414
+ const r2 = this.grabHistoryRadius * this.grabHistoryRadius;
2415
+ for (const entry of this.objects.values()) {
2416
+ if (!entry.tags.includes("grabable")) continue;
2417
+ const history = this.bodyPositionHistory.get(entry.body.id);
2418
+ if (!history) continue;
2419
+ for (const pastPos of history) {
2420
+ const dx = position.x - pastPos.x;
2421
+ const dy = position.y - pastPos.y;
2422
+ if (dx * dx + dy * dy <= r2) {
2423
+ this.grabbedObjectId = entry.id;
2424
+ this.lastGrabMousePosition = { x: position.x, y: position.y };
2425
+ this.grabVelocity = { x: 0, y: 0 };
2426
+ this.grabbedWasDynamic = !entry.body.isStatic;
2427
+ import_matter_js5.default.Body.setStatic(entry.body, true);
2428
+ return entry.id;
2429
+ }
2430
+ }
2431
+ }
2392
2432
  return null;
2393
2433
  }
2394
2434
  /**
@@ -2402,6 +2442,7 @@ var OverlayScene = class {
2402
2442
  import_matter_js5.default.Sleeping.set(entry.body, false);
2403
2443
  import_matter_js5.default.Body.setVelocity(entry.body, this.grabVelocity);
2404
2444
  import_matter_js5.default.Body.setAngularVelocity(entry.body, 0);
2445
+ this.bodyPositionHistory.delete(entry.body.id);
2405
2446
  }
2406
2447
  }
2407
2448
  this.grabbedObjectId = null;
@@ -3394,10 +3435,24 @@ var OverlayScene = class {
3394
3435
  this.updateCallbacks.forEach((cb) => cb(data));
3395
3436
  }
3396
3437
  };
3438
+
3439
+ // src/tags.ts
3440
+ var TAG_FALLING = "falling";
3441
+ var TAG_FOLLOW_WINDOW = "follow_window";
3442
+ var TAG_GRABABLE = "grabable";
3443
+ var TAGS = {
3444
+ FALLING: TAG_FALLING,
3445
+ FOLLOW_WINDOW: TAG_FOLLOW_WINDOW,
3446
+ GRABABLE: TAG_GRABABLE
3447
+ };
3397
3448
  // Annotate the CommonJS export names for ESM import in node:
3398
3449
  0 && (module.exports = {
3399
3450
  BackgroundManager,
3400
3451
  OverlayScene,
3452
+ TAGS,
3453
+ TAG_FALLING,
3454
+ TAG_FOLLOW_WINDOW,
3455
+ TAG_GRABABLE,
3401
3456
  clearFontCache,
3402
3457
  getGlyphData,
3403
3458
  getKerning,