@blorkfield/overlay-core 0.8.11 → 0.10.0
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 +209 -36
- package/dist/index.cjs +350 -102
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -31
- package/dist/index.d.ts +128 -31
- package/dist/index.js +346 -101
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import Matter5 from "matter-js";
|
|
|
5
5
|
import Matter from "matter-js";
|
|
6
6
|
function createEngine(gravity) {
|
|
7
7
|
const engine = Matter.Engine.create();
|
|
8
|
-
engine.gravity.
|
|
8
|
+
engine.gravity.x = gravity.x;
|
|
9
|
+
engine.gravity.y = -gravity.y;
|
|
9
10
|
return engine;
|
|
10
11
|
}
|
|
11
12
|
function createRender(engine, canvas, config) {
|
|
@@ -455,6 +456,7 @@ function createBodyFromVertices(id, x, y, vertices, renderOptions) {
|
|
|
455
456
|
restitution: 0.3,
|
|
456
457
|
friction: 0.1,
|
|
457
458
|
frictionAir: 0.01,
|
|
459
|
+
density: 5e-3,
|
|
458
460
|
label: `entity:${id}`,
|
|
459
461
|
render: renderOptions
|
|
460
462
|
});
|
|
@@ -547,6 +549,7 @@ function createCircleEntity(id, config) {
|
|
|
547
549
|
restitution: 0.3,
|
|
548
550
|
friction: 0.1,
|
|
549
551
|
frictionAir: 0.01,
|
|
552
|
+
density: 5e-3,
|
|
550
553
|
label: `entity:${id}`,
|
|
551
554
|
render: createFillRenderOptions(config)
|
|
552
555
|
});
|
|
@@ -558,6 +561,7 @@ function createCircleEntityWithSprite(id, config, imageWidth, imageHeight) {
|
|
|
558
561
|
restitution: 0.3,
|
|
559
562
|
friction: 0.1,
|
|
560
563
|
frictionAir: 0.01,
|
|
564
|
+
density: 5e-3,
|
|
561
565
|
label: `entity:${id}`,
|
|
562
566
|
render: createSpriteRenderOptions(config, imageWidth, imageHeight)
|
|
563
567
|
});
|
|
@@ -988,8 +992,7 @@ var EffectManager = class {
|
|
|
988
992
|
const objectConfig = this.selectObjectConfig(config.objectConfigs);
|
|
989
993
|
if (!objectConfig) continue;
|
|
990
994
|
const radius = this.calculateRadius(objectConfig);
|
|
991
|
-
const tags =
|
|
992
|
-
if (!tags.includes("falling")) tags.push("falling");
|
|
995
|
+
const tags = (objectConfig.objectConfig.tags ?? []).filter((t) => t !== "static");
|
|
993
996
|
const fullConfig = {
|
|
994
997
|
...objectConfig.objectConfig,
|
|
995
998
|
x: originX,
|
|
@@ -1028,8 +1031,7 @@ var EffectManager = class {
|
|
|
1028
1031
|
const radius = this.calculateRadius(objectConfig);
|
|
1029
1032
|
const x = this.randomInRange(spawnAreaStart + radius, spawnAreaStart + spawnAreaWidth - radius);
|
|
1030
1033
|
const y = bounds.top - radius;
|
|
1031
|
-
const tags =
|
|
1032
|
-
if (!tags.includes("falling")) tags.push("falling");
|
|
1034
|
+
const tags = (objectConfig.objectConfig.tags ?? []).filter((t) => t !== "static");
|
|
1033
1035
|
const fullConfig = {
|
|
1034
1036
|
...objectConfig.objectConfig,
|
|
1035
1037
|
x,
|
|
@@ -1054,8 +1056,7 @@ var EffectManager = class {
|
|
|
1054
1056
|
const objectConfig = this.selectObjectConfig(config.objectConfigs);
|
|
1055
1057
|
if (!objectConfig) return;
|
|
1056
1058
|
const radius = this.calculateRadius(objectConfig);
|
|
1057
|
-
const tags =
|
|
1058
|
-
if (!tags.includes("falling")) tags.push("falling");
|
|
1059
|
+
const tags = (objectConfig.objectConfig.tags ?? []).filter((t) => t !== "static");
|
|
1059
1060
|
const fullConfig = {
|
|
1060
1061
|
...objectConfig.objectConfig,
|
|
1061
1062
|
x: config.origin.x,
|
|
@@ -1407,6 +1408,12 @@ var OverlayScene = class {
|
|
|
1407
1408
|
this.grabHistoryRadius = 20;
|
|
1408
1409
|
// Number of physics substeps per frame — more substeps = better collision at high speeds, more CPU
|
|
1409
1410
|
this.substeps = 2;
|
|
1411
|
+
// Tracks only the bodies with a gravity override — engine gravity handles everyone else
|
|
1412
|
+
this.gravityOverrideEntries = /* @__PURE__ */ new Set();
|
|
1413
|
+
// Tracks only the bodies with follow_window tag — avoids scanning all objects each frame
|
|
1414
|
+
this.followWindowEntries = /* @__PURE__ */ new Set();
|
|
1415
|
+
// IDs of static objects that have pressure thresholds — empty = skip updatePressure entirely
|
|
1416
|
+
this.pressureObstacleIds = /* @__PURE__ */ new Set();
|
|
1410
1417
|
/** Handle mouse down - start grab via programmatic API */
|
|
1411
1418
|
this.handleMouseDown = (event) => {
|
|
1412
1419
|
const rect = this.canvas.getBoundingClientRect();
|
|
@@ -1438,7 +1445,7 @@ var OverlayScene = class {
|
|
|
1438
1445
|
for (const body of bodies) {
|
|
1439
1446
|
const entry = this.findObjectByBody(body);
|
|
1440
1447
|
if (!entry) continue;
|
|
1441
|
-
if (entry.tags.includes("
|
|
1448
|
+
if (!entry.tags.includes("static")) continue;
|
|
1442
1449
|
if (entry.clicksRemaining === void 0) continue;
|
|
1443
1450
|
entry.clicksRemaining--;
|
|
1444
1451
|
const name = this.getObstacleDisplayName(entry);
|
|
@@ -1484,13 +1491,23 @@ var OverlayScene = class {
|
|
|
1484
1491
|
// ==================== PRIVATE ====================
|
|
1485
1492
|
this.loop = () => {
|
|
1486
1493
|
const substepDelta = 1e3 / 60 / this.substeps;
|
|
1494
|
+
const scale = this.engine.gravity.scale;
|
|
1487
1495
|
for (let i = 0; i < this.substeps; i++) {
|
|
1496
|
+
for (const entry of this.gravityOverrideEntries) {
|
|
1497
|
+
if (entry.body.isStatic || entry.body.isSleeping) continue;
|
|
1498
|
+
const g = entry.gravityOverride;
|
|
1499
|
+
Matter5.Body.applyForce(entry.body, entry.body.position, {
|
|
1500
|
+
x: entry.body.mass * (g.x - this.engine.gravity.x) * scale,
|
|
1501
|
+
y: entry.body.mass * (g.y - this.engine.gravity.y) * scale
|
|
1502
|
+
});
|
|
1503
|
+
}
|
|
1488
1504
|
Matter5.Engine.update(this.engine, substepDelta);
|
|
1489
1505
|
}
|
|
1490
1506
|
this.effectManager.update();
|
|
1491
|
-
this.
|
|
1492
|
-
this.
|
|
1493
|
-
|
|
1507
|
+
this.checkExpiration();
|
|
1508
|
+
if (this.pressureObstacleIds.size > 0 || this.floorSegments.length > 0) {
|
|
1509
|
+
this.updatePressure();
|
|
1510
|
+
}
|
|
1494
1511
|
if (this.grabbedObjectId && this.lastGrabMousePosition) {
|
|
1495
1512
|
const entry = this.objects.get(this.grabbedObjectId);
|
|
1496
1513
|
const mouseTarget = this.followTargets.get("mouse");
|
|
@@ -1504,27 +1521,39 @@ var OverlayScene = class {
|
|
|
1504
1521
|
this.lastGrabMousePosition = { x: mouseTarget.x, y: mouseTarget.y };
|
|
1505
1522
|
}
|
|
1506
1523
|
}
|
|
1507
|
-
for (const entry of this.
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1524
|
+
for (const entry of this.followWindowEntries) {
|
|
1525
|
+
if (this.grabbedObjectId === entry.id) continue;
|
|
1526
|
+
const targetKey = entry.followTarget ?? "mouse";
|
|
1527
|
+
let targetPos;
|
|
1528
|
+
targetPos = this.followTargets.get(targetKey);
|
|
1529
|
+
if (!targetPos) {
|
|
1530
|
+
const targetEntry = this.objects.get(targetKey);
|
|
1531
|
+
if (targetEntry) targetPos = targetEntry.body.position;
|
|
1532
|
+
}
|
|
1533
|
+
if (!targetPos) {
|
|
1534
|
+
for (const e of this.objects.values()) {
|
|
1535
|
+
if (e !== entry && e.tags.includes(targetKey)) {
|
|
1536
|
+
targetPos = e.body.position;
|
|
1537
|
+
break;
|
|
1521
1538
|
}
|
|
1522
1539
|
}
|
|
1523
1540
|
}
|
|
1524
|
-
if (
|
|
1541
|
+
if (targetPos) {
|
|
1542
|
+
const dx = targetPos.x - entry.body.position.x;
|
|
1543
|
+
const dy = targetPos.y - entry.body.position.y;
|
|
1544
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
1545
|
+
if (dist > 0) {
|
|
1546
|
+
const speedMult = entry.speedOverride ?? 1;
|
|
1547
|
+
const mag = 1e-3 * speedMult / dist;
|
|
1548
|
+
Matter5.Body.applyForce(entry.body, entry.body.position, { x: mag * dx, y: mag * dy });
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
for (const entry of this.objects.values()) {
|
|
1553
|
+
if (this.config.wrapHorizontal && !entry.tags.includes("static")) {
|
|
1525
1554
|
wrapHorizontal(entry.body, this.config.bounds);
|
|
1526
1555
|
}
|
|
1527
|
-
if (entry.domElement && entry.tags.includes("
|
|
1556
|
+
if (entry.domElement && !entry.tags.includes("static")) {
|
|
1528
1557
|
this.updateDOMElementTransform(entry);
|
|
1529
1558
|
}
|
|
1530
1559
|
if (entry.tags.includes("grabable") && !entry.body.isStatic) {
|
|
@@ -1547,7 +1576,7 @@ var OverlayScene = class {
|
|
|
1547
1576
|
};
|
|
1548
1577
|
this.canvas = canvas;
|
|
1549
1578
|
this.config = {
|
|
1550
|
-
gravity: 1,
|
|
1579
|
+
gravity: { x: 0, y: -1 },
|
|
1551
1580
|
wrapHorizontal: true,
|
|
1552
1581
|
debug: false,
|
|
1553
1582
|
...config
|
|
@@ -1621,7 +1650,7 @@ var OverlayScene = class {
|
|
|
1621
1650
|
const obstacles = [];
|
|
1622
1651
|
const dynamics = [];
|
|
1623
1652
|
for (const entry of this.objects.values()) {
|
|
1624
|
-
if (entry.tags.includes("
|
|
1653
|
+
if (!entry.tags.includes("static")) {
|
|
1625
1654
|
if (Math.abs(entry.body.velocity.y) < 2) {
|
|
1626
1655
|
dynamics.push(entry);
|
|
1627
1656
|
}
|
|
@@ -1844,16 +1873,17 @@ var OverlayScene = class {
|
|
|
1844
1873
|
}
|
|
1845
1874
|
/** Convert a static obstacle to dynamic (make it fall) */
|
|
1846
1875
|
collapseObstacle(entry) {
|
|
1847
|
-
if (entry.tags.includes("
|
|
1876
|
+
if (!entry.tags.includes("static")) return;
|
|
1848
1877
|
const name = this.getObstacleDisplayName(entry);
|
|
1849
1878
|
console.log(`[Pressure] Collapsed: ${name}`);
|
|
1850
1879
|
if (entry.shadow && entry.originalPosition) {
|
|
1851
1880
|
this.createShadow(entry);
|
|
1852
1881
|
}
|
|
1853
|
-
entry.tags.
|
|
1882
|
+
entry.tags = entry.tags.filter((t) => t !== "static");
|
|
1854
1883
|
Matter5.Body.setStatic(entry.body, false);
|
|
1855
1884
|
entry.pressureThreshold = void 0;
|
|
1856
1885
|
entry.wordCollapseTag = void 0;
|
|
1886
|
+
this.pressureObstacleIds.delete(entry.id);
|
|
1857
1887
|
}
|
|
1858
1888
|
/** Create a static shadow copy of an obstacle at its original position */
|
|
1859
1889
|
async createShadow(entry) {
|
|
@@ -1898,6 +1928,7 @@ var OverlayScene = class {
|
|
|
1898
1928
|
id: shadowId,
|
|
1899
1929
|
body,
|
|
1900
1930
|
tags: ["shadow"],
|
|
1931
|
+
entityTag: shadowId,
|
|
1901
1932
|
spawnTime: performance.now(),
|
|
1902
1933
|
weight: 0,
|
|
1903
1934
|
ttfGlyph: {
|
|
@@ -1925,6 +1956,7 @@ var OverlayScene = class {
|
|
|
1925
1956
|
id: shadowId,
|
|
1926
1957
|
body: result.body,
|
|
1927
1958
|
tags: ["shadow"],
|
|
1959
|
+
entityTag: shadowId,
|
|
1928
1960
|
spawnTime: performance.now(),
|
|
1929
1961
|
weight: 0
|
|
1930
1962
|
// Shadows don't contribute to pressure
|
|
@@ -1966,10 +1998,6 @@ var OverlayScene = class {
|
|
|
1966
1998
|
}
|
|
1967
1999
|
return null;
|
|
1968
2000
|
}
|
|
1969
|
-
/** Check if a body is grounded (low vertical velocity indicates resting on something) */
|
|
1970
|
-
isGrounded(body) {
|
|
1971
|
-
return Math.abs(body.velocity.y) < 0.5;
|
|
1972
|
-
}
|
|
1973
2001
|
start() {
|
|
1974
2002
|
Matter5.Render.run(this.render);
|
|
1975
2003
|
this.loop();
|
|
@@ -1993,6 +2021,9 @@ var OverlayScene = class {
|
|
|
1993
2021
|
Matter5.Events.off(this.engine, "collisionStart", this.handleCollisionStart);
|
|
1994
2022
|
Matter5.Engine.clear(this.engine);
|
|
1995
2023
|
this.objects.clear();
|
|
2024
|
+
this.gravityOverrideEntries.clear();
|
|
2025
|
+
this.followWindowEntries.clear();
|
|
2026
|
+
this.pressureObstacleIds.clear();
|
|
1996
2027
|
this.obstaclePressure.clear();
|
|
1997
2028
|
this.previousPressure.clear();
|
|
1998
2029
|
this.pressureLogTimer = 0;
|
|
@@ -2013,6 +2044,106 @@ var OverlayScene = class {
|
|
|
2013
2044
|
}
|
|
2014
2045
|
}
|
|
2015
2046
|
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Set gravity at runtime. Y axis uses physical convention: negative = down, positive = up.
|
|
2049
|
+
* @example
|
|
2050
|
+
* scene.setGravity({ x: 0, y: -1 }); // Normal downward gravity
|
|
2051
|
+
* scene.setGravity({ x: 0, y: 1 }); // Upward gravity
|
|
2052
|
+
* scene.setGravity({ x: 1, y: 0 }); // Rightward gravity
|
|
2053
|
+
* scene.setGravity({ x: 0, y: 0 }); // Zero gravity
|
|
2054
|
+
*/
|
|
2055
|
+
setGravity(gravity) {
|
|
2056
|
+
this.config.gravity = gravity;
|
|
2057
|
+
this.engine.gravity.x = gravity.x;
|
|
2058
|
+
this.engine.gravity.y = -gravity.y;
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Set or clear a per-object gravity override at runtime.
|
|
2062
|
+
* Pass `null` to remove the override and restore scene gravity for that object.
|
|
2063
|
+
*/
|
|
2064
|
+
setObjectGravityOverride(id, gravity) {
|
|
2065
|
+
const entry = this.objects.get(id);
|
|
2066
|
+
if (!entry) return;
|
|
2067
|
+
if (gravity === null) {
|
|
2068
|
+
this.removeTag(id, "gravity_override");
|
|
2069
|
+
} else {
|
|
2070
|
+
entry.gravityOverride = gravity;
|
|
2071
|
+
this.addTag(id, "gravity_override");
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Set or change the follow target for an object's 'follow_window' tag.
|
|
2076
|
+
* Target can be 'mouse', an entity ID, or a tag string (follows first matching entity).
|
|
2077
|
+
* Adds 'follow_window' tag if not already present.
|
|
2078
|
+
*/
|
|
2079
|
+
setFollowWindowTarget(id, target) {
|
|
2080
|
+
const entry = this.objects.get(id);
|
|
2081
|
+
if (!entry) return;
|
|
2082
|
+
entry.followTarget = target;
|
|
2083
|
+
this.addTag(id, "follow_window");
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Set or change the speed multiplier for an object's movement (follow_window and future
|
|
2087
|
+
* movement behaviors). Pass null to remove the override and reset to default speed.
|
|
2088
|
+
* Negative values cause the object to run away from its target.
|
|
2089
|
+
* Adds 'speed_override' tag if not already present.
|
|
2090
|
+
*/
|
|
2091
|
+
setObjectSpeedOverride(id, speed) {
|
|
2092
|
+
const entry = this.objects.get(id);
|
|
2093
|
+
if (!entry) return;
|
|
2094
|
+
if (speed === null) {
|
|
2095
|
+
this.removeTag(id, "speed_override");
|
|
2096
|
+
} else {
|
|
2097
|
+
entry.speedOverride = speed;
|
|
2098
|
+
this.addTag(id, "speed_override");
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* Set or change the physics mass for an object. Pass null to remove the override and restore
|
|
2103
|
+
* the original density-based mass. Adds 'mass_override' tag if not already present.
|
|
2104
|
+
* Higher mass resists applied forces (including follow forces) more strongly.
|
|
2105
|
+
*/
|
|
2106
|
+
setObjectMassOverride(id, mass) {
|
|
2107
|
+
const entry = this.objects.get(id);
|
|
2108
|
+
if (!entry) return;
|
|
2109
|
+
if (mass === null) {
|
|
2110
|
+
this.removeTag(id, "mass_override");
|
|
2111
|
+
} else {
|
|
2112
|
+
if (entry.originalMass === void 0) entry.originalMass = entry.body.mass;
|
|
2113
|
+
entry.massOverride = mass;
|
|
2114
|
+
Matter5.Body.setMass(entry.body, mass);
|
|
2115
|
+
this.addTag(id, "mass_override");
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Set the angular velocity (spin) of an object in radians per second.
|
|
2120
|
+
* Positive = counter-clockwise, negative = clockwise.
|
|
2121
|
+
*/
|
|
2122
|
+
setObjectAngularVelocity(id, omega) {
|
|
2123
|
+
const entry = this.objects.get(id);
|
|
2124
|
+
if (!entry) return;
|
|
2125
|
+
Matter5.Body.setAngularVelocity(entry.body, omega);
|
|
2126
|
+
}
|
|
2127
|
+
/**
|
|
2128
|
+
* Set the absolute scale of an object on each axis. Both physics collision shape and
|
|
2129
|
+
* sprite rendering are updated together. Scaling changes the body's mass proportionally
|
|
2130
|
+
* (area scales by x*y). Use setObjectMassOverride afterwards if you need a fixed mass.
|
|
2131
|
+
* @param x - Scale factor on the X axis (1 = original size)
|
|
2132
|
+
* @param y - Scale factor on the Y axis (1 = original size)
|
|
2133
|
+
*/
|
|
2134
|
+
setObjectScale(id, x, y) {
|
|
2135
|
+
const entry = this.objects.get(id);
|
|
2136
|
+
if (!entry) return;
|
|
2137
|
+
const currentX = entry.scaleX ?? 1;
|
|
2138
|
+
const currentY = entry.scaleY ?? 1;
|
|
2139
|
+
Matter5.Body.scale(entry.body, x / currentX, y / currentY);
|
|
2140
|
+
if (entry.body.render.sprite && entry.baseSpriteSX !== void 0) {
|
|
2141
|
+
entry.body.render.sprite.xScale = entry.baseSpriteSX * x;
|
|
2142
|
+
entry.body.render.sprite.yScale = entry.baseSpriteSY * y;
|
|
2143
|
+
}
|
|
2144
|
+
entry.scaleX = x;
|
|
2145
|
+
entry.scaleY = y;
|
|
2146
|
+
}
|
|
2016
2147
|
/**
|
|
2017
2148
|
* Update the background configuration at runtime.
|
|
2018
2149
|
*/
|
|
@@ -2048,10 +2179,9 @@ var OverlayScene = class {
|
|
|
2048
2179
|
/**
|
|
2049
2180
|
* Spawn an object synchronously.
|
|
2050
2181
|
* Object behavior is determined by tags:
|
|
2051
|
-
* - '
|
|
2182
|
+
* - 'static': Object is not affected by gravity (obstacle). Without this tag, object is dynamic by default.
|
|
2052
2183
|
* - 'follow_window': Object follows mouse when grounded (walks toward mouse)
|
|
2053
2184
|
* - 'grabable': Object can be grabbed and moved with mouse
|
|
2054
|
-
* Without 'falling' tag, object is static.
|
|
2055
2185
|
*/
|
|
2056
2186
|
spawnObject(config) {
|
|
2057
2187
|
if (config.element) {
|
|
@@ -2070,8 +2200,20 @@ var OverlayScene = class {
|
|
|
2070
2200
|
return result.id;
|
|
2071
2201
|
}
|
|
2072
2202
|
const id = crypto.randomUUID();
|
|
2073
|
-
const
|
|
2074
|
-
const
|
|
2203
|
+
const entityDescriptor = config.imageUrl ? config.imageUrl.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "image" : config.radius ? "circle" : config.shape?.type ?? "rect";
|
|
2204
|
+
const entityTag = `${entityDescriptor.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().slice(0, 16)}-${id.slice(0, 4)}`;
|
|
2205
|
+
const tags = [...config.tags ?? []];
|
|
2206
|
+
tags.push(entityTag);
|
|
2207
|
+
if (config.gravityOverride && !tags.includes("gravity_override")) {
|
|
2208
|
+
tags.push("gravity_override");
|
|
2209
|
+
}
|
|
2210
|
+
if (config.speedOverride !== void 0 && !tags.includes("speed_override")) {
|
|
2211
|
+
tags.push("speed_override");
|
|
2212
|
+
}
|
|
2213
|
+
if (config.massOverride !== void 0 && !tags.includes("mass_override")) {
|
|
2214
|
+
tags.push("mass_override");
|
|
2215
|
+
}
|
|
2216
|
+
const isStatic = tags.includes("static");
|
|
2075
2217
|
logger.debug("OverlayScene", `Spawning object`, {
|
|
2076
2218
|
id,
|
|
2077
2219
|
tags,
|
|
@@ -2088,6 +2230,10 @@ var OverlayScene = class {
|
|
|
2088
2230
|
} else {
|
|
2089
2231
|
body = createObstacle(id, config, isStatic);
|
|
2090
2232
|
}
|
|
2233
|
+
const naturalMass = body.mass;
|
|
2234
|
+
if (config.massOverride !== void 0) {
|
|
2235
|
+
Matter5.Body.setMass(body, config.massOverride);
|
|
2236
|
+
}
|
|
2091
2237
|
let pressureThreshold;
|
|
2092
2238
|
if (config.pressureThreshold) {
|
|
2093
2239
|
pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
|
|
@@ -2110,12 +2256,25 @@ var OverlayScene = class {
|
|
|
2110
2256
|
pressureThreshold,
|
|
2111
2257
|
shadow,
|
|
2112
2258
|
originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
|
|
2113
|
-
clicksRemaining
|
|
2259
|
+
clicksRemaining,
|
|
2260
|
+
gravityOverride: config.gravityOverride,
|
|
2261
|
+
followTarget: tags.includes("follow_window") ? config.followTarget ?? "mouse" : void 0,
|
|
2262
|
+
speedOverride: tags.includes("speed_override") ? config.speedOverride ?? 1 : void 0,
|
|
2263
|
+
massOverride: tags.includes("mass_override") ? config.massOverride : void 0,
|
|
2264
|
+
originalMass: tags.includes("mass_override") ? naturalMass : void 0,
|
|
2265
|
+
entityTag,
|
|
2266
|
+
scaleX: 1,
|
|
2267
|
+
scaleY: 1,
|
|
2268
|
+
baseSpriteSX: body.render.sprite?.xScale,
|
|
2269
|
+
baseSpriteSY: body.render.sprite?.yScale
|
|
2114
2270
|
};
|
|
2115
2271
|
this.objects.set(id, entry);
|
|
2272
|
+
if (config.gravityOverride) this.gravityOverrideEntries.add(entry);
|
|
2273
|
+
if (tags.includes("follow_window")) this.followWindowEntries.add(entry);
|
|
2116
2274
|
Matter5.Composite.add(this.engine.world, body);
|
|
2117
2275
|
if (isStatic && pressureThreshold !== void 0) {
|
|
2118
2276
|
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
2277
|
+
this.pressureObstacleIds.add(id);
|
|
2119
2278
|
}
|
|
2120
2279
|
this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
|
|
2121
2280
|
return id;
|
|
@@ -2126,8 +2285,20 @@ var OverlayScene = class {
|
|
|
2126
2285
|
*/
|
|
2127
2286
|
async spawnObjectAsync(config) {
|
|
2128
2287
|
const id = crypto.randomUUID();
|
|
2129
|
-
const
|
|
2130
|
-
const
|
|
2288
|
+
const entityDescriptor = config.imageUrl ? config.imageUrl.split("/").pop()?.replace(/\.[^.]+$/, "") ?? "image" : config.radius ? "circle" : config.shape?.type ?? "rect";
|
|
2289
|
+
const entityTag = `${entityDescriptor.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().slice(0, 16)}-${id.slice(0, 4)}`;
|
|
2290
|
+
const tags = [...config.tags ?? []];
|
|
2291
|
+
tags.push(entityTag);
|
|
2292
|
+
if (config.gravityOverride && !tags.includes("gravity_override")) {
|
|
2293
|
+
tags.push("gravity_override");
|
|
2294
|
+
}
|
|
2295
|
+
if (config.speedOverride !== void 0 && !tags.includes("speed_override")) {
|
|
2296
|
+
tags.push("speed_override");
|
|
2297
|
+
}
|
|
2298
|
+
if (config.massOverride !== void 0 && !tags.includes("mass_override")) {
|
|
2299
|
+
tags.push("mass_override");
|
|
2300
|
+
}
|
|
2301
|
+
const isStatic = tags.includes("static");
|
|
2131
2302
|
logger.debug("OverlayScene", `Spawning object async`, {
|
|
2132
2303
|
id,
|
|
2133
2304
|
tags,
|
|
@@ -2144,6 +2315,10 @@ var OverlayScene = class {
|
|
|
2144
2315
|
} else {
|
|
2145
2316
|
body = await createObstacleAsync(id, config, isStatic);
|
|
2146
2317
|
}
|
|
2318
|
+
const naturalMass = body.mass;
|
|
2319
|
+
if (config.massOverride !== void 0) {
|
|
2320
|
+
Matter5.Body.setMass(body, config.massOverride);
|
|
2321
|
+
}
|
|
2147
2322
|
let pressureThreshold;
|
|
2148
2323
|
if (config.pressureThreshold) {
|
|
2149
2324
|
pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
|
|
@@ -2166,26 +2341,39 @@ var OverlayScene = class {
|
|
|
2166
2341
|
pressureThreshold,
|
|
2167
2342
|
shadow,
|
|
2168
2343
|
originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
|
|
2169
|
-
clicksRemaining
|
|
2344
|
+
clicksRemaining,
|
|
2345
|
+
gravityOverride: config.gravityOverride,
|
|
2346
|
+
followTarget: tags.includes("follow_window") ? config.followTarget ?? "mouse" : void 0,
|
|
2347
|
+
speedOverride: tags.includes("speed_override") ? config.speedOverride ?? 1 : void 0,
|
|
2348
|
+
massOverride: tags.includes("mass_override") ? config.massOverride : void 0,
|
|
2349
|
+
originalMass: tags.includes("mass_override") ? naturalMass : void 0,
|
|
2350
|
+
entityTag,
|
|
2351
|
+
scaleX: 1,
|
|
2352
|
+
scaleY: 1,
|
|
2353
|
+
baseSpriteSX: body.render.sprite?.xScale,
|
|
2354
|
+
baseSpriteSY: body.render.sprite?.yScale
|
|
2170
2355
|
};
|
|
2171
2356
|
this.objects.set(id, entry);
|
|
2357
|
+
if (config.gravityOverride) this.gravityOverrideEntries.add(entry);
|
|
2358
|
+
if (tags.includes("follow_window")) this.followWindowEntries.add(entry);
|
|
2172
2359
|
Matter5.Composite.add(this.engine.world, body);
|
|
2173
2360
|
if (isStatic && pressureThreshold !== void 0) {
|
|
2174
2361
|
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
2362
|
+
this.pressureObstacleIds.add(id);
|
|
2175
2363
|
}
|
|
2176
2364
|
this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
|
|
2177
2365
|
return id;
|
|
2178
2366
|
}
|
|
2179
2367
|
/**
|
|
2180
|
-
*
|
|
2368
|
+
* Make an object dynamic (remove 'static' tag, affected by gravity).
|
|
2181
2369
|
* Also adds 'grabable' tag so released objects can be dragged.
|
|
2182
2370
|
* This is the tag-based replacement for releaseObstacle().
|
|
2183
2371
|
*/
|
|
2184
2372
|
addFallingTag(id) {
|
|
2185
2373
|
const entry = this.objects.get(id);
|
|
2186
2374
|
if (!entry) return;
|
|
2187
|
-
if (
|
|
2188
|
-
entry.tags.
|
|
2375
|
+
if (entry.tags.includes("static")) {
|
|
2376
|
+
entry.tags = entry.tags.filter((t) => t !== "static");
|
|
2189
2377
|
Matter5.Body.setStatic(entry.body, false);
|
|
2190
2378
|
}
|
|
2191
2379
|
if (!entry.tags.includes("grabable")) {
|
|
@@ -2193,34 +2381,66 @@ var OverlayScene = class {
|
|
|
2193
2381
|
}
|
|
2194
2382
|
}
|
|
2195
2383
|
/**
|
|
2196
|
-
* Add a tag to an object.
|
|
2384
|
+
* Add a tag to an object. Tags drive behavior — adding a tag activates the associated effect.
|
|
2385
|
+
* For 'gravity_override', also pass a gravityOverride value via setObjectGravityOverride first,
|
|
2386
|
+
* or the tag will default to {x:0, y:0} (hovering).
|
|
2197
2387
|
*/
|
|
2198
2388
|
addTag(id, tag) {
|
|
2199
2389
|
const entry = this.objects.get(id);
|
|
2200
2390
|
if (!entry) return;
|
|
2201
2391
|
if (!entry.tags.includes(tag)) {
|
|
2202
2392
|
entry.tags.push(tag);
|
|
2203
|
-
if (tag === "
|
|
2204
|
-
Matter5.Body.setStatic(entry.body,
|
|
2393
|
+
if (tag === "static") {
|
|
2394
|
+
Matter5.Body.setStatic(entry.body, true);
|
|
2395
|
+
} else if (tag === "gravity_override") {
|
|
2396
|
+
if (!entry.gravityOverride) entry.gravityOverride = { x: 0, y: 0 };
|
|
2397
|
+
this.gravityOverrideEntries.add(entry);
|
|
2398
|
+
} else if (tag === "follow_window") {
|
|
2399
|
+
if (!entry.followTarget) entry.followTarget = "mouse";
|
|
2400
|
+
this.followWindowEntries.add(entry);
|
|
2401
|
+
} else if (tag === "speed_override") {
|
|
2402
|
+
if (entry.speedOverride === void 0) entry.speedOverride = 1;
|
|
2403
|
+
} else if (tag === "mass_override") {
|
|
2404
|
+
if (entry.originalMass === void 0) entry.originalMass = entry.body.mass;
|
|
2405
|
+
if (entry.massOverride === void 0) entry.massOverride = Math.round(entry.body.mass);
|
|
2406
|
+
Matter5.Body.setMass(entry.body, entry.massOverride);
|
|
2205
2407
|
}
|
|
2206
2408
|
}
|
|
2207
2409
|
}
|
|
2208
2410
|
/**
|
|
2209
|
-
* Remove a tag from an object.
|
|
2411
|
+
* Remove a tag from an object. Tags drive behavior — removing a tag deactivates the associated effect.
|
|
2210
2412
|
*/
|
|
2211
2413
|
removeTag(id, tag) {
|
|
2212
2414
|
const entry = this.objects.get(id);
|
|
2213
2415
|
if (!entry) return;
|
|
2416
|
+
if (tag === entry.entityTag) {
|
|
2417
|
+
logger.warn("OverlayScene", `Cannot remove entity tag '${tag}' from '${id}' \u2014 entity tags are permanent identifiers and cannot be removed`);
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2214
2420
|
const index = entry.tags.indexOf(tag);
|
|
2215
2421
|
if (index !== -1) {
|
|
2216
2422
|
entry.tags.splice(index, 1);
|
|
2217
|
-
if (tag === "
|
|
2218
|
-
Matter5.Body.setStatic(entry.body,
|
|
2423
|
+
if (tag === "static") {
|
|
2424
|
+
Matter5.Body.setStatic(entry.body, false);
|
|
2425
|
+
} else if (tag === "gravity_override") {
|
|
2426
|
+
this.gravityOverrideEntries.delete(entry);
|
|
2427
|
+
entry.gravityOverride = void 0;
|
|
2428
|
+
} else if (tag === "follow_window") {
|
|
2429
|
+
entry.followTarget = void 0;
|
|
2430
|
+
this.followWindowEntries.delete(entry);
|
|
2431
|
+
} else if (tag === "speed_override") {
|
|
2432
|
+
entry.speedOverride = void 0;
|
|
2433
|
+
} else if (tag === "mass_override") {
|
|
2434
|
+
if (entry.originalMass !== void 0) {
|
|
2435
|
+
Matter5.Body.setMass(entry.body, entry.originalMass);
|
|
2436
|
+
}
|
|
2437
|
+
entry.massOverride = void 0;
|
|
2438
|
+
entry.originalMass = void 0;
|
|
2219
2439
|
}
|
|
2220
2440
|
}
|
|
2221
2441
|
}
|
|
2222
2442
|
/**
|
|
2223
|
-
* Release an object (
|
|
2443
|
+
* Release an object (remove 'static' tag to make it dynamic).
|
|
2224
2444
|
* Convenience method - equivalent to addFallingTag().
|
|
2225
2445
|
*/
|
|
2226
2446
|
releaseObject(id) {
|
|
@@ -2235,7 +2455,7 @@ var OverlayScene = class {
|
|
|
2235
2455
|
}
|
|
2236
2456
|
}
|
|
2237
2457
|
/**
|
|
2238
|
-
* Release all static objects (
|
|
2458
|
+
* Release all static objects (remove 'static' tag, add 'grabable' tag).
|
|
2239
2459
|
*/
|
|
2240
2460
|
releaseAllObjects() {
|
|
2241
2461
|
for (const [id] of this.objects) {
|
|
@@ -2243,7 +2463,7 @@ var OverlayScene = class {
|
|
|
2243
2463
|
}
|
|
2244
2464
|
}
|
|
2245
2465
|
/**
|
|
2246
|
-
* Release objects by tag (
|
|
2466
|
+
* Release objects by tag (remove 'static' tag, add 'grabable' tag to matching objects).
|
|
2247
2467
|
*/
|
|
2248
2468
|
releaseObjectsByTag(tag) {
|
|
2249
2469
|
for (const [id, entry] of this.objects) {
|
|
@@ -2257,6 +2477,9 @@ var OverlayScene = class {
|
|
|
2257
2477
|
if (!entry) return;
|
|
2258
2478
|
this.emitLifecycleEvent("objectRemoved", this.toObjectState(entry));
|
|
2259
2479
|
Matter5.Composite.remove(this.engine.world, entry.body);
|
|
2480
|
+
this.gravityOverrideEntries.delete(entry);
|
|
2481
|
+
this.followWindowEntries.delete(entry);
|
|
2482
|
+
this.pressureObstacleIds.delete(id);
|
|
2260
2483
|
this.objects.delete(id);
|
|
2261
2484
|
}
|
|
2262
2485
|
removeObjects(ids) {
|
|
@@ -2269,6 +2492,9 @@ var OverlayScene = class {
|
|
|
2269
2492
|
Matter5.Composite.remove(this.engine.world, entry.body);
|
|
2270
2493
|
}
|
|
2271
2494
|
this.objects.clear();
|
|
2495
|
+
this.gravityOverrideEntries.clear();
|
|
2496
|
+
this.followWindowEntries.clear();
|
|
2497
|
+
this.pressureObstacleIds.clear();
|
|
2272
2498
|
}
|
|
2273
2499
|
removeObjectsByTag(tag) {
|
|
2274
2500
|
const toRemove = [];
|
|
@@ -2426,14 +2652,14 @@ var OverlayScene = class {
|
|
|
2426
2652
|
}
|
|
2427
2653
|
}
|
|
2428
2654
|
/**
|
|
2429
|
-
* Set the velocity of an object.
|
|
2655
|
+
* Set the velocity of an object. Y axis uses physical convention: negative = down, positive = up.
|
|
2430
2656
|
* @param objectId - The ID of the object
|
|
2431
2657
|
* @param velocity - The velocity vector to set
|
|
2432
2658
|
*/
|
|
2433
2659
|
setVelocity(objectId, velocity) {
|
|
2434
2660
|
const entry = this.objects.get(objectId);
|
|
2435
2661
|
if (!entry) return;
|
|
2436
|
-
Matter5.Body.setVelocity(entry.body, velocity);
|
|
2662
|
+
Matter5.Body.setVelocity(entry.body, { x: velocity.x, y: -velocity.y });
|
|
2437
2663
|
}
|
|
2438
2664
|
/**
|
|
2439
2665
|
* Set the position of an object.
|
|
@@ -2656,7 +2882,7 @@ var OverlayScene = class {
|
|
|
2656
2882
|
const width = config.width ?? element.offsetWidth;
|
|
2657
2883
|
const height = config.height ?? element.offsetHeight;
|
|
2658
2884
|
const tags = config.tags ?? [];
|
|
2659
|
-
const isStatic =
|
|
2885
|
+
const isStatic = tags.includes("static");
|
|
2660
2886
|
const body = Matter5.Bodies.rectangle(x, y, width, height, {
|
|
2661
2887
|
isStatic,
|
|
2662
2888
|
label: `dom-${crypto.randomUUID().slice(0, 8)}`,
|
|
@@ -2664,6 +2890,8 @@ var OverlayScene = class {
|
|
|
2664
2890
|
// Don't render the body, DOM element is the visual
|
|
2665
2891
|
});
|
|
2666
2892
|
const id = body.label;
|
|
2893
|
+
const entityTag = `dom-${id.slice(4, 8)}`;
|
|
2894
|
+
tags.push(entityTag);
|
|
2667
2895
|
let pressureThreshold;
|
|
2668
2896
|
if (config.pressureThreshold) {
|
|
2669
2897
|
pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
|
|
@@ -2677,6 +2905,7 @@ var OverlayScene = class {
|
|
|
2677
2905
|
id,
|
|
2678
2906
|
body,
|
|
2679
2907
|
tags,
|
|
2908
|
+
entityTag,
|
|
2680
2909
|
spawnTime: performance.now(),
|
|
2681
2910
|
pressureThreshold,
|
|
2682
2911
|
weight: config.weight ?? 1,
|
|
@@ -2691,12 +2920,13 @@ var OverlayScene = class {
|
|
|
2691
2920
|
this.updateDOMElementTransform(entry);
|
|
2692
2921
|
if (isStatic && pressureThreshold !== void 0) {
|
|
2693
2922
|
this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
|
|
2923
|
+
this.pressureObstacleIds.add(id);
|
|
2694
2924
|
}
|
|
2695
2925
|
if (clicksRemaining !== void 0) {
|
|
2696
2926
|
const clickHandler = () => {
|
|
2697
2927
|
const currentEntry = this.objects.get(id);
|
|
2698
2928
|
if (!currentEntry) return;
|
|
2699
|
-
if (currentEntry.tags.includes("
|
|
2929
|
+
if (!currentEntry.tags.includes("static")) return;
|
|
2700
2930
|
if (currentEntry.clicksRemaining === void 0) return;
|
|
2701
2931
|
currentEntry.clicksRemaining--;
|
|
2702
2932
|
logger.debug("OverlayScene", `Click on DOM element: ${currentEntry.clicksRemaining} clicks remaining`);
|
|
@@ -2741,8 +2971,13 @@ var OverlayScene = class {
|
|
|
2741
2971
|
const fontName = config.fontName ?? this.getDefaultFont()?.name ?? "handwritten";
|
|
2742
2972
|
const basePath = `${fontsBasePath}${fontName}/`;
|
|
2743
2973
|
const stringTag = config.stringTag ?? `str-${crypto.randomUUID().slice(0, 8)}`;
|
|
2744
|
-
const
|
|
2745
|
-
const
|
|
2974
|
+
const isStatic = config.isStatic !== false;
|
|
2975
|
+
const baseTags = [...config.tags ?? []];
|
|
2976
|
+
if (isStatic && !baseTags.includes("static")) baseTags.push("static");
|
|
2977
|
+
else if (!isStatic) {
|
|
2978
|
+
const i = baseTags.indexOf("static");
|
|
2979
|
+
if (i !== -1) baseTags.splice(i, 1);
|
|
2980
|
+
}
|
|
2746
2981
|
const letterColor = config.letterColor;
|
|
2747
2982
|
const letterIds = [];
|
|
2748
2983
|
const letterMap = /* @__PURE__ */ new Map();
|
|
@@ -2884,8 +3119,10 @@ var OverlayScene = class {
|
|
|
2884
3119
|
const resolvedChar = charFileNames.get(char) ?? char;
|
|
2885
3120
|
const originalImageUrl = `${basePath}${resolvedChar}.png`;
|
|
2886
3121
|
const imageUrl = letterColor ? await tintImage(originalImageUrl, letterColor) : originalImageUrl;
|
|
2887
|
-
const tags = [...config.tags ?? [], stringTag, wordTag, `letter-${char}`, `letter-index-${globalCharIndex}`];
|
|
2888
3122
|
const id = crypto.randomUUID();
|
|
3123
|
+
const safeChar = char.match(/[a-zA-Z0-9]/) ? char.toLowerCase() : "sym";
|
|
3124
|
+
const entityTag = `letter-${safeChar}-${id.slice(0, 4)}`;
|
|
3125
|
+
const tags = [...baseTags, entityTag, stringTag, wordTag, `letter-${char}`, `letter-index-${globalCharIndex}`];
|
|
2889
3126
|
const objectConfig = {
|
|
2890
3127
|
x: centerX,
|
|
2891
3128
|
y: centerY,
|
|
@@ -2931,10 +3168,12 @@ var OverlayScene = class {
|
|
|
2931
3168
|
originalPosition: shadow || clicksRemaining !== void 0 ? { x: centerX, y: centerY } : void 0,
|
|
2932
3169
|
imageUrl: shadow || clicksRemaining !== void 0 ? imageUrl : void 0,
|
|
2933
3170
|
imageSize: shadow || clicksRemaining !== void 0 ? letterSize : void 0,
|
|
2934
|
-
clicksRemaining
|
|
3171
|
+
clicksRemaining,
|
|
3172
|
+
entityTag
|
|
2935
3173
|
};
|
|
2936
3174
|
this.objects.set(id, entry);
|
|
2937
3175
|
Matter5.Composite.add(this.engine.world, result.body);
|
|
3176
|
+
if (pressureThreshold !== void 0) this.pressureObstacleIds.add(id);
|
|
2938
3177
|
letterIds.push(id);
|
|
2939
3178
|
letterMap.set(`${char}-${globalCharIndex}`, id);
|
|
2940
3179
|
debugInfo.push({
|
|
@@ -2985,15 +3224,13 @@ var OverlayScene = class {
|
|
|
2985
3224
|
}
|
|
2986
3225
|
/**
|
|
2987
3226
|
* Spawn falling text objects from a string.
|
|
2988
|
-
* Same as addTextObstacles but
|
|
3227
|
+
* Same as addTextObstacles but ensures objects are dynamic (removes 'static' tag if present).
|
|
2989
3228
|
*/
|
|
2990
3229
|
async spawnFallingTextObstacles(config) {
|
|
2991
|
-
|
|
2992
|
-
if (!tags.includes("falling")) tags.push("falling");
|
|
2993
|
-
return this.addTextObstacles({ ...config, tags });
|
|
3230
|
+
return this.addTextObstacles({ ...config, isStatic: false });
|
|
2994
3231
|
}
|
|
2995
3232
|
/**
|
|
2996
|
-
* Release all letters in a word (
|
|
3233
|
+
* Release all letters in a word (remove 'static' tag so they fall).
|
|
2997
3234
|
* @param wordTag - The word tag returned from addTextObstacles
|
|
2998
3235
|
*/
|
|
2999
3236
|
releaseTextObstacles(wordTag) {
|
|
@@ -3040,8 +3277,13 @@ var OverlayScene = class {
|
|
|
3040
3277
|
const { x, y, fontSize, fontUrl } = config;
|
|
3041
3278
|
const text = config.text.replace(/\\n/g, "\n");
|
|
3042
3279
|
const stringTag = config.stringTag ?? `str-${crypto.randomUUID().slice(0, 8)}`;
|
|
3043
|
-
const
|
|
3044
|
-
const
|
|
3280
|
+
const isStatic = config.isStatic !== false;
|
|
3281
|
+
const baseTags = [...config.tags ?? []];
|
|
3282
|
+
if (isStatic && !baseTags.includes("static")) baseTags.push("static");
|
|
3283
|
+
else if (!isStatic) {
|
|
3284
|
+
const i = baseTags.indexOf("static");
|
|
3285
|
+
if (i !== -1) baseTags.splice(i, 1);
|
|
3286
|
+
}
|
|
3045
3287
|
const fillColor = config.fillColor ?? "#ffffff";
|
|
3046
3288
|
const fillColors = config.fillColors;
|
|
3047
3289
|
const lineHeight = config.lineHeight ?? fontSize * 1.2;
|
|
@@ -3115,7 +3357,9 @@ var OverlayScene = class {
|
|
|
3115
3357
|
const wordTag = `${stringTag}-word-${currentWordIndex}`;
|
|
3116
3358
|
wordTagsSet.add(wordTag);
|
|
3117
3359
|
const id = crypto.randomUUID();
|
|
3118
|
-
const
|
|
3360
|
+
const safeChar = char.match(/[a-zA-Z0-9]/) ? char.toLowerCase() : "sym";
|
|
3361
|
+
const entityTag = `letter-${safeChar}-${id.slice(0, 4)}`;
|
|
3362
|
+
const tags = [...baseTags, entityTag, stringTag, wordTag, `letter-${char}`, `letter-index-${globalCharIndex}`];
|
|
3119
3363
|
const bbox = glyphData.boundingBox;
|
|
3120
3364
|
const glyphWidth = bbox ? bbox.x2 - bbox.x1 : glyphData.advanceWidth;
|
|
3121
3365
|
const glyphHeight = bbox ? bbox.y2 - bbox.y1 : fontSize;
|
|
@@ -3177,10 +3421,12 @@ var OverlayScene = class {
|
|
|
3177
3421
|
weight,
|
|
3178
3422
|
shadow,
|
|
3179
3423
|
originalPosition: shadow || clicksRemaining !== void 0 ? { x: body.position.x, y: body.position.y } : void 0,
|
|
3180
|
-
clicksRemaining
|
|
3424
|
+
clicksRemaining,
|
|
3425
|
+
entityTag
|
|
3181
3426
|
};
|
|
3182
3427
|
this.objects.set(id, entry);
|
|
3183
3428
|
Matter5.Composite.add(this.engine.world, body);
|
|
3429
|
+
if (pressureThreshold !== void 0) this.pressureObstacleIds.add(id);
|
|
3184
3430
|
letterIds.push(id);
|
|
3185
3431
|
letterMap.set(`${char}-${globalCharIndex}`, id);
|
|
3186
3432
|
currentX += glyphData.advanceWidth;
|
|
@@ -3220,12 +3466,10 @@ var OverlayScene = class {
|
|
|
3220
3466
|
}
|
|
3221
3467
|
/**
|
|
3222
3468
|
* Spawn falling TTF text objects.
|
|
3223
|
-
* Same as addTTFTextObstacles but
|
|
3469
|
+
* Same as addTTFTextObstacles but ensures objects are dynamic (removes 'static' tag if present).
|
|
3224
3470
|
*/
|
|
3225
3471
|
async spawnFallingTTFTextObstacles(config) {
|
|
3226
|
-
|
|
3227
|
-
if (!tags.includes("falling")) tags.push("falling");
|
|
3228
|
-
return this.addTTFTextObstacles({ ...config, tags });
|
|
3472
|
+
return this.addTTFTextObstacles({ ...config, isStatic: false });
|
|
3229
3473
|
}
|
|
3230
3474
|
// ==================== COMBINED TAG METHODS ====================
|
|
3231
3475
|
removeAllByTag(tag) {
|
|
@@ -3336,37 +3580,29 @@ var OverlayScene = class {
|
|
|
3336
3580
|
entry.domElement.style.setProperty("top", `${y - height / 2}px`, "important");
|
|
3337
3581
|
entry.domElement.style.setProperty("transform", `rotate(${angleDeg}deg)`, "important");
|
|
3338
3582
|
}
|
|
3339
|
-
|
|
3583
|
+
/** TTL expiration + below-floor despawn in a single O(N) pass */
|
|
3584
|
+
checkExpiration() {
|
|
3340
3585
|
const now = performance.now();
|
|
3341
|
-
const expiredObjects = [];
|
|
3342
|
-
for (const [id, entry] of this.objects) {
|
|
3343
|
-
if (entry.ttl !== void 0 && now - entry.spawnTime >= entry.ttl) {
|
|
3344
|
-
expiredObjects.push(id);
|
|
3345
|
-
}
|
|
3346
|
-
}
|
|
3347
|
-
for (const id of expiredObjects) {
|
|
3348
|
-
this.removeObject(id);
|
|
3349
|
-
}
|
|
3350
|
-
}
|
|
3351
|
-
/** Despawn objects that have fallen below the floor by the configured distance */
|
|
3352
|
-
checkDespawnBelowFloor() {
|
|
3353
3586
|
const despawnDistance = this.config.despawnBelowFloor ?? 1;
|
|
3354
3587
|
const containerHeight = this.config.bounds.bottom - this.config.bounds.top;
|
|
3355
3588
|
const despawnY = this.config.bounds.bottom + containerHeight * despawnDistance;
|
|
3356
|
-
const
|
|
3589
|
+
const toRemove = [];
|
|
3357
3590
|
for (const [id, entry] of this.objects) {
|
|
3358
|
-
if (entry.
|
|
3359
|
-
|
|
3591
|
+
if (entry.ttl !== void 0 && now - entry.spawnTime >= entry.ttl) {
|
|
3592
|
+
toRemove.push(id);
|
|
3593
|
+
} else if (entry.body.position.y > despawnY) {
|
|
3594
|
+
toRemove.push(id);
|
|
3360
3595
|
}
|
|
3361
3596
|
}
|
|
3362
|
-
for (const id of
|
|
3597
|
+
for (const id of toRemove) {
|
|
3363
3598
|
this.removeObject(id);
|
|
3364
3599
|
}
|
|
3365
3600
|
}
|
|
3366
3601
|
fireUpdateCallbacks() {
|
|
3602
|
+
if (this.updateCallbacks.length === 0) return;
|
|
3367
3603
|
const objects = [];
|
|
3368
|
-
this.objects.
|
|
3369
|
-
if (entry.tags.includes("
|
|
3604
|
+
for (const entry of this.objects.values()) {
|
|
3605
|
+
if (!entry.tags.includes("static")) {
|
|
3370
3606
|
objects.push({
|
|
3371
3607
|
id: entry.id,
|
|
3372
3608
|
x: entry.body.position.x,
|
|
@@ -3375,28 +3611,37 @@ var OverlayScene = class {
|
|
|
3375
3611
|
tags: entry.tags
|
|
3376
3612
|
});
|
|
3377
3613
|
}
|
|
3378
|
-
}
|
|
3614
|
+
}
|
|
3379
3615
|
const data = { objects };
|
|
3380
|
-
this.updateCallbacks
|
|
3616
|
+
for (const cb of this.updateCallbacks) cb(data);
|
|
3381
3617
|
}
|
|
3382
3618
|
};
|
|
3383
3619
|
|
|
3384
3620
|
// src/tags.ts
|
|
3385
|
-
var
|
|
3621
|
+
var TAG_STATIC = "static";
|
|
3386
3622
|
var TAG_FOLLOW_WINDOW = "follow_window";
|
|
3387
3623
|
var TAG_GRABABLE = "grabable";
|
|
3624
|
+
var TAG_GRAVITY_OVERRIDE = "gravity_override";
|
|
3625
|
+
var TAG_SPEED_OVERRIDE = "speed_override";
|
|
3626
|
+
var TAG_MASS_OVERRIDE = "mass_override";
|
|
3388
3627
|
var TAGS = {
|
|
3389
|
-
|
|
3628
|
+
STATIC: TAG_STATIC,
|
|
3390
3629
|
FOLLOW_WINDOW: TAG_FOLLOW_WINDOW,
|
|
3391
|
-
GRABABLE: TAG_GRABABLE
|
|
3630
|
+
GRABABLE: TAG_GRABABLE,
|
|
3631
|
+
GRAVITY_OVERRIDE: TAG_GRAVITY_OVERRIDE,
|
|
3632
|
+
SPEED_OVERRIDE: TAG_SPEED_OVERRIDE,
|
|
3633
|
+
MASS_OVERRIDE: TAG_MASS_OVERRIDE
|
|
3392
3634
|
};
|
|
3393
3635
|
export {
|
|
3394
3636
|
BackgroundManager,
|
|
3395
3637
|
OverlayScene,
|
|
3396
3638
|
TAGS,
|
|
3397
|
-
TAG_FALLING,
|
|
3398
3639
|
TAG_FOLLOW_WINDOW,
|
|
3399
3640
|
TAG_GRABABLE,
|
|
3641
|
+
TAG_GRAVITY_OVERRIDE,
|
|
3642
|
+
TAG_MASS_OVERRIDE,
|
|
3643
|
+
TAG_SPEED_OVERRIDE,
|
|
3644
|
+
TAG_STATIC,
|
|
3400
3645
|
clearFontCache,
|
|
3401
3646
|
getGlyphData,
|
|
3402
3647
|
getKerning,
|