@blorkfield/overlay-core 0.5.0 → 0.5.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/README.md CHANGED
@@ -48,6 +48,15 @@ Static obstacles track how many dynamic objects are resting on them. When the ac
48
48
 
49
49
  The floor can be divided into independent segments, each with its own pressure threshold. When a segment receives too much weight, it collapses and objects fall through.
50
50
 
51
+ | Option | Description |
52
+ |--------|-------------|
53
+ | `thickness` | Segment height in pixels (single value or array per segment) |
54
+ | `color` | Segment fill color (single value or array per segment) - makes floor visible |
55
+ | `minIntegrity` | Minimum segments required. When remaining segments drop below this, all collapse |
56
+ | `segmentWidths` | Proportional widths for each segment (array that sums to 1.0, e.g., `[0.2, 0.3, 0.5]`) |
57
+
58
+ Example: With 10 segments and `minIntegrity: 7`, once 4 segments have collapsed (leaving 6), all remaining segments collapse together.
59
+
51
60
  ## Quick Start
52
61
 
53
62
  ```typescript
@@ -72,6 +81,8 @@ scene.start();
72
81
 
73
82
  ## Spawning Objects
74
83
 
84
+ All objects are created through `spawnObject()` (or `spawnObjectAsync()` for images). The same config supports canvas-rendered shapes, image-based shapes, and DOM elements.
85
+
75
86
  ### Basic Shapes
76
87
 
77
88
  ```typescript
@@ -118,6 +129,57 @@ const id = await scene.spawnObjectAsync({
118
129
  });
119
130
  ```
120
131
 
132
+ ### DOM Elements
133
+
134
+ Pass a DOM element via the `element` property to link it to physics. The element will move with the physics body when it becomes dynamic.
135
+
136
+ ```typescript
137
+ const contentBox = document.getElementById('content-box');
138
+
139
+ scene.spawnObject({
140
+ element: contentBox,
141
+ x: boxX,
142
+ y: boxY,
143
+ width: contentBox.offsetWidth,
144
+ height: contentBox.offsetHeight,
145
+ tags: ['grabable'],
146
+ pressureThreshold: { value: 50 },
147
+ shadow: { opacity: 0.3 },
148
+ clickToFall: { clicks: 5 }
149
+ });
150
+ ```
151
+
152
+ When a DOM element collapses:
153
+ - The element's CSS transform is updated each frame to follow physics
154
+ - Shadow creates a cloned DOM element at the original position
155
+
156
+ ### Pressure, Shadow, and Click Behavior
157
+
158
+ These options work on any spawned object (shapes, images, or DOM elements):
159
+
160
+ ```typescript
161
+ scene.spawnObject({
162
+ x: 200,
163
+ y: 300,
164
+ width: 150,
165
+ height: 30,
166
+ fillStyle: '#333',
167
+ tags: ['grabable'],
168
+
169
+ // Collapse when 20 units of pressure accumulate
170
+ pressureThreshold: { value: 20 },
171
+
172
+ // This object contributes 5 pressure when resting on something
173
+ weight: 5,
174
+
175
+ // Leave a faded copy when collapsed (true = 0.3 opacity default)
176
+ shadow: { opacity: 0.3 },
177
+
178
+ // Collapse after being clicked 3 times
179
+ clickToFall: { clicks: 3 }
180
+ });
181
+ ```
182
+
121
183
  ## Text Obstacles
122
184
 
123
185
  ### PNG Based Text
@@ -263,7 +325,10 @@ const scene = new OverlayScene(canvas, {
263
325
  background: '#16213e',
264
326
  floorConfig: {
265
327
  segments: 10,
266
- threshold: 100
328
+ threshold: 100,
329
+ thickness: 20,
330
+ color: '#3a4a6a', // Makes floor visible
331
+ minIntegrity: 7 // All collapse if fewer than 7 remain
267
332
  },
268
333
  despawnBelowFloor: 1.0
269
334
  });
@@ -276,7 +341,11 @@ const scene = new OverlayScene(canvas, {
276
341
  | `debug` | false | Show collision wireframes |
277
342
  | `background` | transparent | Canvas background color |
278
343
  | `floorConfig.segments` | 1 | Number of floor segments |
279
- | `floorConfig.threshold` | none | Pressure threshold for floor collapse |
344
+ | `floorConfig.threshold` | none | Pressure threshold for collapse (number or array per segment) |
345
+ | `floorConfig.thickness` | 50 | Floor thickness in pixels (number or array per segment) |
346
+ | `floorConfig.color` | none | Floor color - makes segments visible (string or array per segment) |
347
+ | `floorConfig.minIntegrity` | none | Minimum segments required, otherwise all collapse |
348
+ | `floorConfig.segmentWidths` | none | Proportional widths for each segment (array that sums to 1.0) |
280
349
  | `despawnBelowFloor` | 1.0 | Distance below floor to despawn objects (as fraction of height) |
281
350
 
282
351
  ## Pressure Tracking
package/dist/index.cjs CHANGED
@@ -508,7 +508,7 @@ function createBodyFromVertices(id, x, y, vertices, renderOptions) {
508
508
  function createBoundariesWithFloorConfig(bounds, floorConfig) {
509
509
  const width = bounds.right - bounds.left;
510
510
  const height = bounds.bottom - bounds.top;
511
- const options = { isStatic: true, render: { visible: false } };
511
+ const wallOptions = { isStatic: true, render: { visible: false } };
512
512
  const walls = [
513
513
  // Left wall
514
514
  import_matter_js2.default.Bodies.rectangle(
@@ -516,7 +516,7 @@ function createBoundariesWithFloorConfig(bounds, floorConfig) {
516
516
  bounds.top + height / 2,
517
517
  BOUNDARY_THICKNESS,
518
518
  height,
519
- { ...options, label: "leftWall" }
519
+ { ...wallOptions, label: "leftWall" }
520
520
  ),
521
521
  // Right wall
522
522
  import_matter_js2.default.Bodies.rectangle(
@@ -524,23 +524,44 @@ function createBoundariesWithFloorConfig(bounds, floorConfig) {
524
524
  bounds.top + height / 2,
525
525
  BOUNDARY_THICKNESS,
526
526
  height,
527
- { ...options, label: "rightWall" }
527
+ { ...wallOptions, label: "rightWall" }
528
528
  )
529
529
  ];
530
530
  const segmentCount = floorConfig?.segments ?? 1;
531
- const segmentWidth = width / segmentCount;
532
531
  const floorSegments = [];
532
+ let segmentWidths;
533
+ if (floorConfig?.segmentWidths && floorConfig.segmentWidths.length === segmentCount) {
534
+ const sum = floorConfig.segmentWidths.reduce((a, b) => a + b, 0);
535
+ segmentWidths = floorConfig.segmentWidths.map((w) => w / sum * width);
536
+ } else {
537
+ const equalWidth = width / segmentCount;
538
+ segmentWidths = Array(segmentCount).fill(equalWidth);
539
+ }
540
+ let currentX = bounds.left;
533
541
  for (let i = 0; i < segmentCount; i++) {
534
- const segmentX = bounds.left + (i + 0.5) * segmentWidth;
542
+ const segmentWidth = segmentWidths[i];
543
+ const thickness = floorConfig?.thickness !== void 0 ? Array.isArray(floorConfig.thickness) ? floorConfig.thickness[i] ?? BOUNDARY_THICKNESS : floorConfig.thickness : BOUNDARY_THICKNESS;
544
+ const color = floorConfig?.color !== void 0 ? Array.isArray(floorConfig.color) ? floorConfig.color[i] : floorConfig.color : void 0;
545
+ const segmentX = currentX + segmentWidth / 2;
546
+ const segmentY = bounds.bottom - thickness / 2;
547
+ const segmentOptions = {
548
+ isStatic: true,
549
+ label: `floor-segment-${i}`,
550
+ render: {
551
+ visible: color !== void 0,
552
+ fillStyle: color ?? "#888888"
553
+ }
554
+ };
535
555
  floorSegments.push(
536
556
  import_matter_js2.default.Bodies.rectangle(
537
557
  segmentX,
538
- bounds.bottom + BOUNDARY_THICKNESS / 2,
558
+ segmentY,
539
559
  segmentWidth,
540
- BOUNDARY_THICKNESS,
541
- { ...options, label: `floor-segment-${i}` }
560
+ thickness,
561
+ segmentOptions
542
562
  )
543
563
  );
564
+ currentX += segmentWidth;
544
565
  }
545
566
  return { walls, floorSegments };
546
567
  }
@@ -1232,6 +1253,7 @@ var OverlayScene = class {
1232
1253
  this.boundaries = [...boundariesResult.walls, ...boundariesResult.floorSegments];
1233
1254
  this.floorSegments = boundariesResult.floorSegments;
1234
1255
  import_matter_js5.default.Composite.add(this.engine.world, this.boundaries);
1256
+ this.checkInitialFloorIntegrity();
1235
1257
  this.mouse = import_matter_js5.default.Mouse.create(canvas);
1236
1258
  this.mouseConstraint = import_matter_js5.default.MouseConstraint.create(this.engine, {
1237
1259
  mouse: this.mouse,
@@ -1350,7 +1372,8 @@ var OverlayScene = class {
1350
1372
  if (onObstacles.has(dyn.id)) continue;
1351
1373
  const dynBounds = dyn.body.bounds;
1352
1374
  const horizontalOverlap = dynBounds.max.x > segmentBounds.min.x && dynBounds.min.x < segmentBounds.max.x;
1353
- if (horizontalOverlap) {
1375
+ const nearFloor = dynBounds.max.y >= segmentBounds.min.y - 10;
1376
+ if (horizontalOverlap && nearFloor) {
1354
1377
  resting.add(dyn.id);
1355
1378
  }
1356
1379
  }
@@ -1376,17 +1399,50 @@ var OverlayScene = class {
1376
1399
  const objectIds = this.floorSegmentPressure.get(i);
1377
1400
  const pressure = objectIds ? this.calculateWeightedPressure(objectIds) : 0;
1378
1401
  if (pressure >= threshold) {
1379
- this.collapseFloorSegment(i, pressure, threshold);
1402
+ this.collapseFloorSegment(i, `pressure ${pressure} >= threshold ${threshold}`);
1380
1403
  }
1381
1404
  }
1382
1405
  }
1383
1406
  /** Collapse a single floor segment */
1384
- collapseFloorSegment(index, pressure, threshold) {
1407
+ collapseFloorSegment(index, reason) {
1385
1408
  if (this.collapsedSegments.has(index)) return;
1386
1409
  this.collapsedSegments.add(index);
1387
1410
  const segment = this.floorSegments[index];
1388
1411
  import_matter_js5.default.Composite.remove(this.engine.world, segment);
1389
- console.log(`[Pressure] Floor segment ${index} collapsed! (pressure: ${pressure} >= ${threshold})`);
1412
+ logger.debug("OverlayScene", `Floor segment ${index} collapsed: ${reason}`);
1413
+ this.checkFloorIntegrity();
1414
+ }
1415
+ /** Check if floor integrity requirement is violated and collapse all remaining if so */
1416
+ checkFloorIntegrity() {
1417
+ const minIntegrity = this.config.floorConfig?.minIntegrity;
1418
+ if (minIntegrity === void 0) return;
1419
+ const totalSegments = this.floorSegments.length;
1420
+ const remainingSegments = totalSegments - this.collapsedSegments.size;
1421
+ if (remainingSegments < minIntegrity && remainingSegments > 0) {
1422
+ logger.debug("OverlayScene", `Floor integrity failed: ${remainingSegments} remaining < ${minIntegrity} required. Collapsing all.`);
1423
+ for (let i = 0; i < totalSegments; i++) {
1424
+ if (!this.collapsedSegments.has(i)) {
1425
+ this.collapsedSegments.add(i);
1426
+ const segment = this.floorSegments[i];
1427
+ import_matter_js5.default.Composite.remove(this.engine.world, segment);
1428
+ logger.debug("OverlayScene", `Floor segment ${i} collapsed: integrity failure cascade`);
1429
+ }
1430
+ }
1431
+ }
1432
+ }
1433
+ /** Check floor integrity on initialization (handles minIntegrity > segments) */
1434
+ checkInitialFloorIntegrity() {
1435
+ const minIntegrity = this.config.floorConfig?.minIntegrity;
1436
+ if (minIntegrity === void 0) return;
1437
+ const totalSegments = this.floorSegments.length;
1438
+ if (totalSegments < minIntegrity) {
1439
+ logger.debug("OverlayScene", `Floor integrity impossible: ${totalSegments} segments < ${minIntegrity} required. Collapsing all immediately.`);
1440
+ for (let i = 0; i < totalSegments; i++) {
1441
+ this.collapsedSegments.add(i);
1442
+ const segment = this.floorSegments[i];
1443
+ import_matter_js5.default.Composite.remove(this.engine.world, segment);
1444
+ }
1445
+ }
1390
1446
  }
1391
1447
  /** Log a summary of pressure on all obstacles, grouped by word */
1392
1448
  logPressureSummary() {
@@ -1642,6 +1698,7 @@ var OverlayScene = class {
1642
1698
  this.collapsedSegments.clear();
1643
1699
  this.floorSegmentPressure.clear();
1644
1700
  import_matter_js5.default.Composite.add(this.engine.world, this.boundaries);
1701
+ this.checkInitialFloorIntegrity();
1645
1702
  this.render.options.width = width;
1646
1703
  this.render.options.height = height;
1647
1704
  this.render.canvas.width = width;
@@ -1658,6 +1715,21 @@ var OverlayScene = class {
1658
1715
  * Without 'falling' tag, object is static.
1659
1716
  */
1660
1717
  spawnObject(config) {
1718
+ if (config.element) {
1719
+ const result = this.addDOMObstacleInternal({
1720
+ element: config.element,
1721
+ x: config.x,
1722
+ y: config.y,
1723
+ width: config.width,
1724
+ height: config.height,
1725
+ tags: config.tags,
1726
+ pressureThreshold: config.pressureThreshold,
1727
+ weight: config.weight,
1728
+ shadow: config.shadow === true ? { opacity: 0.3 } : config.shadow || void 0,
1729
+ clickToFall: config.clickToFall
1730
+ });
1731
+ return result.id;
1732
+ }
1661
1733
  const id = crypto.randomUUID();
1662
1734
  const tags = config.tags ?? [];
1663
1735
  const isStatic = !tags.includes("falling");
@@ -1677,6 +1749,17 @@ var OverlayScene = class {
1677
1749
  } else {
1678
1750
  body = createObstacle(id, config, isStatic);
1679
1751
  }
1752
+ let pressureThreshold;
1753
+ if (config.pressureThreshold) {
1754
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
1755
+ }
1756
+ let shadow;
1757
+ if (config.shadow === true) {
1758
+ shadow = { opacity: 0.3 };
1759
+ } else if (config.shadow && typeof config.shadow === "object") {
1760
+ shadow = { opacity: config.shadow.opacity ?? 0.3 };
1761
+ }
1762
+ const clicksRemaining = config.clickToFall?.clicks;
1680
1763
  const entry = {
1681
1764
  id,
1682
1765
  body,
@@ -1684,10 +1767,17 @@ var OverlayScene = class {
1684
1767
  spawnTime: performance.now(),
1685
1768
  ttl: config.ttl,
1686
1769
  despawnEffect: config.despawnEffect,
1687
- weight: config.weight ?? 1
1770
+ weight: config.weight ?? 1,
1771
+ pressureThreshold,
1772
+ shadow,
1773
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
1774
+ clicksRemaining
1688
1775
  };
1689
1776
  this.objects.set(id, entry);
1690
1777
  import_matter_js5.default.Composite.add(this.engine.world, body);
1778
+ if (isStatic && pressureThreshold !== void 0) {
1779
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1780
+ }
1691
1781
  return id;
1692
1782
  }
1693
1783
  /**
@@ -1714,6 +1804,17 @@ var OverlayScene = class {
1714
1804
  } else {
1715
1805
  body = await createObstacleAsync(id, config, isStatic);
1716
1806
  }
1807
+ let pressureThreshold;
1808
+ if (config.pressureThreshold) {
1809
+ pressureThreshold = typeof config.pressureThreshold.value === "number" ? config.pressureThreshold.value : config.pressureThreshold.value[0];
1810
+ }
1811
+ let shadow;
1812
+ if (config.shadow === true) {
1813
+ shadow = { opacity: 0.3 };
1814
+ } else if (config.shadow && typeof config.shadow === "object") {
1815
+ shadow = { opacity: config.shadow.opacity ?? 0.3 };
1816
+ }
1817
+ const clicksRemaining = config.clickToFall?.clicks;
1717
1818
  const entry = {
1718
1819
  id,
1719
1820
  body,
@@ -1721,10 +1822,17 @@ var OverlayScene = class {
1721
1822
  spawnTime: performance.now(),
1722
1823
  ttl: config.ttl,
1723
1824
  despawnEffect: config.despawnEffect,
1724
- weight: config.weight ?? 1
1825
+ weight: config.weight ?? 1,
1826
+ pressureThreshold,
1827
+ shadow,
1828
+ originalPosition: shadow || clicksRemaining !== void 0 ? { x: config.x, y: config.y } : void 0,
1829
+ clicksRemaining
1725
1830
  };
1726
1831
  this.objects.set(id, entry);
1727
1832
  import_matter_js5.default.Composite.add(this.engine.world, body);
1833
+ if (isStatic && pressureThreshold !== void 0) {
1834
+ this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
1835
+ }
1728
1836
  return id;
1729
1837
  }
1730
1838
  /**
@@ -1983,17 +2091,12 @@ var OverlayScene = class {
1983
2091
  areFontsInitialized() {
1984
2092
  return this.fontsInitialized;
1985
2093
  }
1986
- // ==================== DOM OBSTACLE METHODS ====================
2094
+ // ==================== DOM OBSTACLE METHODS (INTERNAL) ====================
1987
2095
  /**
1988
- * Attach a DOM element to physics. The element will follow the physics body
1989
- * and can have pressure threshold, shadow, and click-to-fall behavior.
1990
- *
1991
- * When the element collapses (becomes dynamic), its CSS transform will be
1992
- * updated each frame to match the physics body position and rotation.
1993
- *
1994
- * Shadow creates a cloned DOM element that stays at the original position.
2096
+ * Internal: Attach a DOM element to physics.
2097
+ * Called by spawnObject when element is provided.
1995
2098
  */
1996
- addDOMObstacle(config) {
2099
+ addDOMObstacleInternal(config) {
1997
2100
  const { element, x, y } = config;
1998
2101
  const width = config.width ?? element.offsetWidth;
1999
2102
  const height = config.height ?? element.offsetHeight;
@@ -2030,6 +2133,7 @@ var OverlayScene = class {
2030
2133
  };
2031
2134
  this.objects.set(id, entry);
2032
2135
  import_matter_js5.default.Composite.add(this.engine.world, body);
2136
+ this.updateDOMElementTransform(entry);
2033
2137
  if (isStatic && pressureThreshold !== void 0) {
2034
2138
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2035
2139
  }
@@ -2136,11 +2240,61 @@ var OverlayScene = class {
2136
2240
  maxDimension = Math.max(maxDimension, dims.width, dims.height);
2137
2241
  }
2138
2242
  if (maxDimension === 0) maxDimension = 100;
2243
+ const calculateLineWidth = (line) => {
2244
+ let width = 0;
2245
+ for (const char of line) {
2246
+ if (char === " ") {
2247
+ width += 20;
2248
+ } else if (/^[A-Za-z0-9]$/.test(char)) {
2249
+ const dims = charDimensions.get(char);
2250
+ if (dims) {
2251
+ const scale = letterSize / Math.max(dims.width, dims.height);
2252
+ const scaledWidth = dims.width * scale;
2253
+ const extraSpacing = config.letterSpacing !== void 0 ? config.letterSpacing - scaledWidth : 0;
2254
+ width += scaledWidth + Math.max(0, extraSpacing);
2255
+ }
2256
+ }
2257
+ }
2258
+ return width;
2259
+ };
2260
+ const lineWidths = lines.map((line) => calculateLineWidth(line));
2261
+ const align = config.align ?? "left";
2262
+ const maxLineWidth = Math.max(...lineWidths, 0);
2263
+ let boundsLeft;
2264
+ let boundsRight;
2265
+ switch (align) {
2266
+ case "center":
2267
+ boundsLeft = config.x - maxLineWidth / 2;
2268
+ boundsRight = config.x + maxLineWidth / 2;
2269
+ break;
2270
+ case "right":
2271
+ boundsLeft = config.x - maxLineWidth;
2272
+ boundsRight = config.x;
2273
+ break;
2274
+ default:
2275
+ boundsLeft = config.x;
2276
+ boundsRight = config.x + maxLineWidth;
2277
+ }
2278
+ const boundsTop = config.y - letterSize / 2;
2279
+ const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + letterSize : 0;
2280
+ const boundsBottom = boundsTop + totalHeight;
2139
2281
  let currentY = config.y;
2140
2282
  let globalCharIndex = 0;
2141
- for (const line of lines) {
2283
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2284
+ const line = lines[lineIndex];
2142
2285
  const chars = line.split("");
2143
- let currentX = config.x;
2286
+ const lineWidth = lineWidths[lineIndex];
2287
+ let currentX;
2288
+ switch (align) {
2289
+ case "center":
2290
+ currentX = config.x - lineWidth / 2;
2291
+ break;
2292
+ case "right":
2293
+ currentX = config.x - lineWidth;
2294
+ break;
2295
+ default:
2296
+ currentX = config.x;
2297
+ }
2144
2298
  if (inWord) {
2145
2299
  currentWordIndex++;
2146
2300
  inWord = false;
@@ -2256,12 +2410,21 @@ var OverlayScene = class {
2256
2410
  letterColor,
2257
2411
  lineCount: lines.length
2258
2412
  });
2413
+ const bounds = {
2414
+ left: boundsLeft,
2415
+ right: boundsRight,
2416
+ top: boundsTop,
2417
+ bottom: boundsBottom,
2418
+ width: boundsRight - boundsLeft,
2419
+ height: boundsBottom - boundsTop
2420
+ };
2259
2421
  return {
2260
2422
  letterIds,
2261
2423
  stringTag,
2262
2424
  wordTags,
2263
2425
  letterMap,
2264
- letterDebugInfo: debugInfo
2426
+ letterDebugInfo: debugInfo,
2427
+ bounds
2265
2428
  };
2266
2429
  }
2267
2430
  /**
@@ -2334,10 +2497,43 @@ var OverlayScene = class {
2334
2497
  const fontInfo = this.fonts.find((f) => f.fontUrl === fontUrl);
2335
2498
  const fontFamily = fontInfo?.name ?? "sans-serif";
2336
2499
  const lines = text.split("\n");
2500
+ const lineWidths = lines.map((line) => measureText(loadedFont, line, fontSize));
2501
+ const align = config.align ?? "left";
2502
+ const maxLineWidth = Math.max(...lineWidths, 0);
2503
+ let boundsLeft;
2504
+ let boundsRight;
2505
+ switch (align) {
2506
+ case "center":
2507
+ boundsLeft = x - maxLineWidth / 2;
2508
+ boundsRight = x + maxLineWidth / 2;
2509
+ break;
2510
+ case "right":
2511
+ boundsLeft = x - maxLineWidth;
2512
+ boundsRight = x;
2513
+ break;
2514
+ default:
2515
+ boundsLeft = x;
2516
+ boundsRight = x + maxLineWidth;
2517
+ }
2518
+ const boundsTop = y - fontSize * 0.8;
2519
+ const totalHeight = lines.length > 0 ? (lines.length - 1) * lineHeight + fontSize : 0;
2520
+ const boundsBottom = boundsTop + totalHeight;
2337
2521
  let currentY = y;
2338
2522
  let globalCharIndex = 0;
2339
- for (const line of lines) {
2340
- let currentX = x;
2523
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
2524
+ const line = lines[lineIndex];
2525
+ const lineWidth = lineWidths[lineIndex];
2526
+ let currentX;
2527
+ switch (align) {
2528
+ case "center":
2529
+ currentX = x - lineWidth / 2;
2530
+ break;
2531
+ case "right":
2532
+ currentX = x - lineWidth;
2533
+ break;
2534
+ default:
2535
+ currentX = x;
2536
+ }
2341
2537
  if (inWord) {
2342
2538
  currentWordIndex++;
2343
2539
  inWord = false;
@@ -2447,12 +2643,21 @@ var OverlayScene = class {
2447
2643
  wordTags,
2448
2644
  lineCount: lines.length
2449
2645
  });
2646
+ const bounds = {
2647
+ left: boundsLeft,
2648
+ right: boundsRight,
2649
+ top: boundsTop,
2650
+ bottom: boundsBottom,
2651
+ width: boundsRight - boundsLeft,
2652
+ height: boundsBottom - boundsTop
2653
+ };
2450
2654
  return {
2451
2655
  letterIds,
2452
2656
  stringTag,
2453
2657
  wordTags,
2454
2658
  letterMap,
2455
- letterDebugInfo: []
2659
+ letterDebugInfo: [],
2660
+ bounds
2456
2661
  };
2457
2662
  }
2458
2663
  /**