@blorkfield/overlay-core 0.7.1 → 0.8.1

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/dist/index.js CHANGED
@@ -854,12 +854,6 @@ function clearFontCache() {
854
854
 
855
855
  // src/entity.ts
856
856
  import Matter3 from "matter-js";
857
- var MOUSE_FORCE = 1e-3;
858
- function applyMouseForce(entity, mouseX, grounded) {
859
- if (!grounded) return;
860
- const direction = Math.sign(mouseX - entity.position.x);
861
- Matter3.Body.applyForce(entity, entity.position, { x: MOUSE_FORCE * direction, y: 0 });
862
- }
863
857
  function wrapHorizontal(entity, bounds) {
864
858
  if (entity.position.x < bounds.left) {
865
859
  Matter3.Body.setPosition(entity, { x: bounds.right, y: entity.position.y });
@@ -1380,7 +1374,6 @@ var OverlayScene = class {
1380
1374
  this.objects = /* @__PURE__ */ new Map();
1381
1375
  this.boundaries = [];
1382
1376
  this.updateCallbacks = [];
1383
- this.mouseX = 0;
1384
1377
  this.animationFrameId = null;
1385
1378
  this.mouse = null;
1386
1379
  this.mouseConstraint = null;
@@ -1397,6 +1390,14 @@ var OverlayScene = class {
1397
1390
  this.floorSegmentPressure = /* @__PURE__ */ new Map();
1398
1391
  // segment index -> object IDs
1399
1392
  this.collapsedSegments = /* @__PURE__ */ new Set();
1393
+ // Lifecycle event callbacks
1394
+ this.lifecycleCallbacks = {
1395
+ objectSpawned: [],
1396
+ objectRemoved: [],
1397
+ objectCollision: []
1398
+ };
1399
+ // Follow targets for follow-{key} tagged objects
1400
+ this.followTargets = /* @__PURE__ */ new Map();
1400
1401
  /** Filter drag events - only allow grabbing objects with 'grabable' tag */
1401
1402
  this.handleStartDrag = (event) => {
1402
1403
  const body = event.body;
@@ -1446,17 +1447,48 @@ var OverlayScene = class {
1446
1447
  this.handleAfterRender = () => {
1447
1448
  this.backgroundManager.renderOverlay();
1448
1449
  };
1450
+ /**
1451
+ * Handler for Matter.js collision events.
1452
+ * Emits objectCollision lifecycle events.
1453
+ */
1454
+ this.handleCollisionStart = (event) => {
1455
+ for (const pair of event.pairs) {
1456
+ const entryA = this.findObjectByBody(pair.bodyA);
1457
+ const entryB = this.findObjectByBody(pair.bodyB);
1458
+ if (entryA && entryB) {
1459
+ this.emitLifecycleEvent(
1460
+ "objectCollision",
1461
+ this.toObjectState(entryA),
1462
+ this.toObjectState(entryB)
1463
+ );
1464
+ }
1465
+ }
1466
+ };
1449
1467
  // ==================== PRIVATE ====================
1450
1468
  this.loop = () => {
1451
1469
  this.effectManager.update();
1452
1470
  this.checkTTLExpiration();
1453
1471
  this.checkDespawnBelowFloor();
1454
1472
  this.updatePressure();
1455
- const mouseX = this.mouse?.position.x ?? this.mouseX;
1473
+ if (!this.followTargets.has("mouse") && this.mouse) {
1474
+ this.followTargets.set("mouse", { x: this.mouse.position.x, y: this.mouse.position.y });
1475
+ }
1456
1476
  for (const entry of this.objects.values()) {
1457
1477
  const isDragging = this.mouseConstraint?.body === entry.body;
1458
- if (!isDragging && entry.tags.includes("follow")) {
1459
- applyMouseForce(entry.body, mouseX, this.isGrounded(entry.body));
1478
+ if (!isDragging) {
1479
+ for (const tag of entry.tags) {
1480
+ const key = tag === "follow" ? "mouse" : tag.startsWith("follow-") ? tag.slice(7) : null;
1481
+ if (key) {
1482
+ const target = this.followTargets.get(key);
1483
+ if (target) {
1484
+ const grounded = this.isGrounded(entry.body);
1485
+ if (grounded) {
1486
+ const direction = Math.sign(target.x - entry.body.position.x);
1487
+ Matter5.Body.applyForce(entry.body, entry.body.position, { x: 1e-3 * direction, y: 0 });
1488
+ }
1489
+ }
1490
+ }
1491
+ }
1460
1492
  }
1461
1493
  if (this.config.wrapHorizontal && entry.tags.includes("falling")) {
1462
1494
  wrapHorizontal(entry.body, this.config.bounds);
@@ -1530,6 +1562,7 @@ var OverlayScene = class {
1530
1562
  }
1531
1563
  Matter5.Events.on(this.render, "beforeRender", this.handleBeforeRender);
1532
1564
  Matter5.Events.on(this.render, "afterRender", this.handleAfterRender);
1565
+ Matter5.Events.on(this.engine, "collisionStart", this.handleCollisionStart);
1533
1566
  }
1534
1567
  static createContainer(parent, options = {}) {
1535
1568
  const canvas = document.createElement("canvas");
@@ -1934,6 +1967,7 @@ var OverlayScene = class {
1934
1967
  this.canvas.removeEventListener("click", this.handleCanvasClick);
1935
1968
  Matter5.Events.off(this.render, "beforeRender", this.handleBeforeRender);
1936
1969
  Matter5.Events.off(this.render, "afterRender", this.handleAfterRender);
1970
+ Matter5.Events.off(this.engine, "collisionStart", this.handleCollisionStart);
1937
1971
  Matter5.Engine.clear(this.engine);
1938
1972
  this.objects.clear();
1939
1973
  this.obstaclePressure.clear();
@@ -1942,6 +1976,10 @@ var OverlayScene = class {
1942
1976
  this.floorSegmentPressure.clear();
1943
1977
  this.collapsedSegments.clear();
1944
1978
  this.updateCallbacks = [];
1979
+ this.lifecycleCallbacks.objectSpawned = [];
1980
+ this.lifecycleCallbacks.objectRemoved = [];
1981
+ this.lifecycleCallbacks.objectCollision = [];
1982
+ this.followTargets.clear();
1945
1983
  }
1946
1984
  setDebug(enabled) {
1947
1985
  this.config.debug = enabled;
@@ -2056,6 +2094,7 @@ var OverlayScene = class {
2056
2094
  if (isStatic && pressureThreshold !== void 0) {
2057
2095
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2058
2096
  }
2097
+ this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
2059
2098
  return id;
2060
2099
  }
2061
2100
  /**
@@ -2111,6 +2150,7 @@ var OverlayScene = class {
2111
2150
  if (isStatic && pressureThreshold !== void 0) {
2112
2151
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2113
2152
  }
2153
+ this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
2114
2154
  return id;
2115
2155
  }
2116
2156
  /**
@@ -2192,6 +2232,7 @@ var OverlayScene = class {
2192
2232
  removeObject(id) {
2193
2233
  const entry = this.objects.get(id);
2194
2234
  if (!entry) return;
2235
+ this.emitLifecycleEvent("objectRemoved", this.toObjectState(entry));
2195
2236
  Matter5.Composite.remove(this.engine.world, entry.body);
2196
2237
  this.objects.delete(id);
2197
2238
  }
@@ -2241,8 +2282,208 @@ var OverlayScene = class {
2241
2282
  }
2242
2283
  return Array.from(tagsSet).sort();
2243
2284
  }
2244
- setMousePosition(x, _y) {
2245
- this.mouseX = x;
2285
+ /**
2286
+ * Set the mouse position for follow behavior.
2287
+ * This overrides the browser mouse position for the 'follow' and 'follow-mouse' tags.
2288
+ * @deprecated Use setFollowTarget('mouse', x, y) instead
2289
+ */
2290
+ setMousePosition(x, y) {
2291
+ this.setFollowTarget("mouse", x, y);
2292
+ }
2293
+ /**
2294
+ * Set a follow target position. Objects with 'follow-{key}' tag will
2295
+ * automatically move toward this target each frame.
2296
+ * @param key - The target key (e.g., 'absolute' for 'follow-absolute' tag)
2297
+ * @param x - Target X position
2298
+ * @param y - Target Y position
2299
+ */
2300
+ setFollowTarget(key, x, y) {
2301
+ this.followTargets.set(key, { x, y });
2302
+ if (key === "mouse" && this.mouse) {
2303
+ this.mouse.position.x = x;
2304
+ this.mouse.position.y = y;
2305
+ this.mouse.absolute.x = x;
2306
+ this.mouse.absolute.y = y;
2307
+ }
2308
+ }
2309
+ /**
2310
+ * Remove a follow target. Objects with the corresponding tag will stop following.
2311
+ * @param key - The target key to remove
2312
+ */
2313
+ removeFollowTarget(key) {
2314
+ this.followTargets.delete(key);
2315
+ }
2316
+ /**
2317
+ * Get all registered follow target keys.
2318
+ * @returns Array of follow target keys
2319
+ */
2320
+ getFollowTargetKeys() {
2321
+ return Array.from(this.followTargets.keys());
2322
+ }
2323
+ // ==================== GRAB/DRAG METHODS ====================
2324
+ /**
2325
+ * Programmatically grab an object at the current mouse position.
2326
+ * Uses the externally set mouse position (via setFollowTarget('mouse', x, y))
2327
+ * or the native canvas mouse position if no external position is set.
2328
+ * Only objects with the 'grabable' tag can be grabbed.
2329
+ * @returns The ID of the grabbed object, or null if no grabable object at position
2330
+ */
2331
+ startGrab() {
2332
+ if (!this.mouseConstraint || !this.mouse) return null;
2333
+ const mouseTarget = this.followTargets.get("mouse");
2334
+ const position = mouseTarget ?? { x: this.mouse.position.x, y: this.mouse.position.y };
2335
+ const bodies = Matter5.Query.point(
2336
+ Matter5.Composite.allBodies(this.engine.world),
2337
+ position
2338
+ );
2339
+ for (const body of bodies) {
2340
+ const entry = this.findObjectByBody(body);
2341
+ if (entry && entry.tags.includes("grabable")) {
2342
+ this.mouseConstraint.constraint.bodyB = entry.body;
2343
+ this.mouseConstraint.constraint.pointB = {
2344
+ x: position.x - entry.body.position.x,
2345
+ y: position.y - entry.body.position.y
2346
+ };
2347
+ return entry.id;
2348
+ }
2349
+ }
2350
+ return null;
2351
+ }
2352
+ /**
2353
+ * Release any currently grabbed object.
2354
+ */
2355
+ endGrab() {
2356
+ if (this.mouseConstraint) {
2357
+ this.mouseConstraint.constraint.bodyB = null;
2358
+ }
2359
+ }
2360
+ /**
2361
+ * Get the ID of the currently grabbed object.
2362
+ * @returns The ID of the grabbed object, or null if nothing is grabbed
2363
+ */
2364
+ getGrabbedObject() {
2365
+ if (!this.mouseConstraint?.constraint.bodyB) return null;
2366
+ const entry = this.findObjectByBody(this.mouseConstraint.constraint.bodyB);
2367
+ return entry?.id ?? null;
2368
+ }
2369
+ // ==================== PHYSICS MANIPULATION METHODS ====================
2370
+ /**
2371
+ * Apply a force to an object.
2372
+ * @param objectId - The ID of the object
2373
+ * @param force - The force vector to apply
2374
+ */
2375
+ applyForce(objectId, force) {
2376
+ const entry = this.objects.get(objectId);
2377
+ if (!entry) return;
2378
+ Matter5.Body.applyForce(entry.body, entry.body.position, force);
2379
+ }
2380
+ /**
2381
+ * Apply a force to all objects with a specific tag.
2382
+ * @param tag - The tag to match
2383
+ * @param force - The force vector to apply
2384
+ */
2385
+ applyForceToTag(tag, force) {
2386
+ for (const entry of this.objects.values()) {
2387
+ if (entry.tags.includes(tag)) {
2388
+ Matter5.Body.applyForce(entry.body, entry.body.position, force);
2389
+ }
2390
+ }
2391
+ }
2392
+ /**
2393
+ * Set the velocity of an object.
2394
+ * @param objectId - The ID of the object
2395
+ * @param velocity - The velocity vector to set
2396
+ */
2397
+ setVelocity(objectId, velocity) {
2398
+ const entry = this.objects.get(objectId);
2399
+ if (!entry) return;
2400
+ Matter5.Body.setVelocity(entry.body, velocity);
2401
+ }
2402
+ /**
2403
+ * Set the position of an object.
2404
+ * @param objectId - The ID of the object
2405
+ * @param position - The position to set
2406
+ */
2407
+ setPosition(objectId, position) {
2408
+ const entry = this.objects.get(objectId);
2409
+ if (!entry) return;
2410
+ Matter5.Body.setPosition(entry.body, position);
2411
+ }
2412
+ // ==================== OBJECT STATE METHODS ====================
2413
+ /**
2414
+ * Get the current state of an object.
2415
+ * @param id - The ID of the object
2416
+ * @returns The object state, or null if not found
2417
+ */
2418
+ getObject(id) {
2419
+ const entry = this.objects.get(id);
2420
+ if (!entry) return null;
2421
+ return {
2422
+ id: entry.id,
2423
+ x: entry.body.position.x,
2424
+ y: entry.body.position.y,
2425
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2426
+ angle: entry.body.angle,
2427
+ tags: [...entry.tags]
2428
+ };
2429
+ }
2430
+ /**
2431
+ * Get the current state of all objects with a specific tag.
2432
+ * @param tag - The tag to match
2433
+ * @returns Array of object states
2434
+ */
2435
+ getObjectsByTag(tag) {
2436
+ const result = [];
2437
+ for (const entry of this.objects.values()) {
2438
+ if (entry.tags.includes(tag)) {
2439
+ result.push({
2440
+ id: entry.id,
2441
+ x: entry.body.position.x,
2442
+ y: entry.body.position.y,
2443
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2444
+ angle: entry.body.angle,
2445
+ tags: [...entry.tags]
2446
+ });
2447
+ }
2448
+ }
2449
+ return result;
2450
+ }
2451
+ // ==================== LIFECYCLE EVENTS ====================
2452
+ /**
2453
+ * Subscribe to a lifecycle event.
2454
+ * @param event - The event type to subscribe to
2455
+ * @param callback - The callback to invoke when the event occurs
2456
+ */
2457
+ on(event, callback) {
2458
+ this.lifecycleCallbacks[event].push(callback);
2459
+ }
2460
+ /**
2461
+ * Unsubscribe from a lifecycle event.
2462
+ * @param event - The event type to unsubscribe from
2463
+ * @param callback - The callback to remove
2464
+ */
2465
+ off(event, callback) {
2466
+ const arr = this.lifecycleCallbacks[event];
2467
+ const idx = arr.indexOf(callback);
2468
+ if (idx !== -1) arr.splice(idx, 1);
2469
+ }
2470
+ /** Create ObjectState from an ObjectEntry */
2471
+ toObjectState(entry) {
2472
+ return {
2473
+ id: entry.id,
2474
+ x: entry.body.position.x,
2475
+ y: entry.body.position.y,
2476
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2477
+ angle: entry.body.angle,
2478
+ tags: [...entry.tags]
2479
+ };
2480
+ }
2481
+ /** Emit a lifecycle event to all registered callbacks */
2482
+ emitLifecycleEvent(event, ...args) {
2483
+ const callbacks = this.lifecycleCallbacks[event];
2484
+ for (const cb of callbacks) {
2485
+ cb(...args);
2486
+ }
2246
2487
  }
2247
2488
  // ==================== PRESSURE TRACKING METHODS ====================
2248
2489
  /**